diff --git a/.gitmodules b/.gitmodules index c9dac09..9afec17 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "skynet"] path = skynet url = git@120.26.43.151:common/skynet.git +[submodule "src/csvdata"] + path = src/csvdata + url = git@120.26.43.151:wasteland/csvdata.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/README.md diff --git a/docs/call 的调用流程分析.txt b/docs/call 的调用流程分析.txt new file mode 100644 index 0000000..42797a9 --- /dev/null +++ b/docs/call 的调用流程分析.txt @@ -0,0 +1,163 @@ +call 的调用流程分析 + +一个lua vm 中 任意时刻 只能运行一个 coroutine +导入重入的原因是,call 操作中 让出执行权,另一个coroutine 修改了某个值,则唤醒call的coroutine时,再来操作那个值,可能已经改变了 + +coroutine 理解: +--------------------------------------------------- +function foo (a) + print("foo", a) + return coroutine.yield(2*a) +end + +co = coroutine.create(function (a,b) + print("co-body", a, b) + local r = foo(a+1) + print("co-body", r) + local r, s = coroutine.yield(a+b, a-b) + print("co-body", r, s) + return b, "end" +end) + +print("main", coroutine.resume(co, 1, 10)) +print("main", coroutine.resume(co, "r")) +print("main", coroutine.resume(co, "x", "y")) +print("main", coroutine.resume(co, "x", "y")) +当你运行它,将产生下列输出: + +co-body 1 10 +foo 2 +main true 4 +co-body r +main true 11 -9 +co-body x y +main true 10 end +main false cannot resume dead coroutine + +coroutine.resume 将会返回 yield的参数 +继续执行coroutine.resume,coroutine.yield将返回coroutine.resume除第一个参数的其他参数 + +coroutine.resume +开始或继续协程 co 的运行。 当你第一次延续一个协程,它会从主体函数处开始运行。 + val1, ... 这些值会以参数形式传入主体函数。 如果该协程被让出,resume 会重新启动它; + val1, ... 这些参数会作为让出点的返回值。 + +如果协程运行起来没有错误, resume 返回 true 加上传给 yield 的所有值 (当协程让出), + 或是主体函数的所有返回值(当协程中止)。 如果有任何错误发生, resume 返回 false 加错误消息。 +--------------------------------------------------- +A call B + +skynet.call(addr, typename, ...) + +[A] +1. 根据 typename 找到 消息的 pack 和 unpack 类型 +2. 将消息发送出去 将消息 pack 压入 对方的消息队列 +3. 将当前coroutine 让出执行权, + +考虑 skynet.call 所在coroutine +在dispatch 消息中 +494: suspend(co, coroutine.resume(co, session,source, p.unpack(msg,sz, ...))) +suspend(co, true, "CALL", session) +suspend(co, result, command, param, size) +result = true, command = "CALL", param = session + +4. 初始化 session_id_coroutine[session] = co + +[B] +1. 通过dispatch_message,处理完相关请求 +2. 若中间出现错误,将错误信息转发至A, 正常情况下通过skynet.ret()返回 + +考虑处理消息所在coroutine +在dispatch 消息中 (记录 co->session, so->source) +494: suspend(co, coroutine.resume(co, session,source, p.unpack(msg,sz, ...))) +suspend(co, true, "RETURN", msg, sz) +suspend(co, result, command, param, size) +result = true, command = "RETURN", param = msg, size = sz +c.send(co_address, skynet.PTYPE_RESPONSE, co_session, param, size) +将消息发送回去 + +考虑在B coroutine中 发生错误 +在 lua-skynet.c _cb函数中,对消息分发函数(lua function)作了pcall处理,若发生错误,将错误内容输出到logger服务中 +在 skynet.lua 中 raw_dispatch_message 函数中 +suspend(co, coroutine.resume(co, session, source, p.unpack(msg,sz, ...))) + +若 coroutine.resume 失败 +将返回 false, 以及错误信息 +在 suspend 函数中 if not result then 判断下 将错误信息发送给A(这里有更多信息) 并掉用 +error(debug.traceback(co,tostring(command))) +将错误堆栈 让 c层 捕获到 输出到 logger服务中 + +[A] +1. 从 session_id_coroutine 中 根据session找到coroutine, 并置空session_id_coroutine +2. 在raw_dispatch_message 中 +-- skynet.PTYPE_RESPONSE = 1, read skynet.h +if prototype == 1 then +处理suspend(co, coroutine.resume(co, true, msg, sz)) +这时候在yield_call中 +local succ, msg, sz = coroutine_yield("CALL", session) +返回coroutine.resume 除第一个参数的其他参数 +local succ, msg, sz = true, msg, sz + +==================================================================== +send 操作就比较简单,A端发送出去就不管了,B端发生错误也不会理睬, +它在 B 端 dispatch_message 阶段 suspend 中 走的 command == nil + + +重要的数据结构 +在被调用的对象中,记录调用者信息 +session_coroutine_id[co] = session +session_coroutine_address[co] = sourceAddr +因为 coroutine 是需要被复用的,所以coroutine退出时候,会清理这两个结构 + +调用者 +watching_session[targetAddr] = session +在挂起当前协程的时候,记录 地址和session +1. 出错信息 +2. 当前vm退出,可告知被call对方,出错信息 + +==================================================================== + +queue 的实现 +当运行该coroutine的时候,调用skynet.wake,生成session,coroutine_yield("SLEEP", session) + +在 suspend 中,设置 +session_id_coroutine[session] = co +sleep_session[co] = session + +dispatch_wakeup 中 +coroutine.resume(co, false, "BREAK")) +继续运行 +sleep_session[co] = nil +session_id_coroutine[session] = nil + +function () + option { + yield "sleep" + } + + # A call B, 则 A 是 yield "call"; B 是 yield "return" + operation { + option yield "call" + option yield "return" + } + + yield "exit" +end + +==================================================================== + +redirect 实现 +skynet.redirect = function(dest, source, typename, ...) + return c.redirect(dest, source, proto[typename].id, ...) +end + +使用范例: + skynet.redirect(agent, c.client or 0, "client", 0, msg, sz) + +1. dest +2. source +3. typeId +4. session +5,6 skynet.pack(...) + +==================================================================== diff --git a/docs/skynet变量含义.txt b/docs/skynet变量含义.txt new file mode 100644 index 0000000..d0e0a69 --- /dev/null +++ b/docs/skynet变量含义.txt @@ -0,0 +1,32 @@ +==================================================================== +session 从 1 到 0x7FFFFFFF +若收到消息 + +如果 session == 0 + +可能 socket 消息 + +可能 text error 消息 + +如果 type == 1 + +可能 response 消息 来源 : call 回复消息 或者 timeout 通知消息 + +==================================================================== +若发送信息: +c.send(dest, type, session, msg, sz) + +如果 session == 0 则为 send 消息 不需要返回 +如果 session 为 nil 则 需要指定底层分配 如 call 操作 + +type 若 session 为 nil, 则 type | PTYPE_TAG_ALLOCSESSION + +谁来清空内存??? + +在发送端生成,由接受者来清理 + +如果是 LUA_TSTRING 分配内存 + +如果是 LUA_TLIGHTUSERDATA seri 的时候 分配内存,传递指针,dispatch_message的时候释放内存 + +==================================================================== diff --git a/docs/skynet消息队列.txt b/docs/skynet消息队列.txt new file mode 100644 index 0000000..cf58ee1 --- /dev/null +++ b/docs/skynet消息队列.txt @@ -0,0 +1,10 @@ +session, type + +session 是保证 call 操作中 A->B->A 过程中,处理能衔接起来 + +type 操作 是为了 找打相应的 pack,unpack 以及 dispatch 函数 + +skynet 含有两级队列 全局队列和服务私有队列 +全局队列里面包含服务私有队列的头指针 + +服务私有队列 一次只被处理一个 \ No newline at end of file diff --git a/docs/stm分析.txt b/docs/stm分析.txt new file mode 100644 index 0000000..05f6eb3 --- /dev/null +++ b/docs/stm分析.txt @@ -0,0 +1,73 @@ +lua_createtable(L, 0, 3); 创建大小为3的table + +lua_pushcfunction(L, lcopy); 将lcopy压栈 + +lua_setfield(L, -2, "copy"); + +{ + copy = lcopy, +} + +==================================================================== + +lua_createtable(L, 0, 2); 创建大小为2的table + +lua_pushcfunction(L, ldeletewriter), +lua_setfield(L, -2, "__gc"); + +lua_pushcfunction(L, lupdate), +lua_setfield(L, -2, "__call"); + +table = { + __gc = ldeletewriter, + __call = lupdate, +} + +luaL_Reg writer[] = { + { "new", lnewwriter }, + { NULL, NULL }, +}; +luaL_setfuncs(L, writer, 1); +将 writer 压入栈顶,并将table作为上值 + +==================================================================== + +lua_createtable(L, 0, 2); +lua_pushcfunction(L, ldeletereader), +lua_setfield(L, -2, "__gc"); +lua_pushcfunction(L, lread), +lua_setfield(L, -2, "__call"); + +table = { + __gc = ldeletereader, + __call = lread, +} + +luaL_Reg reader[] = { + { "newcopy", lnewreader }, + { NULL, NULL }, +}; +luaL_setfuncs(L, reader, 1); +将 reader 压入栈顶,并将table作为上值 + +==================================================================== + +{ + copy = lcopy, + writer = ..., + reader = ..., +} + +==================================================================== + + luaL_Reg m[] = { + { "pop", lpop }, + { "push", lpush }, + { "size", lsize }, + { NULL, NULL }, + }; + luaL_newmetatable(L, "dangge.deque"); 创建一个meta表并压栈 + lua_pushvalue(L, -1); 复制一个meta表 + lua_setfield(L, -2, "__index"); meta = {__index = meta} + luaL_setfuncs(L, m, 0); meta = {pop = lpop, push = lpush, size = lsize} + diff --git a/docs/stringhelper.txt b/docs/stringhelper.txt new file mode 100644 index 0000000..8a07f12 --- /dev/null +++ b/docs/stringhelper.txt @@ -0,0 +1,84 @@ +stringUtil.lua 中提供的接口 +============ +接口设计参考 redis 方式 + +针对 "K1=V1 K2=V2 ... Kn=Vn" 结构的CRUD(增删改查)操作 +K1~Kn是唯一的;K1~Kn,V1~Vn都为整数,不可为浮点数 + +setv 操作为修改 k 的值 v,若不存在 k 则将k=v附在字符串末尾 +string.setv(str, k, v, delimiter) + +msetv 操作为修改 {[k1]=v1, [k2]=v2, ..., [kn]=vn} 中对应的kv值,若字符串中不包含则以 k=v 的形式附在末尾 +string.msetv(str, vs, delimiter) + +incrv 操作为修改 k 对应的值 +delta,若字符串中不包含则以 k=delta 的形式附在末尾 +string.incrv(str, k, delta, delimiter) + +mincrv 操作为修改 {[k1]=d1, [k2]=d2, ..., [kn]=dn} 中对应的k 是与之对应的 v+d,若字符串中不包含则以 k=d 的形式附在末尾 +string.mincrv(str, ds, delimiter) + +delk 操作为 删除字符串中 包含k 的k=v结构, 若不存在则跳过 +string.delk(str, k, delimiter) + +mdelk 操作为 删除字符串中 包含 {k1, k2, ..., kn} 中任意元素的结构,若不存在则跳过 +string.mdelk(str, ks, delimiter) + +getv 操作为 获取字符串中 包含 k 中的 v 值,若不存在返回default值 +string.getv(str, k, default, delimiter) + +如果要获取多个k的值,则可通过 string.toNumMap() 将字符串转化为 一个table,通过访问table获取相应的值 +ps:尽量将多个操作集中为一个table 然后调用 string.msetv or string.mdelk + +============ +string.setv_dk(str, k1, k2, v, delimiter) +string.msetv_dk(str, vs, delimiter) +string.incrv_dk(str, k1, k2, delta, delimiter) +string.mincrv_dk(str, ds, delimiter) +string.delk_dk(str, k1, k2, delimiter) +string.mdelk_dk(str, ks, delimiter) +string.getv_dk(str, k1, k2, default, delimiter) +string.toNumMap(str, delimiter) + +这里是多键结构,操作含义如上 + +============ + +"N1#N2#...#Nn" 是一个集合 +toArray #为任意分隔符 生成 {N1, N2, ..., Nn} +string.toArray(str, toNum, delimiter) +value 获取某个序号的元素 序号范围 1~n +string.value(str, index, default, notNum, delimiter) +sismember 是否存在 value 元素 +string.sismember(str, value, delimiter) +sadd 添加 值为value 元素,若存在则跳过 +string.sadd(str, value, delimiter) +srem 删除 值为value 元素 +string.srem(str, value, delimiter) + +============ + +直接从字符串中 随机 +string.randWeight(str, bMulti) +string.randLine(str) + +============ + +"1010101" 由1,0组成的元素 +getbit 获取序号pos 的元素 +string.getbit(str, pos) +bitcnt 获取 元素值为1 的个数 +string.bitcnt(str) +设置位置为pos的值为yes +string.setbit(str, pos, yes) + +============ + +lua-strh.c 中为lua层提供的接口,这些都是辅助接口,不要直接在逻辑代码中直接使用,使用stringUtil.lua中封装后的接口 +modify +moddk +getv +getvdk +toarray +tonummap +value + diff --git a/docs/任务设计详解.txt b/docs/任务设计详解.txt new file mode 100644 index 0000000..f2c40e2 --- /dev/null +++ b/docs/任务设计详解.txt @@ -0,0 +1,12 @@ +成就系统设计: + +Role:missionInit +读取配表,将missionCsv按类型生成self.missions +[type] = {id1, id2, ...} 便于 后面程序索引 + +Role:checkMissionTask(type, value, count) +成就暂时有三种类型 + +1. 匹配型 +2. 累积型 +3. 收集型 diff --git a/docs/启动服务流程分析.txt b/docs/启动服务流程分析.txt new file mode 100644 index 0000000..68881cc --- /dev/null +++ b/docs/启动服务流程分析.txt @@ -0,0 +1,114 @@ +function skynet.launch(...) + c.command("LAUNCH", table.concat({...}, " ")) + ...... +end + +lua-skynet.c | _command | skynet_command +skynet_server.c | cmd_launch | skynet_context_new + +==================================================================== + +function skynet.newservice(name, ...) + return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...) +end + +service/launcher.lua + +返回 "RESPONSE", skynet.pack + +newservice 收到 "RESPONSE" + +launch 服务就有所有服务的地址 + +可以统计所有服务,gc某个服务,stats某个服务 + +==================================================================== + +再来看 skynet_context_new + +1. 获取 skynet_module +struct skynet_module { + const char * name; 模块名 + void * module; 模块对应的动态库(snlua, 或者其他动态库) + skynet_dl_create create; 创建函数 + skynet_dl_init init; 初始化函数 + skynet_dl_release release; 释放函数 + skynet_dl_signal signal; 信号函数 +}; + +2. 调用 M->create() 函数 +创建 lua vm +struct snlua { + lua_State * L; + struct skynet_context * ctx; +}; +构建 snlua 结构 + +3. 创建并初始化 上下文 +struct skynet_context { + void * instance; snlua结构 + struct skynet_module * mod; + void * cb_ud; + skynet_cb cb; callback函数指针 + struct message_queue *queue; 消息队列 + FILE * logfile; 日志输出文件 + char result[32]; + uint32_t handle; + int session_id; + int ref; + bool init; + bool endless; + CHECKCALLING_DECL +}; +创建消息队列 + +4. 调用 M->init()函数 +设置回调函数 +skynet_callback(ctx, l , _launch); +ctx->cb 回调函数 _launch +ctx->cb_ud snlua结构 + +"REG" 注册 +发送第一个消息 +skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY, 0, tmp, sz); + +调用 _launch 函数 +skynet_callback(context, NULL, NULL); +将回调函数清空 +接着调用 _init 函数 + +将 skynet_context 的指针地址 存入 lua 环境中 (通过轻量用户数据来实现) +加载常用库 +将 LUA_PATH LUA_CPATH LUA_SERVICE LUA_PRELOAD 写入全局table中 +通过 loader.lua 加载所有的lua库 + +5. 将次级消息队列压入全局消息队列 +skynet_globalmq_push + +6. 在服务主模块中调用 +skynet.start 设置回调函数 +c.callback 将 _cb作为键,值lua 回调函数 写入lua环境中 +skynet_callback + +lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD); +lua_State *gL = lua_tothread(L,-1); + +cb_ud 保存 主线程,主线程不会被回收,生命周期同vm + +主线程 栈上永远保存着两个值 +traceback +回调函数 + +调用 回调函数 +r = lua_pcall(L, 5, 0, 1); +栈上第一个函数作为 错误处理函数 + +主线程 调用回调函数 回调函数分配线程去处理 每条消息 + +7. 错误处理 +在 发生错误的 服务 处理 + +回调函数发生在主线程 + + + diff --git a/docs/自动增长类变量实现.txt b/docs/自动增长类变量实现.txt new file mode 100644 index 0000000..9973b38 --- /dev/null +++ b/docs/自动增长类变量实现.txt @@ -0,0 +1,53 @@ +Role.magics = { + talentPoint = { + timestr = "talentLastTime", -- timestamps + limit = function (role) + return globalCsv.talentPointLimit + end, + recover = "talentRecoveryTime", -- globaldefine + collectIn = "in_talentPoint", + collectOut = "out_talentPoint", + cycle = true, -- 回复满,是否复用时间 + },-- 天赋点 +} + +方法: +1. produce 生产 + +2. consume 消费 + +3. onRecoverTimer 定时更新 + +4. onRecoverLogin 登录回复 + +使用前准备: + +在 timestamp 类中添加 talentLastTime ,上次修改时间 + +在 globaldefine 中添加 talentPointLimit ,添加上限值,如上,limit中是function 可以根据role的属性来调整上限值;例如可根据vip调整上限值 + +在 globaldefine 中添加 talentRecoveryTime ,每(多少)个单位时间回复该值,这里默认以分钟为单位 + +collectIn 和 out_talentPoint 变量 是日志字段 + +cycle 是标志位 意思是 满了后消耗,会接着上次剩余时间回复该值 + +==================================================================== + +对外提供接口 + +role:produce(params) +role:consume(params) + +params 可使用字段 + +field + +delta + +cantOver + +notify + + + diff --git a/kill.sh b/kill.sh new file mode 100755 index 0000000..335a612 --- /dev/null +++ b/kill.sh @@ -0,0 +1,4 @@ +#! /bin/sh + +kill -9 `cat skynet.pid` 2>/dev/null +echo "服务端已经关闭" \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..aedcbb4 --- /dev/null +++ b/run.sh @@ -0,0 +1,12 @@ +#! /bin/sh + +pid=`cat skynet.pid` +run=`ps aux | grep skynet | grep -v grep | grep -c $pid` + +if [ $run = 1 ]; then + echo "服务端正在运行" + exit 0 +fi + +skynet/skynet src/config +echo "服务端启动完毕" \ No newline at end of file diff --git a/src/GlobalVar.lua b/src/GlobalVar.lua new file mode 100644 index 0000000..eae6725 --- /dev/null +++ b/src/GlobalVar.lua @@ -0,0 +1,211 @@ +XXTEA_KEY = "699D448D6D24f7F941E9F6E99F823E18" + +MAX_ROLE_NUM = 1000000 + +RESET_TIME = 4 + +MAX_QUALITY_LEVEL = 5 + +MAX_HERO_STAR = 6 +MAX_HERO_LVL = 100 +MAX_EVOL_LVL = 5 -- 最高进化等级 +MAX_BREAK_LVL = 16 + +MAX_VIP = 15 + +MAIL_EXPIRE_TIME = 7*86400 + +MAX_FRIEND_SEARCH_COUNT = 5-- 最大搜索数目 + +MAX_YZ_LEVEL = 10 + +HOUR_ZONE = 9 -- 时区 + +carbonType = { + Normal = 10000, + Special = 20000, + Tower = 30000, + Trial = 40000, + Worldboss = 50000, + Yz = 60000, + Practice = 70000, +} + +FinishType = { + AllKill = 1, --全部清光 + BossKill = 2, --干光boss +} + +StageType = { + Common = 1, -- 普通 + Arrange = 2, -- 传送门 + WeakArrange = 3, -- 脆弱的传送门 + Supply = 4, -- 补给点 + Power = 5, -- 采集点 + Select = 6, -- 抉择点 + Cross = 7, -- 穿越点 + RandSelect = 8, -- 随机selet + OnceArrange = 9, -- 一次性召唤点 + +} + +AiType = { + Warn = 1, -- 警戒型 + Occupy = 2, -- 占领型 + Special = 3, -- 特殊型 + Eat = 4, -- 吞噬特殊型 +} + +MonsterType = { + Boss = 1, -- Boss + Assault = 2, -- 突击怪 + Maker = 3, -- 制造者 + Dregs = 4, -- 渣滓 + Eat = 5, -- 吞噬者 + Ghost = 6, -- 隐藏boss +} + +ItemType = { + RandGiftClose = 1, -- 随机道具 不打开 + RandGiftOpen = 2, -- 随机道具 打开 + Material1 = 3, -- 油 + Material2 = 4, -- 魔力 + Material3 = 5, -- 食材 + Material4 = 6, -- 调料 + HeroSkin = 7, -- 食灵皮肤券 + PackageStudy = 8, -- 套餐研究素材 + Hero = 9, -- 武将 + QuickProp = 10, -- 快速道具 + BuildDraw = 11, -- 建造图纸 + Diamond = 12, -- 钻石 + Medel = 13, -- 勋章 + EnergyItem = 14, -- 次元能量,固定消耗道具 + Seed = 15, -- 种子 + QuickPlant = 16, -- 快速种植 + JobPerfer = 17, -- 职业偏向 + LoveItem = 18, -- 好感度材料 + HeadFarme = 19, -- 头像框 + HeadIcon = 20, -- 头像 + HomeSkin = 21, -- 主城皮肤 + Equip = 22, -- 装备 + Proof = 23, -- 交易凭证 + Dress = 24, -- 时装 + Build = 25, -- 建筑材料 + HeroBag = 26, -- 食灵背包扩展凭证 + EquipBag = 27, -- 武器背包扩展凭证 + GiftBag = 28, -- 礼物背包扩展凭证 + TeamBag = 29, -- 套餐扩展凭证 + HeroBuild = 30, -- 烹饪扩展凭证 + EquipBuild = 31, -- 采购扩展凭证 + PvpCoin = 32, --pvp积分, 商城购买 + ChooseBox = 33, --玩家选择盒子, + DrawCoin = 34, --抽奖券, + ActivityCoin = 35, --活动积分 + DinerBox = 36, --便当盒 + Fitment = 37, --家具 + DinerCar = 38, --餐车 + Accessory = 39, --配件 + DinerCarNum = 40, --餐车数量扩展 + DinerBoxNum = 41, --便当盒数量扩展 + AccMaterial = 42, --升级消耗材料 + DinerCoin = 43, --金币银币 + PangCi = 44, --胖次 + SkillUpM = 45, --技能升级材料 +} + +ROUND = {[5]=true,[6]=true,[9]=true,[13]=true,[14]=true,} + +-- 属性枚举 +AttsEnum = { + hp = 1, -- 血量 + atk = 2, -- 攻击 + phyDef = 3, -- 物理防御 + hit = 4, -- 命中 + miss = 5, -- 闪避 + crit = 6, -- 暴击 + atkSpeed = 7, -- 攻击速度 + critHurt = 8, -- 暴伤 +} + +AttsEnumEx = { + [1] = "hp", -- 血量 + [2] = "atk", -- 攻击 + [3] = "phyDef", -- 物理防御 + [4] = "hit", -- 命中 + [5] = "miss", -- 闪避 + [6] = "crit", -- 暴击 + [7] = "atkSpeed", -- 攻击速度 + [8] = "critHurt", -- 暴伤 +} + +-- 物品起始id +ItemStartId = { + hero = 1000, -- 英雄 + equip = 2000, -- 装备 +} + +HeroType = { + legend = 1, -- 传说英雄 + diamond = 2, -- 钻石英雄 + normal = 3, -- 免费英雄 + lowStar = 4, -- 低星英雄 + monster = 5, -- 怪物 + npc = 6, -- npc + beauty = 7, -- 女神 +} + +ExploreType = { + Common = 1, -- 普通 + Born = 2, -- 出生点 + Score = 3, -- 积分点 + Cure = 4, -- 治疗点 + Item = 5, -- 道具店 + Cross = 6, -- 传送门 + Enemy = 7, -- 战斗点 + Event = 8, -- 抉择点 +} + +ExploreItemType = { + Bomb = 1, -- 炸弹 + Weak = 2, -- 虚弱 + Slow = 3, -- 迟缓 + Invincible = 4, -- 无敌 + Forbiden = 5, -- 封印 + Victory = 6, -- 必胜 + Dice1 = 7, -- 固定点数1 + Dice2 = 8, -- 固定点数2 + Dice3 = 9, -- 固定点数3 + Dice4 = 10, -- 固定点数4 + Dice5 = 11, -- 固定点数5 + Dice6 = 12, -- 固定点数6 + DiceAll = 13, -- 任意点数 + Fight = 14, -- 决斗卡 +} + +RoomEvent = { + move = 1, + useItem = 2, + playerOnStage = 3, + playerStart = 4, + playerEnd = 5, + playerExit = 6, + playerDice = 7, + playerFight = 8, + eventCross = 9, + eventFight = 10, + eventScore = 11, + eventItem = 12, + eventCure = 13, + addScore = 14, + updateMap = 15, + addMainList = 16, + addWaitList = 17, + playerRevive= 18, + playerTalk = 19, + addHealth = 20, +} + +BANTYPE = { + default = 0, + heartWarning = 1, +} \ No newline at end of file diff --git a/src/ProtocolCode.lua b/src/ProtocolCode.lua new file mode 100644 index 0000000..d151d11 --- /dev/null +++ b/src/ProtocolCode.lua @@ -0,0 +1,260 @@ +-- 协议号 +actionCodes = { + Sys_heartBeat = 1, + Sys_errorMsg = 3, + Sys_innerErrorMsg = 4, + Sys_commonNotice = 5, + Sys_maintainNotice = 6, + Sys_kickdown = 7, + Sys_runningHorse = 8, + + Gm_clientRequest = 20, + Gm_receiveResponse = 21, + + Role_notifyNewEvent = 100, + Role_queryLoginRpc = 101, + Role_createRpc = 102, + Role_loginRpc = 103, + Role_updateProperty = 105, + Role_updateProperties = 106, + Role_changeFormationRpc = 107, + Role_setCrownRpc = 108, + Role_formationPosRpc = 109, + Role_entrustRpc = 110, + Role_finishEntrustRpc = 111, + Role_updateStoryBook = 112, + Role_finishTalkRpc = 113, + Role_updateHeroBook = 114, + Role_guideRpc = 115, + Role_changeAutoSateRpc = 116, + Role_signRpc = 117, + Role_dailyTaskRpc = 118, + Role_mainTaskRpc = 119, + Role_missionRpc = 120, + Role_formationQuickRpc = 121, + Role_changeNameRpc = 122, + Role_changeHomeBgRpc = 123, + Role_changeHeadIconRpc = 124, + Role_changeHeadFrameRpc = 125, + Role_cookNotesRpc = 126, + Role_cookAddFavoritesRpc = 127, + Role_cookDelFavoritesRpc = 128, + Role_drawCodeRpc = 129, + Role_chat = 130, + Role_signGiftRpc = 131, + Role_changeToRealUserRpc = 132, + Role_cafeSignRpc = 133, + Role_syncTimeRpc = 134, + Role_getLevelRankRpc = 135, + Role_deletAccountRpc = 136, + Role_getItemRankRpc = 137, + + Hero_loadInfos = 201, + Hero_updateProperty = 202, + Hero_decomposeRpc = 203, + Hero_qualityRpc = 204, + Hero_strengthRpc = 205, + Hero_lockRpc = 206, + Hero_researchRpc = 207, + Hero_finishResearchRpc = 208, + Hero_treatRpc = 209, + Hero_finishTreatRpc = 210, + Hero_loveItemRpc = 211, + Hero_finishLoveTaskRpc = 212, + Hero_changeDressRpc = 213, + Hero_quickTreatRpc = 214, + Hero_skillUpRpc = 215, + Hero_changeNameRpc = 216, + Hero_likeHeroRpc = 217, + Hero_commentHeroRpc = 218, + Hero_getCommentsRpc = 219, + Hero_likeCommentRpc = 220, + + + + Item_updateProperty = 301, + + Store_produceRpc = 401, + Store_finishBuildRpc = 402, + Store_diamondBuyRpc = 403, + Store_itemBuyRpc = 404, + Store_rechargeRpc = 405, + Store_ayncPurchaseRpc = 406, + Store_purchaseOrderResult = 407, + Store_iapPurchaseRpc = 408, + Store_iapCancelPurchase = 409, + Store_restorePurchaseRpc = 410, + Store_googlePurchaseRpc = 411, + Store_googleCancelPurchase = 412, + Store_samsungPurchaseRpc = 413, + Store_samsungCancelPurchase = 414, + + Trade_getInfoRpc = 450, + Trade_sellRpc = 451, + Trade_buyRpc = 452, + Trade_giveUpRpc = 453, + Trade_cleanBuyRpc = 454, + + Carbon_arrangeCarbonRpc = 500, + Carbon_moveRpc = 501, + Carbon_actionEndRpc = 502, + Carbon_beginGameRpc = 503, + Carbon_endGameRpc = 504, + Carbon_cancelMoveRpc = 505, + Carbon_updateProperty = 506, + Carbon_endCarbonRpc = 507, + Carbon_exitTeamRpc = 508, + Carbon_supplyRpc = 509, + Map_updateProperty = 510, + Carbon_changePosRpc = 511, + Carbon_generateBoss = 512, + Carbon_givpUpBoss = 513, + Carbon_drawBossAward = 514, + Carbon_SelectRpc = 515, + + Friend_updateProperty = 550, + Friend_listRpc = 551, + Friend_searchRpc = 552, + Friend_randomRpc = 553, + Friend_deleteRpc = 554, + Friend_applyRpc = 555, + Friend_applyListRpc = 556, + Friend_handleApplyRpc = 557, + Friend_report = 558, + + Farm_updateProperty = 600, + Farm_drawMaterial = 601, + Farm_levelUpBuilding = 602, + Farm_changeShowMedal = 603, + Farm_plantRpc = 604, + Farm_useItemRpc = 605, + Farm_getPlantRpc = 606, + Farm_farmInfoRpc = 607, + Farm_changeFarmerRpc = 608, + + Tower_updateProperty = 630, + Tower_battleBeginRpc = 631, + Tower_battleEndRpc = 632, + Tower_resetRpc = 633, + Tower_cureRpc = 634, + Tower_changeFormatRpc = 635, + Tower_getRankRpc = 636, + Tower_formatQuickRpc = 637, + Tower_formationPosRpc = 638, + Tower_breakRpc = 639, + + Email_listRpc = 650, + Email_drawAttachRpc = 651, + Email_checkRpc = 652, + Email_delRpc = 653, + Email_drawAllAttachRpc = 654, + + Pvp_updateProperty = 670, + Pvp_battleBeginRpc = 671, + Pvp_battleEndRpc = 672, + Pvp_resetRpc = 673, + Pvp_enterPvpRpc = 674, + Pvp_getRankRpc = 675, + Pvp_changeFormatRpc = 676, + Pvp_formatQuickRpc = 677, + Pvp_formationPosRpc = 678, + Pvp_skillOrderRpc = 679, + Pvp_shopBuyRpc = 680, + + Equip_updateProperty = 750, + Equip_wearEquipRpc = 751, + Equip_repairEquipRpc = 752, + Equip_forgeEquipAttrsRpc = 753, + Equip_replaceEquipAttrRpc = 754, + Equip_buildEquipRpc = 755, + Equip_finishBuildEquipRpc = 756, + Equip_decomposEquipRpc = 757, + Equip_resetEquipRpc = 758, + Equip_upEquipRpc = 759, + + Activity_getRewardRpc = 801, + Activity_lotteryGiftRpc = 802, + Activity_shopBuyRpc = 803, + Activity_puzzleRewardRpc = 804, + Activity_inheritRewardRpc = 805, + Activity_midAutRewardRpc = 806, + Activity_clearRed = 807, + Activity_halloweenMoveRpc = 808, + Activity_halloweenResetRpc = 809, + Activity_halloweenRewardRpc = 810, + Activity_traditionalRewardRpc = 811, + Activity_africanRpc = 812, + Activity_oldBackRpc = 813, + Activity_orderRewardRpc = 814, + Activity_cookHeroRpc = 815, + + Moon_arrangeCarbonRpc = 850, + Moon_moveRpc = 851, + Moon_actionEndRpc = 852, + Moon_beginGameRpc = 853, + Moon_endGameRpc = 854, + Moon_cancelMoveRpc = 855, + Moon_updateProperty = 856, + Moon_endCarbonRpc = 857, + Moon_exitTeamRpc = 858, + Moon_supplyRpc = 859, + Moon_changePosRpc = 860, + Moon_generateBoss = 861, + Moon_givpUpBoss = 862, + Moon_drawBossAward = 863, + Moon_SelectRpc = 864, + + Paradise_arrangeCarbonRpc = 875, + Paradise_moveRpc = 876, + Paradise_actionEndRpc = 877, + Paradise_beginGameRpc = 878, + Paradise_endGameRpc = 879, + Paradise_cancelMoveRpc = 880, + Paradise_updateProperty = 881, + Paradise_endCarbonRpc = 882, + Paradise_exitTeamRpc = 883, + Paradise_supplyRpc = 884, + Paradise_changePosRpc = 885, + Paradise_SelectRpc = 886, + Paradise_buyCountRpc = 887, + + Diner_loadRpc = 900, + Diner_updateProperty = 901, + Diner_updateProperties = 902, + Diner_accessoryUpdateProperty = 903, + Diner_itemUpdateProperty = 904, + Diner_wearAccessoryRpc = 905, + Diner_upLevelAccessoryRpc = 906, + Diner_supplyMaterialRpc = 907, + Diner_changeCarRpc = 908, + Diner_recycleAccessoryRpc = 909, + Diner_heroToBoxRpc = 910, + Diner_saveBoxFitmentRpc = 911, + Diner_recycleFitmentsRpc = 912, + Diner_sellRpc = 913, + Diner_finishSellRpc = 914, + Diner_shopBuyRpc = 915, + Diner_drawRewardRpc = 916, + Diner_talentUpdateProperty = 917, + + Diner_likeOtherBoxRpc = 918, + Diner_comfortRankRpc = 919, + Diner_nearLikeMeRpc = 920, + Diner_friendListRpc = 921, + Diner_getBoxDataRpc = 922, + Diner_changeTalkRpc = 923, + Diner_changeTalkBgRpc = 924, +} + +rpcResponseBegin = 10000 + +actionHandlers = {} +for key, value in pairs(actionCodes) do + local suffix = string.sub(key, -3, -1) + local handlerName = string.gsub(key, "_", ".") + + if suffix == "Rpc" then + actionHandlers[value + rpcResponseBegin] = handlerName .. "Response" + end + actionHandlers[value] = string.gsub(key, "_", ".") +end \ No newline at end of file diff --git a/src/RedisKeys.lua b/src/RedisKeys.lua new file mode 100644 index 0000000..a3eeefa --- /dev/null +++ b/src/RedisKeys.lua @@ -0,0 +1,41 @@ +-- role +R_FARM_KEY = "role:%d:farm" +R_TOWER_KEY = "role:%d:tower" +R_COOKLOG_KEY = "role:%d:cooklog" +R_TRADELOG_KEY = "role:%d:tradelog" +R_PVP_KEY = "role:%d:pvp" +R_DINER_KEY = "role:%d:diner" +-- rank +RANK_PVP = "rank:pvp" +RANK_TRADE = "rank:trade" +RANK_TOWER = "rank:tower" +RANK_BOX = "rank:box" -- 盒子舒适度排行榜 +MAP_LIKE = "map:box:like" --点赞个数 +RANK_LEVEL = "rank:level" --等级排名 +RANK_ITEM = "rank:item" --活动物品排行 +-- 日志 +NOTE_COOK_KEY = "note:cook:%d" + +TRADE_KEY = "trade:%d" +TRADE_ID_KEY = "tradeIDs" + +TASK_ACTIVE = "task:%d:active" -- 记录激活的任务 +TASK_FINISH = "task:%d:finish" -- 记录完成的任务 + +BOSS_SET = "boss:%d:%d" +BOSS_INFO = "boss:battle" + +FRIEND_KEY = "role:%d:friend" --哈希表 +FRIEND_APPLY_KEY = "role:%d:apply" -- set +FRIEND_DINER_LIKE_KEY = "role:%d:diner:like" -- list + +UNION_SET = "global:union" +UNION_KEY = "union:%d" +UNION_ROLE = "union:%d:%d" +UNION_ROLE_SET = "union:%d:roleList" +UNION_APPLY_SET = "union:%d:applyList" + +PVP_KING = "pvp:king" --王者传奇组 +PVP_HONOR = "pvp:honor" --荣耀咸鱼组 +PVP_NOOB = "pvp:noob" --菜鸟组 +PVP_INFO = "pvp:info" --玩家信息组 \ No newline at end of file diff --git a/src/actions/GmAction.lua b/src/actions/GmAction.lua new file mode 100644 index 0000000..168d11c --- /dev/null +++ b/src/actions/GmAction.lua @@ -0,0 +1,18 @@ +local _M = {} +local redisproxy = redisproxy +function _M.clientRequest(agent, data) + local msg = MsgPack.unpack(data) + local role = agent.role + local action = _M[msg.cmd] + local bin = MsgPack.pack({ cmd = "指令失败" }) + if not action then + SendPacket(actionCodes.Gm_receiveResponse, bin) + return true + end + local ret = action(role, msg) + bin = MsgPack.pack({ cmd = ret }) + SendPacket(actionCodes.Gm_receiveResponse, bin) + return true +end + +return _M \ No newline at end of file diff --git a/src/actions/NgxAction.lua b/src/actions/NgxAction.lua new file mode 100644 index 0000000..9bd11aa --- /dev/null +++ b/src/actions/NgxAction.lua @@ -0,0 +1,65 @@ +local string_format = string.format +local mcast_util = mcast_util +local gmFuncs = require "actions.GmAction" + +local function proc_online(cmd, roleId, pms) + local agent = datacenter.get("agent", roleId) + if agent then + local ok, result = pcall(skynet.call, agent.serv, "lua", cmd, pms) + return ok and result or "指令在线失败" + end + return "not_online" +end + +local _M = {} +local __const = {["id"]=1,["pm1"]=1,["pm2"]=1,["pm3"]=1} + +local _T = setmetatable({}, {__index = function(_, k) + return function (pms) + for k, v in pairs(pms) do + -- tonum(v, v) 能tonumber 则 转换,不能则返回原来的值 + pms[k] = __const[k] and tonum(v, v) or v + end + -- 群体操作 + if not pms.id or pms.id == 0 then + return _M[k] and _M[k](pms) or "指令不存在" + end + -- 个体操作 + if not gmFuncs[k] then return "指令不存在" end + -- 在线操作 + local isOn = proc_online(k, pms.id, pms) + if isOn ~= "not_online" then + return isOn + end + -- 如果_M有直接操作,跳过load角色 + if _M[k] then return _M[k](pms) end + -- 离线操作 + local role = require("models.Role").new({key = string_format("role:%d", pms.id)}) + local ret = role:load() + if not ret then + return "角色不存在" + end + role:loadAll() + return gmFuncs[k](role, pms) + end +end}) + +-- 在线广播 +function _M.broadcast(pms) + local bin = MsgPack.pack({body = pms.pm2}) + local codes = { + ["common"] = actionCodes.Sys_commonNotice, + ["maintain"] = actionCodes.Sys_maintainNotice, + } + if not codes[pms.pm1] then return "错误" end + + mcast_util.pub_world(codes[pms.pm1], bin) + return "广播成功" +end + +function _M.online(pms) + local count = datacenter.get("onlineCount") or 0 + return count +end + +return _T \ No newline at end of file diff --git a/src/actions/RoleAction.lua b/src/actions/RoleAction.lua new file mode 100644 index 0000000..7f7dcbd --- /dev/null +++ b/src/actions/RoleAction.lua @@ -0,0 +1,319 @@ +local ipairs = ipairs +local table = table +local math = math +local next = next +local string = string +local redisproxy = redisproxy +local MsgPack = MsgPack +local getRandomName = getRandomName +local mcast_util = mcast_util +local string_format = string.format +local tonumber = tonumber +local require = require +local table_insert = table.insert +local tconcat = table.concat +local httpc = require("http.httpc") + +local WAVE_HERO_NUMS = 150 +local WAVE_EQUIP_NUMS = 150 + +local function validName(name) + name = string.upper(name) + local exist = redisproxy:exists(string_format("user:%s", name)) + if exist then return "existed" end + + local SERV = string_format("NAMED%d", math.random(1, 5)) + local legal = skynet.call(SERV, "lua", "check", name) + return legal and "ok" or "illegal" +end + +-- 随机玩家名 +local function randomRoleName() + -- 过滤已经存在的名字 + local name + repeat + name = getRandomName() + until validName(name) == "ok" + return name +end + +local function setRoleName(uid, roleId) + local result + local name + local dbName + repeat + name = randomRoleName() + dbName = string.upper(name) + result = redisproxy:setnx(string_format("user:%s", dbName), roleId) + until result == 1 + redisproxy:set(string_format("uid:%s", uid), dbName) + return name +end + +local _M = {} +function _M.loginRpc( agent, data ) + local msg = MsgPack.unpack(data) + local response = {} + + if msg.version ~= globalCsv.version then + response.result = "UPDATE_TIP" + SendPacket(actionCodes.Role_loginRpc, MsgPack.pack(response)) + return true + end + + -- 1. + local roleId = redisproxy:get(string_format("user:%s", string.upper(msg.name))) + if not roleId then + response.result = "NOT_EXIST" + SendPacket(actionCodes.Role_loginRpc, MsgPack.pack(response)) + return true + end + + roleId = tonumber(roleId) + + --维护不能登录 + local maintain = tonumber(redisproxy:hget("autoincrement_set", "maintain")) + if maintain and maintain > 0 then + if tonumber(redisproxy:hget(string_format("role:%d", roleId), "ignoreMaintain")) ~= 1 then + response.result = "MAINTAIN_TIP" + SendPacket(actionCodes.Role_loginRpc, MsgPack.pack(response)) + return true + end + end + + local now = skynet.timex() + local role = agent.role + -- 2 + if not role then + local roleKey = string_format("role:%d", roleId) + if not redisproxy:exists(roleKey) then + response.result = "DB_ERROR" + SendPacket(actionCodes.Role_loginRpc, MsgPack.pack(response)) + return true + end + -- 2a + role = require("models.Role").new({key = roleKey}) + role:load() + role:loadAll() + else + role:reloadWhenLogin() + end + + if not msg.isGMlogin then + local banTime = role:getProperty("banTime") + if banTime > now then + response.result = "BAN_TIP" + response.banTime = banTime + response.banType = role:getProperty("banType") + response.roleId = roleId + SendPacket(actionCodes.Role_loginRpc, MsgPack.pack(response)) + return true + end + if banTime ~= 0 then + -- 清除封号状态 + role:setBan(0) + end + end + SERV_OPEN = redisproxy:hget("autoincrement_set", "server_start") + local lastLoginTime = role:getProperty("lastLoginTime") + + -- 跨天登陆事件 + if not role:onCrossDay(now) then + role:checkActivityStatus(lastLoginTime, now) + if role:getProperty("carbonDouble") == "" then + role:initCarbonDouble() + end + end + -- 登陆回复 + role:onRecoverLogin(now) + -- 引导是否连续 + role:checkGuide() + + + role:setProperty("lastLoginTime", now) + if msg.device and type(msg.device) == "string" then + role:setProperty("device", msg.device) + end + + response.role = role:data() + response.result = "SUCCESS" + response.serverTime = now + + -- 需要加载模块数据 + local modules = { + "carbons","maps", + } + + local heroIds = {} + for heroId, _ in pairs(role.heros) do + table.insert(heroIds, heroId) + end + local heroWave = math.ceil(#heroIds / WAVE_HERO_NUMS) + + local equipIds = {} + for equipId, _ in pairs(role.equips) do + table.insert(equipIds, equipId) + end + local equipWave = math.ceil(#equipIds / WAVE_EQUIP_NUMS) + + if #heroIds <= 50 then + heroWave = 0 + table_insert(modules, "heros") + end + if #equipIds <= 50 then + equipWave = 0 + table_insert(modules, "equips") + end + + for _, name in ipairs(modules) do + response[name] = {} + for id, unit in pairs(role[name]) do + response[name][id] = unit:data() + end + end + response.wave = 1 + heroWave + equipWave + + SendPacket(actionCodes.Role_loginRpc, MsgPack.pack(response)) + + local heroIndex = 1 + for index = 2, 1 + heroWave do + local heroResponse = {heros = {}} + for i = heroIndex, heroIndex + WAVE_HERO_NUMS do + local heroId = heroIds[i] + if not heroId then + break + end + local hero = role.heros[heroId] + table_insert(heroResponse.heros, hero:data()) + heroIndex = heroIndex + 1 + end + heroResponse.heroWave = index + SendPacket(actionCodes.Role_loginRpc, MsgPack.pack(heroResponse)) + end + local equipIndex = 1 + for index = 2 + heroWave, 1 + heroWave + equipWave do + local equipResponse = {equips = {}} + for i = equipIndex, equipIndex + WAVE_EQUIP_NUMS do + local equipId = equipIds[i] + if not equipId then + break + end + local equip = role.equips[equipId] + table_insert(equipResponse.equips, equip:data()) + equipIndex = equipIndex + 1 + end + equipResponse.equipWave = index + SendPacket(actionCodes.Role_loginRpc, MsgPack.pack(equipResponse)) + end + + -- role:log("login", { ip = agent.ip, diamond = role:getProperty("diamond"), reDiamond = role:getProperty("reDiamond")}) + + datacenter.set("agent", roleId, { + serv = skynet.self(), + fd = agent.client_fd, + gate_serv = agent.gate_serv, + }) + agent.role = role + role.lessmsg = false + + start_agent_timer() + -- 注册全服广播 + local channel = math.randomInt(1, 1) + local w_channel = datacenter.get( ("MC_W_CHANNEL" .. channel) ) + if w_channel then + mcast_util.sub_world(w_channel) + end + return true +end + +function _M.createRpc(agent, data) + local msg = MsgPack.unpack(data) + local response = {} + + -- 再次检查uid + local uid = tostring(msg.uid) + local user = redisproxy:get(string_format("uid:%s", uid)) + if user then + response.result = "SUCCESS" + response.roleName = user + SendPacket(actionCodes.Role_createRpc, MsgPack.pack(response)) + return true + end + + local roleId = getNextRoleId() + if not roleId then + response.result = "DB_FULL" + SendPacket(actionCodes.Role_createRpc, MsgPack.pack(response)) + return true + end + local roleName = setRoleName(msg.uid, roleId) + + local newRole = require("models.Role").new({ + key = string_format("role:%d", roleId), + id = roleId, + uid = tostring(msg.uid), + subId = msg.subId or 0, + name = roleName, + uname = msg.uname or "", + device = tostring(msg.device) + }) + + if newRole:create() then + --更新USER表 + response.result = "SUCCESS" + response.roleId = roleId + response.roleName = string.upper(roleName) + else + response.result = "DB_ERROR" + SendPacket(actionCodes.Role_createRpc, MsgPack.pack(response)) + return true + end + + -- 给角色自动加载当前副本数据 + newRole:addCarbon({carbonId = 10101, status = 0, starNum = 0}) + -- 给角色自动加载农场信息 + newRole:addFarm() + -- 给角色自动加载爬塔信息 + newRole:addTower() + -- 给角色增加pvpInfo + newRole:addPvpInfo() + --给新角色增加diner + newRole:addDiner() + -- 关闭 锁定新食灵 + local autoStatus = newRole:getProperty("autoStatus") + newRole:setProperty("autoStatus", autoStatus:setv(3,1)) + + for index, hero in ipairs(globalCsv["birthHero"]) do + hero.notNotify = true + hero.desc = "birth_award" + local heroId = newRole:awardHero(hero.type, hero) + if index == 1 then + local newHero = newRole.heros[heroId] + newHero:setProperty("lock", 1) + newHero:setProperty("formation", 1) + newRole:setProperty("crown", heroId) + newRole:setProperty("formationJson", json.encode({["1"] = {list = {["1"] = heroId}, pos = {["1"] = heroId}, lock = {}}})) + end + end + -- 出生道具 + local ucode = getActionCode(newRole) + for id, num in pairs(globalCsv["birthItem"]:toNumMap()) do + newRole:awardItemCsv(id, {count = num, desc = "birth_award", notNotify = true, ucode = ucode}) + end + + -- 欢迎邮件 + -- redisproxy:insertEmail({roleId = roleId, emailId = 1}) + -- redisproxy:insertEmail({roleId = roleId, emailId = 2}) + + newRole:log("create", { ip = agent.ip, ucode = ucode}) + + SendPacket(actionCodes.Role_createRpc, MsgPack.pack(response)) + return true +end + +function _M.syncTimeRpc(agent, data) + SendPacket(actionCodes.Role_syncTimeRpc, MsgPack.pack({nowTime = skynet.timex()})) + return true +end + +return _M \ No newline at end of file diff --git a/src/agent.lua b/src/agent.lua new file mode 100644 index 0000000..160f96b --- /dev/null +++ b/src/agent.lua @@ -0,0 +1,286 @@ +require "ProtocolCode" +require "shared.init" +require "utils.init" +require "GlobalVar" +require "RedisKeys" +require "skynet.manager" + +local harbor = require "skynet.harbor" +local queue = require "skynet.queue" +local netpack = require "skynet.netpack" +local socket = require "skynet.socket" +local sharedata = require "skynet.sharedata" +local xxtea = require "xxtea" + +skynet = require "skynet" +redisproxy = require "shared.redisproxy" +datacenter = require "skynet.datacenter" +mcast_util = require "services/mcast_util" +globalCsv = require "csvdata/GlobalDefine" + +local CMD = {} +local agentInfo = {} -- { client_fd, role, gate_serv, open_timer} + +local agent_util, cs + +--- {{{ 定时器相关 +local function handle_timeout() + if not agentInfo.open_timer then return end + + if not agentInfo.role then + skynet.timeout(100, handle_timeout) + return + end + + agent_util:update(agentInfo) + skynet.timeout(100, handle_timeout) +end + +function start_agent_timer() + agentInfo.open_timer = true + skynet.timeout(150, handle_timeout) +end + +function cancel_agent_timer() + agentInfo.open_timer = false +end +---- 定时器相关 }}} + +function SendPacket(actionCode, bin, client_fd) + if #bin > 0 then bin = xxtea.encrypt(bin, XXTEA_KEY) end + + local handlerName = actionHandlers[actionCode] + if string.sub(handlerName, -3, -1) == "Rpc" then + actionCode = actionCode + rpcResponseBegin + end + + local client_fd = client_fd or agentInfo.client_fd + local head = string.pack("H", actionCode) + return socket.write(client_fd, netpack.pack(head .. bin)) +end + +function rpcAgent(roleId, funcName, ...) + local agent = datacenter.get("agent", roleId) + if agent then + return skynet.call(agent.serv, "lua", funcName, ...) + end +end + +function rpcParter(serv, func, ...) + if serv then + local ok, result = pcall(skynet.call, serv, "role", func, ...) + if ok then + return result + end + end +end + +local string_format = string.format +local table_unpack = table.unpack +function rpcRole(roleId, funcName, ...) + local fields = ... + local agent = datacenter.get("agent", roleId) + if agent and agent.serv then + if funcName == "getProperties" then + return true, skynet.call(agent.serv, "role", funcName, fields) + else + return true, skynet.call(agent.serv, "role", funcName, ...) + end + else + local rediskey = string_format("role:%d", roleId) + if funcName == "setProperty" then + return false, redisproxy:hset(rediskey, ...) + elseif funcName == "getProperty" then + return false, redisproxy:hget(rediskey, ...) + elseif funcName == "getProperties" then + local sRole = require("models.Role") + local returnValue = redisproxy:hmget(rediskey, table_unpack(...)) + local ret = {} + for index, key in ipairs(fields) do + local typ = sRole.schema[key][1] + local def = sRole.schema[key][2] + if typ == "number" then + ret[key] = tonumber(returnValue[index] or def) + else + ret[key] = returnValue[index] + end + end + return false, ret + elseif funcName == "setProperties" then + local result = {} + for k,v in pairs(fields) do + result[#result+1] = k + result[#result+1] = v + end + return false, redisproxy:hmset(rediskey, table_unpack(result)) + end + end +end + +function rpcUnion(funcName, ...) + local serv = agentInfo.userv + if not serv then + local consortiaId = agentInfo.role:getProperty("consortiaId") + if consortiaId == 0 then return true,1000 end + local union = datacenter.get("union", consortiaId) + if not union or not union.serv then + skynet.error("rpcUnion error: union serv addr not exist", funcName, consortiaId) + return + end + serv = union.serv + end + return skynet.call(serv, "lua", funcName, ...) +end + +function rpcOtherUnion(id, funcName, ...) + local union = datacenter.get("union", id) + if union and union.serv then + return skynet.call(union.serv, "lua", funcName, ...) + end +end + +skynet.register_protocol { + name = "client", + id = skynet.PTYPE_CLIENT, + unpack = function (msg, sz) + local data = skynet.tostring(msg, sz) + local cmd = string.unpack("H", string.sub(data, 1, 2)) + return cmd, string.sub(data, 3) + end, + dispatch = function(session, address, cmd, data) + cs(function() + if cmd == actionCodes.Sys_heartBeat then + agent_util:heart_beat(agentInfo) + return + end + local actionName = actionHandlers[cmd] + if not actionName then + print("actionName not exist", actionName) + return + end + local modName, funcName = actionName:match("(%w+)%.(%w+)") + + local ok, action = pcall(require, "actions." .. modName .. "Action") + if not ok then + print("require module name error", action, modName) + return + end + + local method = action[funcName] + + if type(method) ~= "function" then + print("ERROR_SERVER_INVALID_ACTION", modName, funcName) + return + end + + if #data > 0 then data = xxtea.decrypt(data, XXTEA_KEY) end + local result = method(agentInfo, data) + if not result then + SendPacket(actionCodes.Sys_innerErrorMsg, MsgPack.pack({id = cmd})) + end + end) + end +} + +skynet.register_protocol { + name = "role", + id = 12, + pack = skynet.pack, + unpack = skynet.unpack, + dispatch = function(session, address, submethod, ...) + local result + if not agentInfo.role then + result = "__OFFLINE__" + else + result = agentInfo.role[submethod](agentInfo.role, ...) + end + + skynet.ret(skynet.pack(result)) + end, +} + +-- function CMD.start(gate, fd, ip) +function CMD.start(session, source, gate, fd, ip) + ignoreHeartbeat = false + + agentInfo.client_fd = fd + agentInfo.gate_serv = gate + agentInfo.ip = ip + + agent_util:reset() + math.randomInit() + + -- 这里将消息伪装成 watchdog 发出,这样就由 A->B->C->B->A 变成 A->B->C->A + skynet.redirect(gate, source, "lua", session, skynet.pack("forward", fd, 0, skynet.self())) +end + +function CMD.close() + cancel_agent_timer() + mcast_util.usub_world() + mcast_util.usub_union() + + local role = agentInfo.role + if not role then return end + role:log("logout", {online = skynet.timex()-role:getProperty("lastLoginTime")}) + role:onOfflineEvent() +end + +function CMD.exit() + if agentInfo.role then + -- role:log("logout", {online = skynet.timex()-role:getProperty("lastLoginTime")}) + datacenter.set("agent", agentInfo.role:getProperty("id"), nil) + end + skynet.exit() +end + +function CMD.subUnion(consortiaId, union) + mcast_util.sub_union(consortiaId, union.chan) + agentInfo.userv = union.serv +end + +function CMD:usubUnion() + mcast_util.usub_union() + agentInfo.userv = nil +end + +local function routeGM(cmd, params) + if type(params) ~= "table" or not agentInfo.role then + return "指令失败" + end + local _M = require "actions.GmAction" + return _M[cmd](agentInfo.role, params) +end + +skynet.start(function() + skynet.dispatch("lua", function(session, source, command, ...) + local f = CMD[command] + if f then + if command == "exit" then + f(...) + elseif command == "start" then + f(session, source, ...) + else + skynet.ret(skynet.pack(f(...))) + end + else + skynet.ret(skynet.pack(routeGM(command, ...))) + end + end) + + redisd = harbor.queryname("REDIS") + if tonumber(skynet.getenv "logd") == 1 then + logd = harbor.queryname("LOGD") + end + + cs = queue() + + -- csv + csvdb = sharedata.query("csvdata") + -- 错误码特殊处理 + -- todo + -- for key, value in pairs(csvdb["sys_codesCsv"]) do + -- _G[string.upper(value.varname)] = key + -- end + + agent_util = require "services/agent_util" +end) diff --git a/src/config b/src/config new file mode 100644 index 0000000..93a06b2 --- /dev/null +++ b/src/config @@ -0,0 +1,17 @@ +root = "./" +thread = 8 +logger = "server.log" +harbor = 0 +start = "main" -- main script +bootstrap = "snlua bootstrap" -- The service for bootstrap +logd = 0 -- 是否开启日志 +servId = 1 +baseId = 0 +codeurl = "127.0.0.1:8686" + +lua_path = root .."skynet/lualib/?.lua;"..root.."src/?.lua;"..root.."tools/?.lua" +luaservice = root.."skynet/service/?.lua;"..root.."src/?.lua" +lualoader = "skynet/lualib/loader.lua" +preload = "./src/preload.lua" -- run preload.lua before every lua service run +cpath = root.."skynet/cservice/?.so" +lua_cpath = "skynet/luaclib/?.so" \ No newline at end of file diff --git a/src/csvdata b/src/csvdata new file mode 160000 index 0000000..3aabf5c --- /dev/null +++ b/src/csvdata @@ -0,0 +1 @@ +Subproject commit 3aabf5c43c36e6da7f63f237301b4772f0129e88 diff --git a/src/main.lua b/src/main.lua new file mode 100644 index 0000000..163d386 --- /dev/null +++ b/src/main.lua @@ -0,0 +1,29 @@ +local skynet = require "skynet" + +local max_client = 64 + +skynet.start(function() + print("Server start") + skynet.newservice("console") + skynet.newservice("debug_console", 3001) + + local ngxd = skynet.newservice("services/ngxd", 3002) + local watchdog = skynet.newservice("services/watchdog", max_client) + skynet.call(watchdog, "lua", "start", { + port = 3003, + maxclient = max_client, + ngxd = ngxd, + + redishost = "127.0.0.1", + redisport = 6379, + redisdb = 4, + auth = nil, + + mongohost = "127.0.0.1", + mongoport = nil, + mongouser = nil, + mongopswd = nil, + }) + + skynet.exit() +end) diff --git a/src/models/Role.lua b/src/models/Role.lua new file mode 100644 index 0000000..5abfb0e --- /dev/null +++ b/src/models/Role.lua @@ -0,0 +1,32 @@ +local Role = class("Role", require("shared.ModelBase")) + +function Role:ctor( properties ) + Role.super.ctor(self, properties) + + self.heros = {} + self.ignoreHeartbeat = false + +end + +Role.schema = { + key = {"string"}, + id = {"number"}, + uid = {"string", ""}, + sid = {"number", 0}, + name = {"string", ""}, +} + +Role.fields = { + id = true, + uid = true, + sid = true, + name = true, +} + +function Role:data() + return { + id = self:getProperty("id"), + } +end + +return Role \ No newline at end of file diff --git a/src/nodenames.lua b/src/nodenames.lua new file mode 100644 index 0000000..5eb8678 --- /dev/null +++ b/src/nodenames.lua @@ -0,0 +1,3 @@ +center = "127.0.0.1:9000" + +node_01 = "127.0.0.1:9898" \ No newline at end of file diff --git a/src/preload.lua b/src/preload.lua new file mode 100644 index 0000000..eba52bc --- /dev/null +++ b/src/preload.lua @@ -0,0 +1,6 @@ + +local skynet = require "skynet" + +skynet.timex = function () + return math.floor(skynet.time()) +end \ No newline at end of file diff --git a/src/rdsscripts/RedisScripts.lua b/src/rdsscripts/RedisScripts.lua new file mode 100644 index 0000000..c3182e6 --- /dev/null +++ b/src/rdsscripts/RedisScripts.lua @@ -0,0 +1,20 @@ +local _M = {} + +_M["rankDetails"] = { + file = "src/rdsscripts/rankDetails.lua", + sha1 = nil, +} +_M["insertEmail"] = { + file = "src/rdsscripts/insertEmail.lua", + sha1 = nil, +} +_M["refreshAssist"] = { + file = "src/rdsscripts/refreshAssist.lua", + sha1 = nil, +} +_M["assistInfo"] = { + file = "src/rdsscripts/assistInfo.lua", + sha1 = nil, +} + +return _M \ No newline at end of file diff --git a/src/rdsscripts/assistInfo.lua b/src/rdsscripts/assistInfo.lua new file mode 100644 index 0000000..7d2c0bc --- /dev/null +++ b/src/rdsscripts/assistInfo.lua @@ -0,0 +1,55 @@ +local roleId = tonumber(KEYS[1]) + +local formationJson = redis.call("hget", string.format("role:%d", roleId), "pveFormationJson") + +local formation = cjson.decode(formationJson) + +local function formatAttrEx(str) + local tb = {} + for k, v in str:gmatch("([%d.]+)=([%d.]+)") do + tb[#tb+1] = {tonumber(k), tonumber(v)} + end + return tb +end + +local function formatEquips(str) + local tb = {} + for k, v in str:gmatch("([%d.]+)=([%d.]+)") do + tb[tonumber(k)] = tonumber(v) + end + return tb +end + +local heroFields = {"type", "level", "star", "evolveCount", "wake", "breakLevel", "equips"} +local equipFields = {"type", "level", "evolCount", "attrEx"} +for _, hero in ipairs(formation.heros) do + if hero.leader then + local heroInfo = redis.call("hmget", string.format("hero:%d:%d", roleId, hero.id), unpack(heroFields)) + local equipstr = heroInfo[7] + local equips = {} + for part, equipId in equipstr:gmatch("(%d+)=(%d+)") do + part, equipId = tonumber(part), tonumber(equipId) + if equipId ~= 0 then + local equipInfo = redis.call("hmget", string.format("equip:%d:%d", roleId, equipId), unpack(equipFields)) + equips[equipId] = {} + for index, value in ipairs(equipInfo) do + equips[equipId][equipFields[index]] = index ~= 4 and tonumber(value) or formatAttrEx(equipInfo[4] or "") + end + end + end + return cmsgpack.pack { + type = tonumber(heroInfo[1]), + level = tonumber(heroInfo[2]), + star = tonumber(heroInfo[3]), + evolveCount = tonumber(heroInfo[4]), + wake = tonumber(heroInfo[5]), + breakLevel = tonumber(heroInfo[6]), + equips = formatEquips(heroInfo[7]), + equipDtls = equips, + } + end +end + +return cmsgpack.pack {} + + diff --git a/src/rdsscripts/insertEmail.lua b/src/rdsscripts/insertEmail.lua new file mode 100644 index 0000000..c26f66d --- /dev/null +++ b/src/rdsscripts/insertEmail.lua @@ -0,0 +1,27 @@ +local con1 = KEYS[4] or "" +local con2 = KEYS[5] or "" +local con3 = KEYS[6] or "" +local att1 = KEYS[7] or "" +local att2 = KEYS[8] or "" +local att3 = KEYS[9] or "" +local title = KEYS[10] or "" +local content = KEYS[11] or "" +local attachments = KEYS[12] or "" + +-- local roleInfo = redis.call("HGET", string.format("role:%d", KEYS[1]), "delete") + +-- if tonumber(roleInfo) == 1 then return end + +local id = redis.call("HINCRBY", string.format("role:%d:autoincr", KEYS[1]), "email", 1) +redis.call("LPUSH", string.format("role:%d:emailIds", KEYS[1]), id) +local deleteIds = redis.call("LRANGE", string.format("role:%d:emailIds", KEYS[1]), 50, -1) +for _, deleteId in ipairs(deleteIds) do + redis.call("DEL", string.format("email:%d:%d", KEYS[1], deleteId)) +end + +redis.call("LTRIM", string.format("role:%d:emailIds", KEYS[1]), 0, 49) +redis.call("HMSET", string.format("email:%d:%d", KEYS[1], id), "id", tostring(id), "emailId", KEYS[2], + "status", "0", "createtime", KEYS[3], + "con1", con1, "con2", con2, "con3", con3, + "att1", att1, "att2", att2, "att3", att3, + "title", title, "content", content, "attachments", attachments) diff --git a/src/rdsscripts/rankDetails.lua b/src/rdsscripts/rankDetails.lua new file mode 100644 index 0000000..fa2234b --- /dev/null +++ b/src/rdsscripts/rankDetails.lua @@ -0,0 +1,30 @@ +local field = KEYS[1] +local roleId = tonumber(KEYS[2]) + +local formationJson = redis.call("hget", string.format("role:%d", roleId), field .. "FormationJson") + +local formation = cjson.decode(formationJson) + +local response = {formation = {}, heros = {}} + +if formation.heros then + for _, hero in ipairs(formation.heros) do + table.insert(response.formation, hero.id) + end +end + +local heroIds = redis.call("smembers", string.format("role:%d:heroIds", roleId)) + +local heroFields = {"type", "level", "star", "evolveCount", "wake", "breakLevel"} +for _, heroId in ipairs(heroIds) do + local heroId = tonumber(heroId) + local heroInfo = redis.call("hmget", string.format("hero:%d:%d", roleId, heroId), unpack(heroFields)) + + local tb = {} + for k, v in ipairs(heroInfo) do + tb[heroFields[k]] = tonumber(v) + end + response.heros[heroId] = tb +end + +return cmsgpack.pack(response) \ No newline at end of file diff --git a/src/rdsscripts/refreshAssist.lua b/src/rdsscripts/refreshAssist.lua new file mode 100644 index 0000000..ed466df --- /dev/null +++ b/src/rdsscripts/refreshAssist.lua @@ -0,0 +1,58 @@ +local roleId = tonumber(KEYS[1]) + +local friendKey = "role:%d:friend" +local assistKey = "role:%d:assist" + +local function formatTable(tbl) + local t = {} + for _, id in ipairs(tbl) do + t[tonumber(id)] = 1 + end + return t +end + +local friendIds = redis.call("smembers", friendKey:format(roleId)) +local assistIds = formatTable(redis.call("smembers", assistKey:format(roleId))) + +local heroFields = {"type", "level", "star", "evolveCount", "wake", "breakLevel", "battleValue", "dress"} +local function getLeader(id, formation) + for _, hero in ipairs(formation.heros) do + if hero.leader then + local heroInfo = redis.call("hmget", string.format("hero:%d:%d", id, hero.id), unpack(heroFields)) + return { + type = tonumber(heroInfo[1]), + level = tonumber(heroInfo[2]), + star = tonumber(heroInfo[3]), + evolveCount = tonumber(heroInfo[4]), + wake = tonumber(heroInfo[5]), + breakLevel = tonumber(heroInfo[6]), + battleValue = tonumber(heroInfo[7]), + dress = tonumber(heroInfo[8]), + } + end + end +end + +local response = {} +for _, id in ipairs(friendIds) do + id = tonumber(id) + local dtls = redis.call("hmget", string.format("role:%d", id), + "name", "level", "vip", "pveFormationJson") + local formation = cjson.decode(dtls[4]) + local leader = getLeader(id, formation) + if leader then + table.insert(response, { + roleId = id, + name = dtls[1], + level = tonumber(dtls[2]), + vip = tonumber(dtls[3]), + leader = leader, + used = assistIds[id] or 0, + }) + end +end + +return cmsgpack.pack(response) + + + diff --git a/src/services/agent_ctrl.lua b/src/services/agent_ctrl.lua new file mode 100644 index 0000000..44d1b21 --- /dev/null +++ b/src/services/agent_ctrl.lua @@ -0,0 +1,208 @@ +local skynet = require "skynet" +local socket = require "skynet.socket" +local redisproxy = require "shared.redisproxy" +local netpack = require "skynet.netpack" +local xxtea = require "xxtea" +local deque = require "deque" +local datacenter = require "skynet.datacenter" + +local pcall = pcall +local string_format = string.format + +local poold + +-- agent过期时间 10分钟 +local AGENT_EXPIRE_TIME = 300 + +local _M = { + -- fd -> uid + f2u = {}, + -- uid -> 32 << fd | agent + u2f = {}, + -- fd -> ip + f2i = {}, + -- fd -> expire + f2e = {}, + online = 0, +} + +local function get_f(pack) + return (pack >> 32) & 0x7fffffff +end + +local function get_a(pack) + return pack & 0xffffffff +end + +local function set_pack(fd, agent) + return (fd << 32) | agent +end + +function _M:init(obj, serice) + self.factory = deque.clone(obj) + poold = serice +end + +-- @desc: agent退出 +function _M:exit_agent(fd) + self.f2e[fd] = nil + local uid = self.f2u[fd] + if not uid then return end + + local pack = self.u2f[uid] + if not pack then + self.f2u[fd] = nil + return + end + + local agent = get_a(pack) + + pcall(skynet.send, agent, "lua", "exit") + pcall(skynet.send, poold, "lua", "feed") + + self.f2u[fd] = nil + self.u2f[uid] = nil +end + +-- @desc: 客户端连入 +function _M:socket_open(fd, addr) + self.f2i[fd] = addr +end + +-- @desc: 网络关闭 +function _M:socket_close(fd) + self.f2i[fd] = nil + local uid = self.f2u[fd] + if not uid then return end + self.f2e[fd] = skynet.timex() + AGENT_EXPIRE_TIME + + if not self.u2f[uid] then + self.f2u[fd] = nil + return + end + local agent = get_a(self.u2f[uid]) + local ok, _ = pcall(skynet.call, agent, "lua", "close") + if not ok then self:exit_agent(fd) end +end + +-- @desc: 网络出错 +function _M:socket_error(fd) + self.f2i[fd] = nil + local uid = self.f2u[fd] + if not uid then return end + + if not self.u2f[uid] then + self.f2u[fd] = nil + return + end + local agent = get_a(self.u2f[uid]) + -- 无论失败否,应该让逻辑走下去,保证agent状态以及索引正确 + pcall(skynet.call, agent, "lua", "close") + self:exit_agent(fd) +end + +local function table_nums(t) + local count = 0 + for k, v in pairs(t) do + count = count + 1 + end + return count +end + +local next_check_time = 0 +local next_log_time = 0 +local CHECK_AGENT_STATUS_INTERVAL = 100 -- 检查agent状态的定时间隔 +-- @desc: 检查agent状态,若过期,则让agent退出;并定时打日志统计在线人数 +function _M:check_agent_status() + local now = skynet.timex() + if now >= next_check_time then + next_check_time = now + CHECK_AGENT_STATUS_INTERVAL + for fd, expire in pairs(self.f2e) do + if expire < now then + self:exit_agent(fd) + end + end + end + + if now >= next_log_time and now % 60 == 0 and logd then + next_log_time = now + 60 + local count = table_nums(self.u2f) + datacenter.set("onlineCount", count) + pcall(skynet.send, logd, "lua", "log", "online", {count = count}) + end +end + +local function query_agent_response(fd, response) + local head = string.pack("H", actionCodes.Role_queryLoginRpc + rpcResponseBegin) + + local bin = MsgPack.pack(response) + if #bin > 0 then bin = xxtea.encrypt(bin, XXTEA_KEY) end + socket.write(fd, netpack.pack(head .. bin)) +end + +-- @desc: 玩家登陆第一个包,queryLogin,watchdog为客户端分配一个agent,并告诉gate分配成功,之后的消息直接走agent +function _M:query_agent(fd, uid) + local pack = self.u2f[uid] + if pack then + local f = get_f(pack) + if fd == f then + skynet.error(string.format("%s same fd %d", uid, fd)) + return + end + + -- self.f2u[f] 肯定存在;self.f2e[f]不存在,则说明在线,则需要踢下线 + if not self.f2e[f] then + local head = string.pack("H", actionCodes.Sys_kickdown) + -- local bin = MsgPack.pack({body = "该账号已登上其他机器"}) + local bin = MsgPack.pack({body = "既にこのアカウントを利用している端末があります。"}) + if #bin > 0 then bin = xxtea.encrypt(bin, XXTEA_KEY) end + socket.write(f, netpack.pack(head .. bin)) + skynet.timeout(10, function () + skynet.call(gate_serv, "lua", "kick", f) + end) + end + + local agent = get_a(pack) + local ok = pcall(skynet.call, agent, "lua", "start", gate_serv, fd, self.f2i[fd]) + if not ok then + query_agent_response(fd, {ret = "INNER_ERROR"}) + return + end + + self.f2e[f] = nil + self.f2u[f] = nil + self.u2f[uid] = set_pack(fd, agent) + else + -- 该uid未存储,则说明至少超过10分钟未登陆,由agent池服务pop出一个agent + local agent = self.factory:pop() + if not agent then + -- 服务器满 + query_agent_response(fd, {ret = "RET_SERVER_FULL"}) + return + end + + local ok = pcall(skynet.call, agent, "lua", "start", gate_serv, fd, self.f2i[fd]) + if not ok then + self.factory:push(agent) + query_agent_response(fd, {ret = "INNER_ERROR"}) + return + end + + self.u2f[uid] = set_pack(fd, agent) + end + + self.f2u[fd] = uid + + local response = {} + + local user = redisproxy:get(string_format("uid:%s", uid)) + if user then + response.ret = "RET_HAS_EXISTED" + response.name = user + else + response.ret = "RET_NOT_EXIST" + end + query_agent_response(fd, response) +end + +return _M \ No newline at end of file diff --git a/src/services/agent_util.lua b/src/services/agent_util.lua new file mode 100644 index 0000000..593ef6e --- /dev/null +++ b/src/services/agent_util.lua @@ -0,0 +1,109 @@ + +local _M = { } + +-- 超时次数 +local heartTimeoutCount = 0 +-- 加速次数 +local heartQuickCount = 0 +-- 上次检查心跳时间 +local lastHeartCheckTime = 0 +-- 下次进入定时检查的时间 +local nextCheckTime = 0 +-- 心跳误差允许范围 +local HEART_BEAT_ERROR_LIMIT = 1 +-- 最大超时次数 +local HEART_TIMEOUT_COUNT_MAX = 20 +-- 最大加速次数 +local HEART_QUICK_COUNT_MAX = 5 +-- 心跳定时间隔 +local HEART_TIMER_INTERVAL = 5 + +local function check_heart_beat(agent, now) + -- 充值等操作不检查心跳 + local role = agent.role + if role.ignoreHeartbeat then + lastHeartCheckTime = now - HEART_TIMER_INTERVAL + heartTimeoutCount = 0 + return + end + if lastHeartCheckTime - now > HEART_TIMER_INTERVAL or + now - lastHeartCheckTime > HEART_TIMER_INTERVAL then + heartTimeoutCount = heartTimeoutCount + 1 + if heartTimeoutCount >= HEART_TIMEOUT_COUNT_MAX then + skynet.error("timeout! then agent will shut down by self", agent.client_fd, role:getProperty("name"), role:getProperty("id")) + skynet.call(agent.gate_serv, "lua", "kick", agent.client_fd) + heartTimeoutCount = 0 + end + else + heartTimeoutCount = 0 + end +end + +local PointDataMark = {} +local resetTimeStr = string.format("%02d00", RESET_TIME) + +local function check_daily_reset(agent, now) + local date = os.date("*t", now) + local timeStr = string.format("%02d%02d", date.hour, date.min) + local dataStr = date.year .. string.format("%02d", date.month) .. string.format("%02d", date.day) + + local function timeEffect(checkTimeStr) + if timeStr ~= checkTimeStr then + return false + end + if PointDataMark[dataStr] and PointDataMark[dataStr][checkTimeStr] then + return false + end + PointDataMark[dataStr] = PointDataMark[dataStr] or {} + PointDataMark[dataStr][checkTimeStr] = true + return true + end + + if timeEffect(resetTimeStr) then + -- 刷新每日数据 + local role = agent.role + if role then + role:onCrossDay(now, true) + end + end +end + +function _M:update(agent) + local now = skynet.timex() + local role = agent.role + if now >= nextCheckTime then + pcall(check_heart_beat, agent, now) + nextCheckTime = now + HEART_TIMER_INTERVAL + end + pcall(check_daily_reset, agent, now) + pcall(role.onRecoverTimer, role, now) +end + +function _M:heart_beat(agent) + local now = skynet.timex() + if now == lastHeartCheckTime then + return + end + if now - lastHeartCheckTime <= HEART_TIMER_INTERVAL - HEART_BEAT_ERROR_LIMIT then + heartQuickCount = heartQuickCount + 1 + if heartQuickCount == HEART_QUICK_COUNT_MAX then + -- 将错误写入日志 + local role = agent.role + skynet.error("Warning, heart beating is too quick, shut down the agent", agent.client_fd, role:getProperty("name"), role:getProperty("id")) + -- skynet.call(agent.gate_serv, "lua", "kick", agent.client_fd) + role:warningHeartTooQuick() + heartQuickCount = 0 + end + else + heartQuickCount = 0 + end + lastHeartCheckTime = now +end + +function _M:reset() + heartTimeoutCount = 0 + heartQuickCount = 0 + lastHeartCheckTime = skynet.timex() +end + +return _M diff --git a/src/services/chated.lua b/src/services/chated.lua new file mode 100644 index 0000000..aec8020 --- /dev/null +++ b/src/services/chated.lua @@ -0,0 +1,56 @@ +local skynet = require "skynet" +require "skynet.manager" +local crab = require "crab.c" + +local table_insert = table.insert +local table_unpack = table.unpack +local mode, id, dict = ... + +local function toutf8(name) + local t = {} + for _, v in utf8.codes(name) do + table_insert(t, v) + end + return t +end + +if mode == "sub" then + local CMD = {} + dict = tonumber(dict) + + function CMD.check(name) + if name:find("%c") then + return false + end + local utftb = toutf8(name) + if crab.filter(dict, utftb) then + return false, utf8.char(table_unpack(utftb)) + end + return true + end + + skynet.start(function() + skynet.dispatch("lua", function(_, _, command, ...) + local f = CMD[command] + skynet.ret(skynet.pack(f(...))) + end) + + skynet.register(string.format("CHATED%d", id)) + end) +else + skynet.start(function() + local ok, forbidNames = pcall(require, "csvdata.forbid_chat") + if not ok then forbidNames = {} end + + local words = {} + for _, data in ipairs(forbidNames) do + local ok, utftb = pcall(toutf8, data.name) + if ok then table.insert(words, utftb) end + end + local d = crab.open(words) + + for i = 1, 5 do + skynet.newservice(SERVICE_NAME, "sub", i, d) + end + end) +end diff --git a/src/services/csvdatad.lua b/src/services/csvdatad.lua new file mode 100644 index 0000000..203f13b --- /dev/null +++ b/src/services/csvdatad.lua @@ -0,0 +1,64 @@ +local sharedata = require "skynet.sharedata" +local skynet = require "skynet" +local lfs = require "lfs" +local redisproxy = require "shared.redisproxy" +require "shared.init" +require "utils.init" +require "csvdata.init" +require "skynet.manager" +require "RedisKeys" + +local csvdb = {} + +local function formatFileName(filename) + filename = string.trim(filename) + local basename = filename:match("([^/]+)%.lua$") + if not basename then return end + local loadname = filename:match("^src/([^.]+)%.lua$") + loadname = loadname:gsub('/', '.') + return basename, loadname +end + +local function travCsv(rootPath, pathes) + pathes = pathes or {} + local modified = false + local ok, files, iter = pcall(lfs.dir, rootPath) + if not ok then return modified end + for entry in files, iter do + -- 过滤 . 开始的字符串包括 . .. .git .开头的文件名 + if string.byte(entry, 1) ~= 46 then + local pathfile = rootPath .. '/' .. entry + local attrs = lfs.attributes(pathfile) + if attrs.mode == 'directory' then + modified = travCsv(pathfile, pathes) or modified + else + local basename, loadname = formatFileName(pathfile) + if basename and tonum(pathes[loadname]) < attrs.modification then + modified = true + if basename == "init" then + require(loadname) + else + csvdb[basename .. "Csv"] = require(loadname) + end + pathes[loadname] = attrs.modification + end + end + end + end + return modified +end + +-- 每分钟检查是否有更改 +local file2timeMap = {} +local function handle_timeout() + if travCsv("src/csvdata", file2timeMap) then + sharedata.update("csvdata", csvdb) + end + skynet.timeout(100*5, handle_timeout) +end + +skynet.start(function () + travCsv("src/csvdata", file2timeMap) + sharedata.new("csvdata", csvdb) + -- handle_timeout() +end) diff --git a/src/services/dbseed.lua b/src/services/dbseed.lua new file mode 100644 index 0000000..b7eff0f --- /dev/null +++ b/src/services/dbseed.lua @@ -0,0 +1,54 @@ +require "csvdata.init" +require "shared.init" +require "utils.init" +require "GlobalVar" +require "RedisKeys" +require "ProtocolCode" +require "skynet.manager" +local harbor = require "skynet.harbor" + +skynet = require "skynet" + +redisproxy = require("shared.redisproxy") +globalCsv = require "csvdata/GlobalDefine" + +SendPacket = function ( ... ) end + +local function initRedisDb( ... ) + local servId = tonumber(skynet.getenv("servId")) + if servId then + redisproxy:hsetnx("autoincrement_set", "role", servId * MAX_ROLE_NUM) + redisproxy:hsetnx("autoincrement_set", "union", servId * MAX_ROLE_NUM) + redisproxy:hsetnx("autoincrement_set", "trade", servId * MAX_ROLE_NUM * 100) + redisproxy:hsetnx("autoincrement_set", "email", 0) + redisproxy:hsetnx("autoincrement_set", "emailTimestamp", 0) + redisproxy:hsetnx("autoincrement_set", "delay_email", 0) + end +end + +local steps = { + [1] = { + handler = initRedisDb, + desc = "initialize redis database " + } +} + +skynet.start(function () + redisd = harbor.queryname("REDIS") + + redisproxy = require("shared.redisproxy") + + local new = redisproxy:hsetnx("autoincrement_set", "server_start", os.date("%Y%m%d", skynet.timex())) + if not new then + print("server has been initialized...") + skynet.exit() + return + end + + for _, action in ipairs(steps) do + print(action.desc .. "start ...") + action.handler() + print(action.desc .. "finished ...") + end + skynet.exit() +end) \ No newline at end of file diff --git a/src/services/globald.lua b/src/services/globald.lua new file mode 100644 index 0000000..6c3c287 --- /dev/null +++ b/src/services/globald.lua @@ -0,0 +1,109 @@ +local skynet = require "skynet" +local harbor = require "skynet.harbor" +local json = require("shared.json") +local redisproxy = require("shared.redisproxy") + +require "shared.init" +require "utils.init" +require "RedisKeys" +require "skynet.manager" + +local ipairs = ipairs +local table_insert = table.insert +local tarr2tab = table.array2Table +local string_format = string.format + +local pointDataMark = {} +local utils = {} + +local CHECK_MAIL_STATUS_INTERVAL = 60 + +local function mailQuene() + local delayEmail = tonum(redisproxy:hget("autoincrement_set", "delay_email")) + if delayEmail == 0 then + return + end + local begin = math.max(delayEmail - 100, 1) + local mails = redisproxy:pipelining(function (red) + for id = begin, delayEmail do + red:hgetall(string.format("delayEmail:%s", id)) + end + end) + if not mails then + return + end + local now = skynet.timex() + local mailList = {} + for index, data in ipairs(mails) do + if next(data) then + local email = tarr2tab(data) + if tonum(email.startTime) <= now then + table_insert(mailList, email) + + if #mailList > 100 then + break + end + end + end + end + if #mailList == 0 then + return + end + redisproxy:pipelining(function (red) + for _, email in ipairs(mailList) do + red:del(string_format("delayEmail:%s", email.id)) + end + end) + table.sort(mailList, function(a, b) + return tonum(a.id) < tonum(b.id) + end) + for _, email in ipairs(mailList) do + local gid = redisproxy:hincrby("autoincrement_set", "email", 1) + if email.mid then + redisproxy:hmset(string_format("globalEmail:%s", gid), + "id", gid, + "createtime", email.endTime, + "title", email.title, + "content", email.content, + "attachments", email.attachments, + "endtime", email.endTime, + "mid", email.mid, + "timestamp", email.startTime + ) + else + redisproxy:hmset(string_format("globalEmail:%s", gid), + "id", gid, + "createtime", email.endTime, + "title", email.title, + "content", email.content, + "attachments", email.attachments, + "endtime", email.endTime, + "timestamp", email.startTime + ) + end + end + redisproxy:hset("autoincrement_set", "emailTimestamp", now) +end + +-- @desc: 定时邮件队列检测 +local function check_mail_queue() + pcall(mailQuene) + skynet.timeout(CHECK_MAIL_STATUS_INTERVAL, check_mail_queue) +end + +local CMD = {} +function CMD.start() + check_mail_queue() +end + +local function __init__() + skynet.dispatch("lua", function(_, _, command, ...) + if CMD[command] then + skynet.ret(skynet.pack(CMD[command](...))) + end + end) + redisd = harbor.queryname("REDIS") + skynet.register("GLOBALD") +end + +skynet.start(__init__) diff --git a/src/services/logd.lua b/src/services/logd.lua new file mode 100644 index 0000000..7b6885e --- /dev/null +++ b/src/services/logd.lua @@ -0,0 +1,118 @@ +local skynet = require "skynet" +local mongo = require "mongo" +local queue = require "skynet.queue" +local bson = require "bson" +local socketdriver = require "socketdriver" + +local serverId = tonumber(skynet.getenv("servId")) + +require "shared.init" +require "skynet.manager" + +local table_insert = table.insert +local pairs = pairs +local ipairs = ipairs +local string_format = string.format + +local CMD, cache, client, cs = {}, {} +local auto = {} + +local rsyslog_fd + +-- function getNextSequence(collect) +-- local index = auto[collect] +-- if not index then +-- local res = client.counters:findAndModify({ +-- query = {}, +-- update = {["$inc"] = {[collect] = 1}}, +-- upsert = true, +-- new = true, +-- }) +-- index = res.value[collect] +-- else +-- index = index + 1 +-- end +-- auto[collect] = index +-- return index +-- end + +local dateTypes = {"timestamp", "createTime", "lastLoginTime"} +local stringTypes = {"s1", "s2", "s3"} +local intTypes = {"int1", "int2", "int3", "int4"} +function CMD.log(collect, doc) + -- write to rsyslog + local now = skynet.timex() + doc["timestamp"] = now + doc["server"] = serverId + + for _, field in pairs(stringTypes) do + if doc[field] then + doc[field] = tostring(doc[field]) + end + end + for _, field in pairs(intTypes) do + if doc[field] then + doc[field] = tonumber(doc[field]) + end + end + for _, field in pairs(dateTypes) do + if doc[field] then + doc[field .. "_t"] = tonumber(os.date("%Y%m%d%H%M%S", doc[field])) + end + end + -- Local-6 + Info + socketdriver.send(rsyslog_fd, string.format("<182>%s: %s\n", collect, json.encode(doc))) + + -- if not cache[collect] then cache[collect] = {} end + -- doc["timestamp"] = now + -- doc["_id"] = getNextSequence(collect) + -- table_insert(cache[collect], doc) +end + +function CMD.open(conf) + rsyslog_fd = socketdriver.connect("127.0.0.1", 514) + socketdriver.start(rsyslog_fd) + + -- local db = mongo.client { + -- host = conf.mongohost, + -- port = conf.mongoport or 27017, + -- username = conf.mongouser, + -- password = conf.mongopswd, + -- } + + -- assert(db, "mongo connect error") + + -- local servId = skynet.getenv "servId" + -- client = db["s"..servId] +end + +-- local function __loop__() +-- while true do +-- cs(function () +-- for col, docs in pairs(cache) do +-- client[col]:batch_insert(docs) +-- client.counters:update({}, {["$set"]={[col]=auto[col]}}) +-- end +-- cache = {} +-- end) +-- skynet.sleep(200) +-- end +-- end + +local function __init__() + skynet.dispatch("lua", function (_, _, command, ...) + local f = CMD[command] + if command == "open" then + skynet.ret(skynet.pack(f(...))) + else + local collect, doc = ... + cs(function() f(collect, doc) end) + end + end) + cs = queue() + + -- skynet.fork(__loop__) + skynet.register "LOGD" +end + +skynet.start(__init__) diff --git a/src/services/mcast_util.lua b/src/services/mcast_util.lua new file mode 100644 index 0000000..495d74d --- /dev/null +++ b/src/services/mcast_util.lua @@ -0,0 +1,109 @@ + +local _M = {} + +local mc = require "skynet.multicast" +local datacenter = require "skynet.datacenter" +local skynet = require "skynet" + +local chan_w, chan_g +local chan_ws = {} + +function _M.sub_world(w_channel) + chan_w = mc.new { + channel = w_channel, + dispatch = function (channel, source, ...) + if select("#", ...) == 0 then + return + end + local actionCode, bin = ... + if SendPacket then + SendPacket(actionCode, bin) + end + end + } + chan_w:subscribe() +end + +function _M.usub_world() + if chan_w then + chan_w:unsubscribe() + chan_w = nil + end +end + +function _M.sub_worlds(w_channels) + for _, cell in pairs(w_channels) do + local chan = mc.new { + channel = cell, + dispatch = function (channel, source, ...) + if select("#", ...) == 0 then + return + end + local actionCode, bin = ... + if SendPacket then + SendPacket(actionCode, bin) + end + end + } + chan:subscribe() + table.insert(chan_ws, chan) + end +end + +function _M.pub_world(actionCode, bin) + if not bin then + return + end + if #chan_ws > 0 then + for _, chan in pairs(chan_ws) do + chan:publish(actionCode, bin) + end + return + end + if not bin or not chan_w then return end + chan_w:publish(actionCode, bin) +end + +function _M.sub_union(gid, chan) + chan_g = mc.new { + channel = chan, + dispatch = function (channel, source, ...) + if select("#", ...) == 0 then + return + end + local actionCode, bin = ... + if SendPacket then + SendPacket(actionCode, bin) + end + end + } + chan_g:subscribe() +end + +function _M.usub_union() + if chan_g then + chan_g:unsubscribe() + chan_g = nil + end +end + +function _M.pub_union(actionCode, bin) + if not bin or not chan_g then return end + chan_g:publish(actionCode, bin) +end + +function _M.pub_person(fromFd, toRoleId, actionCode, bin) + if 0 == redisproxy:exists(string.format("role:%d", toRoleId)) then + return SYS_ERROR_CHAT_NOT_EXIST + end + -- 若在线,实时发送聊天信息 + local agent = datacenter.get("agent", toRoleId) + if agent then + SendPacket(actionCode, bin, agent.fd) + SendPacket(actionCode, bin, fromFd) + return + end + return SYS_ERROR_CHAT_NOT_ONLINE +end + +return _M \ No newline at end of file diff --git a/src/services/named.lua b/src/services/named.lua new file mode 100644 index 0000000..a2c5d27 --- /dev/null +++ b/src/services/named.lua @@ -0,0 +1,56 @@ +local skynet = require "skynet" +require "skynet.manager" +local crab = require "crab.c" + +local table_insert = table.insert +local table_unpack = table.unpack +local mode, id, dict = ... + +local function toutf8(name) + local t = {} + for _, v in utf8.codes(name) do + table_insert(t, v) + end + return t +end + +if mode == "sub" then + local CMD = {} + dict = tonumber(dict) + + function CMD.check(name) + if name:find("%c") then + return false + end + local utftb = toutf8(name) + if crab.filter(dict, utftb) then + return false, utf8.char(table_unpack(utftb)) + end + return true + end + + skynet.start(function() + skynet.dispatch("lua", function(_, _, command, ...) + local f = CMD[command] + skynet.ret(skynet.pack(f(...))) + end) + + skynet.register(string.format("NAMED%d", id)) + end) +else + skynet.start(function() + local ok, forbidNames = pcall(require, "csvdata.name_forbid") + if not ok then forbidNames = {} end + + local words = {} + for _, data in ipairs(forbidNames) do + local ok, utftb = pcall(toutf8, data.name) + if ok then table.insert(words, utftb) end + end + local d = crab.open(words) + + for i = 1, 5 do + skynet.newservice(SERVICE_NAME, "sub", i, d) + end + end) +end diff --git a/src/services/ngxd.lua b/src/services/ngxd.lua new file mode 100644 index 0000000..5240b64 --- /dev/null +++ b/src/services/ngxd.lua @@ -0,0 +1,104 @@ +skynet = require "skynet" +redisproxy = require "shared.redisproxy" +netpack = require "skynet.netpack" +datacenter = require "skynet.datacenter" +sharedata = require "skynet.sharedata" +mcast_util = require "services/mcast_util" +globalCsv = require "csvdata/GlobalDefine" + +local socket = require "skynet.socket" +local harbor = require "skynet.harbor" + +require "shared.init" +require "utils.init" +require "ProtocolCode" +require "skynet.manager" +require "RedisKeys" +require "GlobalVar" + +local port = tonumber(...) +local SEP = "$end$" +local actions = {} + +SendPacket = function(...) end +rpcAgent = function(...) end +rpcParter = function(...) end +rpcRole = function(...) end +rpcUnion = function(...) end +rpcOtherUnion = function(...) end + +skynet.register_protocol { + name = "role", + id = 12, + pack = skynet.pack, + unpack = skynet.unpack, +} + +local function process(request) + local req = json.decode(request) + if req["handle"] == "checkStatus" then + return "success" + end + + local modName, funcName = req["handle"]:match("([%w_]+)%.([%w_]+)") + if not modName or not funcName then + return table.concat({"handle格式错误", modName, funcName}, "\t") + end + local action = require("actions." .. modName .. "Action") + return action[funcName](req) +end + +local function main_loop(stdin, send) + socket.lock(stdin) + while true do + local request = socket.readline(stdin, SEP) + if not request then + break + end + + local response = process(request) + if response then send(response) end + end + socket.unlock(stdin) +end + +local function start() + local listen_socket = socket.listen("0.0.0.0", port) + print("Start nginx proxy at port: ", port) + + redisd = harbor.queryname("REDIS") + + csvdb = sharedata.query("csvdata") + + socket.start(listen_socket, function(id, addr) + local function send(...) + local t = { ... } + socket.write(id, table.concat(t, "\t")) + socket.write(id, SEP) + end + socket.start(id) + skynet.fork(main_loop, id, send) + end) + + -- 注册全服广播 + local channels = {} + for i = 1, 10 do + local channel = datacenter.get("MC_W_CHANNEL" .. i) + if channel then + table.insert(channels, channel) + end + end + if #channels > 0 then + mcast_util.sub_worlds(channels) + end +end + +local function __init__() + skynet.dispatch("lua", function(_,_, command, ...) + if command == "start" then + skynet.ret(skynet.pack(start(...))) + end + end) +end + +skynet.start(__init__) diff --git a/src/services/poold.lua b/src/services/poold.lua new file mode 100644 index 0000000..9629846 --- /dev/null +++ b/src/services/poold.lua @@ -0,0 +1,45 @@ +--[[ + deque 为了减少锁竞争,尽量保证 a服务 push;b服务 pop +]] +local skynet = require "skynet" +local deque = require "deque" + +local CMD = {} +local factory + +local dead = 0 +-- agent死亡,通知poold补充,当累计到5个agent时,马上生成5个agent放入poold中 +-- 当然这里也可以写得更复杂,参考redis落地规则 +function CMD.feed() + dead = dead + 1 + if dead == 5 then + dead = 0 + for i=1, 5 do + factory:push(skynet.newservice("agent")) + end + end +end + +-- 系统启动,生成count个agent放入双端队列中 +function CMD.start(count) + factory, addr = deque.new(count) + + for i=1, count do + factory:push(skynet.newservice("agent")) + end + + return addr +end + +local function __init__() + skynet.dispatch("lua", function (_, _, command, ...) + local f = CMD[command] + if command == "start" then + skynet.ret(skynet.pack(f(...))) + return + end + f(...) + end) +end + +skynet.start(__init__) diff --git a/src/services/redisd.lua b/src/services/redisd.lua new file mode 100644 index 0000000..465f63d --- /dev/null +++ b/src/services/redisd.lua @@ -0,0 +1,39 @@ +local skynet = require "skynet" +require "skynet.manager" +local redis = require "skynet.db.redis" + +local db + +local command = {} + +function command.open(conf) + db = redis.connect({ + host = conf.redishost, + port = conf.redisport, + db = conf.redisdb or 0, + auth = conf.auth, + }) + + --[[ + local csvdata = require("csvdata.world_boss_battle") + for i=1, #csvdata do + for j=1, #csvdata[i] do + db:del(string.format("boss:%d:%d", i, j)) + end + end + --]] + db:del("rtpvp") + +end + +skynet.start(function() + skynet.dispatch("lua", function(session, address, cmd, ...) + if cmd == "open" then + local f = command[string.lower(cmd)] + skynet.ret(skynet.pack(f(...))) + else + skynet.ret(skynet.pack(db[string.lower(cmd)](db, ...))) + end + end) + skynet.register "REDIS" +end) \ No newline at end of file diff --git a/src/services/uniond.lua b/src/services/uniond.lua new file mode 100644 index 0000000..6733dfb --- /dev/null +++ b/src/services/uniond.lua @@ -0,0 +1,143 @@ +require "shared.init" +require "utils.init" +require "ProtocolCode" +require "GlobalVar" +require "RedisKeys" +require "skynet.manager" + +local sharedata = require "sharedata" +local redisproxy = require "shared.redisproxy" +local datacenter = require "datacenter" +local queue = require "skynet.queue" + +skynet = require "skynet" +globalCsv = require "csvdata.GlobalDefine" + +local table_pack = table.pack +local table_unpack = table.unpack +local string_format = string.format +local pairs = pairs +local ipairs = ipairs +local tonumber = tonumber + +-- 维护在线状态 + +local unionInfo, CMD, cs = {}, {} + +skynet.register_protocol { + name = "role", + id = 12, + pack = skynet.pack, + unpack = skynet.unpack, +} + +local function handle_timeout() + local now = skynet.timex() + unionInfo:onTimer(now) + skynet.timeout(100, handle_timeout) +end + +--[[ +getProperties({"field1", "field2", ...}) +return: {field1 = value1, field2 = value2, ...} +------- +setProperties({field1 = value1, field2 = value2, ...}) +]] +function rpcRole(roleId, funcName, ...) + if not unionInfo.members[roleId] then return end + local serv = unionInfo.members[roleId].serv + if serv then + if funcName == "getProperties" then + return skynet.call(serv, "role", funcName, table_unpack(...)) + else + return skynet.call(serv, "role", funcName, ...) + end + else + local rediskey = string_format("role:%d", roleId) + if funcName == "getProperty" then + return redisproxy:hget(rediskey, ...) + elseif funcName == "setProperty" then + return redisproxy:hset(rediskey, ...) + elseif funcName == "getProperties" then + local sRole = require "models.Role" + local fields = table_pack(...) + local rets = redisproxy:hmget(rediskey, table_unpack(fields, 1, fields.n)) + local result = {} + for i=1, fields.n do + local typ = sRole.schema[fields[i]][1] + local def = sRole.schema[fields[i]][2] + if typ == "number" then + result[fields[i]] = tonumber(rets[i] or def) + else + result[fields[i]] = rets[i] + end + end + return result + elseif funcName == "setProperties" then + local fields = ... + local params = {} + for field, value in pairs(fields) do + params[#params+1] = field + params[#params+1] = value + end + return redisproxy:hmset(rediskey, table_unpack(params)) + end + end +end + +-- 加载联盟数据 +function CMD.load(unionId) + unionInfo = require("models.Union").new({key = UNION_KEY:format(unionId)}) + unionInfo:load() + unionInfo:loadMembers() +end + +-- 创建一个联盟 +function CMD.new(roleId) + local unionId = redisproxy:hincrby("autoincrement_set", "union", 1) + unionInfo = require("models.Union").new({ + key = UNION_KEY:format(unionId), + master = roleId + }) + unionInfo:create() + unionInfo:addMember(roleId, true) + redisproxy:sadd(UNION_SET, unionId) + return unionId +end + +-- 登录/登出 需要向联盟服务报备 +function CMD.sign(cmd, roleId, serv) + if not unionInfo.members[roleId] then return end + if cmd == "login" then + unionInfo.members[roleId].serv = serv + elseif cmd == "logout" then + unionInfo.members[roleId].serv = nil + end +end + +local function __init__() + skynet.dispatch("lua", function(_, _, command, ...) + cs(function (...) + if CMD[command] then + skynet.ret(skynet.pack(CMD[command](...))) + return + else + if unionInfo and unionInfo[submethod] then + if command == 'dismiss' then + unionInfo:dismiss(...) + return + end + local result = unionInfo[submethod](unionInfo, ...) + skynet.ret(skynet.pack(result)) + return + end + end + skynet.error("uniond commond error cmd=", command) + end) + end) + skynet.register("UNIOND") + csvdb = sharedata.query("csvdata") + cs = queue() +end + +skynet.start(__init__) \ No newline at end of file diff --git a/src/services/watchdog.lua b/src/services/watchdog.lua new file mode 100644 index 0000000..a4cb406 --- /dev/null +++ b/src/services/watchdog.lua @@ -0,0 +1,125 @@ +local skynet = require "skynet" +local redisproxy = require "shared.redisproxy" +local socket = require "skynet.socket" +local netpack = require "skynet.netpack" +local datacenter = require "skynet.datacenter" +local snax = require "skynet.snax" +local agent_ctrl = require "services.agent_ctrl" +local xxtea = require "xxtea" +local mc = require "skynet.multicast" + +require "ProtocolCode" +require "GlobalVar" +require "shared.init" +require "utils.init" + +local CMD, SOCKET = {}, {} +local globald + +local pool_size = tonumber(...) + +function SOCKET.open(fd, addr) + skynet.call(gate_serv, "lua", "accept" , fd) + agent_ctrl:socket_open(fd, addr) +end + +function SOCKET.close(fd) + print("socket close", fd) + agent_ctrl:socket_close(fd) +end + +function SOCKET.error(fd, msg) + print("socket error",fd, msg) + agent_ctrl:socket_error(fd) +end + +function SOCKET.data(fd, msg) + local cmd = string.unpack("H", string.sub(msg, 1, 2)) + if cmd == actionCodes.Role_queryLoginRpc then + local data = MsgPack.unpack(xxtea.decrypt(string.sub(msg, 3), XXTEA_KEY)) + -- TODO: 先检测uid的合法性 + if not data or not data.uid then return end + agent_ctrl:query_agent(fd, data.uid) + end +end + +local use_logd = tonumber(skynet.getenv "logd") + +-- @desc: agent状态定时检测 +function check_agent_status() + pcall(agent_ctrl.check_agent_status, agent_ctrl) + skynet.timeout(1, check_agent_status) +end + +-- 创建world以及union channel 用于广播 +function create_mutilcast() + for i = 1, 10 do + local chan_w = mc:new() + datacenter.set("MC_W_CHANNEL" .. i, chan_w.channel) + end +end + +function CMD.start(conf) + skynet.call(gate_serv, "lua", "open" , conf) + skynet.call(redisd, "lua", "open", conf) + + if use_logd == 1 then + skynet.call(logd, "lua", "open", conf) + end + -- 开启agent状态检测定时器 + check_agent_status() + -- 创建广播服务 + create_mutilcast() + + skynet.call(conf.ngxd, "lua", "start") + + -- roomServer = skynet.newservice("services/roomServer") + -- skynet.call(roomServer, "lua", "start") + + globald = skynet.newservice("services/globald") + skynet.call(globald, "lua", "start") + + skynet.newservice("services/dbseed") + +end + +function CMD.forceClose(fd) + agent_ctrl:exit_agent(fd) +end + +skynet.start(function() + skynet.dispatch("lua", function(session, source, cmd, subcmd, ...) + if cmd == "socket" then + local f = SOCKET[subcmd] + f(...) + -- socket api don't need return + else + local f = assert(CMD[cmd]) + skynet.ret(skynet.pack(f(subcmd, ...))) + end + end) + -- 数据库服务 + redisd = skynet.newservice("services/redisd") + + -- load all csv data + skynet.newservice("services/csvdatad") + print("launch csvdatad ...") + + -- 日志服务 + if use_logd == 1 then + logd = skynet.newservice("services/logd") + end + + local poold = skynet.newservice("services/poold") + local obj = skynet.call(poold, "lua", "start", pool_size) + + agent_ctrl:init(obj, poold) + + print(string.format("launch %d agent at the beginning", pool_size)) + + -- 全局工具函数 + skynet.newservice("services/named") + skynet.newservice("services/chated") + -- 网关服务 + gate_serv = skynet.newservice("gate") +end) diff --git a/src/shared/ModelBase.lua b/src/shared/ModelBase.lua new file mode 100644 index 0000000..f020b74 --- /dev/null +++ b/src/shared/ModelBase.lua @@ -0,0 +1,240 @@ +local ModelBase = class("ModelBase") +ModelBase.key = "key" +ModelBase.schema = { + key = {"string"} +} +ModelBase.fields = {} -- 数据库字段 field, update 是否立即更新 + +local string_format = string.format +local table_insert = table.insert +local table_unpack = table.unpack +local assert = assert +local next = next +local ipairs = ipairs +local pairs = pairs +local tostring = tostring +local tonumber = tonumber +local redisproxy = redisproxy + +local function filterProperties(properties, filter) + for i, field in ipairs(filter) do + properties[field] = nil + end +end + +function ModelBase:ctor(properties) + self.isModelBase_ = true + -- self.dirtyFields = {} + + if type(properties) ~= "table" then properties = {} end + self:setProperties(properties, true) --缺少的域将设置默认值 +end + +--[[-- + +返回对象的 ID 值。 + +**Returns:** + +- ID 值 + +]] +function ModelBase:getKey() + local id = self[self.class.key .. "_"] + assert(id ~= nil, string_format("%s [%s:getKey()] Invalid key", tostring(self), self.class.__cname)) + return id +end + +function ModelBase:load(properties) + if not self:isValidKey() then + print(string_format("%s [%s:id] should be set before load", tostring(self), self.class.__cname)) + return false + end + + if not properties then + properties = redisproxy:hgetall(self:getKey()) + end + + if not next(properties) then return false end + + self:setProperties(properties, true) + + self:onLoad() + + return true +end + +--创建model对应的redis数据, 必须已经设置了ID +function ModelBase:create() + if not self:isValidKey() then + print(string_format("%s [%s:key] should be set before create", tostring(self), self.class.__cname)) + return nil + end + + --将所有的域都置为dirty, 存储到redis + -- for fieldName, update in pairs(self.class.fields) do + -- self.dirtyFields[fieldName] = true + -- end + self:save() + self:onCreate() + + return self +end + +function ModelBase:save() + local redisProperties = self:getProperties() + + local params = {} + for fieldName, value in pairs(redisProperties) do + -- if self.dirtyFields[fieldName] then + local propname = fieldName .. "_" + table_insert(params, fieldName) + table_insert(params, self[propname]) + -- end + end + if next(params) then + redisproxy:hmset(self:getKey(), table_unpack(params)) + end +end + +--[[-- + +确定对象是否设置了有效的 key。 + +]] +function ModelBase:isValidKey() + local propname = self.class.key .. "_" + local key = self[propname] + return type(key) == "string" and key ~= "" +end + +--[[-- + +修改对象的属性。 +NOTE: 如果properties缺少schema中的域, 将用默认值来填充 + +**Parameters:** + +- properties: 包含属性值的数组 + +]] +function ModelBase:setProperties(properties, notWrite) + assert(type(properties) == "table", "Invalid properties") + -- string_format("%s [%s:setProperties()] Invalid properties", tostring(self), self.class.__cname)) + + local params = {} + for field, schema in pairs(self.class.schema) do + local typ, def = table_unpack(schema) + local propname = field .. "_" + + local val = properties[field] or def + if val ~= nil then + if typ == "number" then val = tonumber(val) end + assert(type(val) == typ, + string_format("%s [%s:setProperties()] Type mismatch, %s expected %s, actual is %s", + tostring(self), self.class.__cname, field, typ, type(val))) + self[propname] = val + table_insert(params, field) + table_insert(params, val) + end + end + if not notWrite and next(params) then + redisproxy:hmset(self:getKey(), table_unpack(params)) + end +end + +--[[-- + +取得对象的属性值。 + +**Parameters:** + +- fields: 要取得哪些属性的值,如果未指定该参数,则返回 fields 中设定的属性 +- filter: 要从结果中过滤掉哪些属性,如果未指定则不过滤 + +**Returns:** + +- 包含属性值的数组 + +]] +function ModelBase:getProperties(fields, filter) + local schema = self.class.schema + if type(fields) ~= "table" then fields = table.keys(self.class.fields) end + + local properties = {} + for i, field in ipairs(fields) do + local propname = field .. "_" + local typ = schema[field][1] + local val = self[propname] + assert(type(val) == typ, + string_format("%s [%s:getProperties()] Type mismatch, %s expected %s, actual is %s", + tostring(self), self.class.__cname, field, typ, type(val))) + properties[field] = val + end + + if type(filter) == "table" then + filterProperties(properties, filter) + end + + return properties +end + +function ModelBase:getProperty(property) + if type(property) ~= "string" then return nil end + if not self.class.schema[property] then return nil end + return self:getProperties({property})[property] +end + +function ModelBase:setProperty(property, value, update) + if not self.class.schema[property] then + print(string_format("%s [%s:setProperty()] Invalid property : %s", + tostring(self), self.class.__cname, property)) + return + end + + local typ, def = table_unpack(self.class.schema[property]) + local propname = property .. "_" + + if typ == "number" then value = tonumber(value) end + assert(type(value) == typ, + string_format("%s [%s:setProperties()] Type mismatch, %s expected %s, actual is %s", + tostring(self), self.class.__cname, property, typ, type(value))) + self[propname] = value + + if self.class.fields[property] or update then + redisproxy:hset(self:getKey(), property, value) + else + -- self.dirtyFields[property] = true -- record the fields been modified + end +end + +function ModelBase:incrProperty(property, value, update) + if not self.class.schema[property] then + print(string_format("%s [%s:setProperty()] Invalid property : %s", + tostring(self), self.class.__cname, property)) + return + end + + local typ, def = table_unpack(self.class.schema[property]) + local propname = property .. "_" + + if typ == "number" then value = tonumber(value) end + assert(type(value) == typ, + string_format("%s [%s:setProperties()] Type mismatch, %s expected %s, actual is %s", + tostring(self), self.class.__cname, property, typ, type(value))) + self[propname] = self[propname] + value + + if self.class.fields[property] or update then + return redisproxy:hincrby(self:getKey(), property, value) + else + -- self.dirtyFields[property] = true -- record the fields been modified + end +end + +function ModelBase:onLoad() +end + +function ModelBase:onCreate() +end + +return ModelBase \ No newline at end of file diff --git a/src/shared/crypto.lua b/src/shared/crypto.lua new file mode 100644 index 0000000..ac93596 --- /dev/null +++ b/src/shared/crypto.lua @@ -0,0 +1,24 @@ +local crypto = {} + +function crypto.encryptXXTEA(plaintext, key) + return CCCrypto:encryptXXTEALua(plaintext, string.len(plaintext), key, string.len(key)) +end + +function crypto.decryptXXTEA(ciphertext, key) + return CCCrypto:decryptXXTEALua(ciphertext, string.len(ciphertext), key, string.len(key)) +end + +function crypto.encodeBase64(plaintext) + return CCCrypto:encodeBase64Lua(plaintext, string.len(plaintext)) +end + +function crypto.decodeBase64(ciphertext) + return CCCrypto:decodeBase64Lua(ciphertext) +end + +function crypto.md5(input, isRawOutput) + if type(isRawOutput) ~= "boolean" then isRawOutput = false end + return CCCrypto:MD5Lua(input, isRawOutput) +end + +return crypto diff --git a/src/shared/debug.lua b/src/shared/debug.lua new file mode 100644 index 0000000..300910d --- /dev/null +++ b/src/shared/debug.lua @@ -0,0 +1,327 @@ +--[[ + +Copyright (c) 2011-2012 qeeplay.com + +http://dualface.github.com/quick-cocos2d-x/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +--[[-- + +Debug functions. + +## Functions ## + +- echo +- echoInfo +- echoError +- printf + +]] +local skynet = require "skynet" +if ngx and ngx.say then + echo_ = ngx.say +elseif CCLuaLog then + echo_ = CCLuaLog +end +if not echo_ then echo_ = print end +if not echof_ then echof_ = skynet.error end + +io.output():setvbuf('no') + +function echo(...) + local arr = {} + for i, a in ipairs({...}) do + arr[#arr + 1] = tostring(a) + end + + echo_(table.concat(arr, "\t")) +end + +function echof( ... ) + local arr = {} + for i, a in ipairs({...}) do + arr[#arr + 1] = tostring(a) + end + + echof_(table.concat(arr, "\t")) +end + +if CCLuaLog then print = echo end + +--[[-- + +Output a formatted string. + +Depends on the platform, output to console or log file. @see echo(). + +@param string format +@param mixed ... + +@see echo + +]] +function printf(fmt, ...) + echo(string.format(tostring(fmt), ...)) +end + +function echoError(fmt, ...) + echo(string.format("[ERR] %s%s", string.format(tostring(fmt), ...), debug.traceback("", 2))) +end + +function echoInfo(fmt, ...) + echo("[INFO] " .. string.format(tostring(fmt), ...)) +end + +function echoLog(tag, fmt, ...) + echo(string.format("[%s] %s", string.upper(tostring(tag)), string.format(tostring(fmt), ...))) +end + +function getPackageName(moduleName) + local packageName = "" + local pos = string.find(string.reverse(moduleName), "%.") + if pos then + packageName = string.sub(moduleName, 1, string.len(moduleName) - pos + 1) + end + return packageName +end + +--[[-- + +Dumps information about a variable. + +@param mixed object +@param string label +@param bool isReturnContents +@param int nesting +@return nil|string + +]] +function dump(object, label, isReturnContents, nesting) + if type(nesting) ~= "number" then nesting = 99 end + + local lookupTable = {} + local result = {} + + local function _v(v) + if type(v) == "string" then + v = "\"" .. v .. "\"" + end + return tostring(v) + end + + local traceback = string.split(debug.traceback("", 2), "\n") + echo("dump from: " .. string.trim(traceback[3])) + + local function _dump(object, label, indent, nest, keylen) + label = label or "" + spc = "" + if type(keylen) == "number" then + spc = string.rep(" ", keylen - string.len(_v(label))) + end + if type(object) ~= "table" then + result[#result +1 ] = string.format("%s%s%s = %s", indent, _v(label), spc, _v(object)) + elseif lookupTable[object] then + result[#result +1 ] = string.format("%s%s%s = *REF*", indent, label, spc) + else + lookupTable[object] = true + if nest > nesting then + result[#result +1 ] = string.format("%s%s = *MAX NESTING*", indent, label) + else + result[#result +1 ] = string.format("%s%s = {", indent, _v(label)) + local indent2 = indent.." " + local keys = {} + local keylen = 0 + local values = {} + for k, v in pairs(object) do + keys[#keys + 1] = k + local vk = _v(k) + local vkl = string.len(vk) + if vkl > keylen then keylen = vkl end + values[k] = v + end + table.sort(keys, function(a, b) + if type(a) == "number" and type(b) == "number" then + return a < b + else + return tostring(a) < tostring(b) + end + end) + for i, k in ipairs(keys) do + _dump(values[k], k, indent2, nest + 1, keylen) + end + result[#result +1] = string.format("%s}", indent) + end + end + end + _dump(object, label, "- ", 1) + + if isReturnContents then + return table.concat(result, "\n") + end + + for i, line in ipairs(result) do + echo(line) + end +end + + +--[[-- + +Dumps information about a variable into file. + +@param mixed object +@param string label +@param bool isReturnContents +@param int nesting +@return nil|string + +]] +function dump2file(object, label, isReturnContents, nesting) + if type(nesting) ~= "number" then nesting = 99 end + + local lookupTable = {} + local result = {} + + local function _v(v) + if type(v) == "string" then + v = "\"" .. v .. "\"" + end + return tostring(v) + end + + local traceback = string.split(debug.traceback("", 2), "\n") + echo("dump from: " .. string.trim(traceback[3])) + + local function _dump(object, label, indent, nest, keylen) + label = label or "" + spc = "" + if type(keylen) == "number" then + spc = string.rep(" ", keylen - string.len(_v(label))) + end + if type(object) ~= "table" then + result[#result +1 ] = string.format("%s%s%s = %s", indent, _v(label), spc, _v(object)) + elseif lookupTable[object] then + result[#result +1 ] = string.format("%s%s%s = *REF*", indent, label, spc) + else + lookupTable[object] = true + if nest > nesting then + result[#result +1 ] = string.format("%s%s = *MAX NESTING*", indent, label) + else + result[#result +1 ] = string.format("%s%s = {", indent, _v(label)) + local indent2 = indent.." " + local keys = {} + local keylen = 0 + local values = {} + for k, v in pairs(object) do + keys[#keys + 1] = k + local vk = _v(k) + local vkl = string.len(vk) + if vkl > keylen then keylen = vkl end + values[k] = v + end + table.sort(keys, function(a, b) + if type(a) == "number" and type(b) == "number" then + return a < b + else + return tostring(a) < tostring(b) + end + end) + for i, k in ipairs(keys) do + _dump(values[k], k, indent2, nest + 1, keylen) + end + result[#result +1] = string.format("%s}", indent) + end + end + end + _dump(object, label, "- ", 1) + + if isReturnContents then + return table.concat(result, "\n") + end + + for i, line in ipairs(result) do + echof(line) + end +end + +--[[-- + +Outputs or returns a parsable string representation of a variable. + +@param mixed object +@param string label +@return string + +]] +function vardump(object, label) + local lookupTable = {} + local result = {} + + local function _v(v) + if type(v) == "string" then + v = "\"" .. v .. "\"" + end + return tostring(v) + end + + local function _vardump(object, label, indent, nest) + label = label or "" + local postfix = "" + if nest > 1 then postfix = "," end + if type(object) ~= "table" then + if type(label) == "string" then + result[#result +1] = string.format("%s%s = %s%s", indent, label, _v(object), postfix) + else + result[#result +1] = string.format("%s%s%s", indent, _v(object), postfix) + end + elseif not lookupTable[object] then + lookupTable[object] = true + + if type(label) == "string" then + result[#result +1 ] = string.format("%s%s = {", indent, label) + else + result[#result +1 ] = string.format("%s{", indent) + end + local indent2 = indent .. " " + local keys = {} + local values = {} + for k, v in pairs(object) do + keys[#keys + 1] = k + values[k] = v + end + table.sort(keys, function(a, b) + if type(a) == "number" and type(b) == "number" then + return a < b + else + return tostring(a) < tostring(b) + end + end) + for i, k in ipairs(keys) do + _vardump(values[k], k, indent2, nest + 1) + end + result[#result +1] = string.format("%s}%s", indent, postfix) + end + end + _vardump(object, label, "", 1) + + return table.concat(result, "\n") +end diff --git a/src/shared/functions.lua b/src/shared/functions.lua new file mode 100644 index 0000000..ad2a390 --- /dev/null +++ b/src/shared/functions.lua @@ -0,0 +1,846 @@ +--[[ + +Copyright (c) 2011-2012 qeeplay.com + +http://dualface.github.com/quick-cocos2d-x/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +--[[-- + +Convert to number. + +@param mixed v +@return number + +]] +function tonum(v, default) + default = default or 0 + return tonumber(v) or default +end + +--[[-- + +Convert to integer. + +@param mixed v +@return number(integer) + +]] +function toint(v) + return math.round(tonumber(v)) +end + +--[[-- + +Convert to boolean. + +@param mixed v +@return boolean + +]] +function tobool(v) + return (v ~= nil and v ~= false) +end + +--[[-- + +Convert to table. + +@param mixed v +@return table + +]] +function totable(v) + if type(v) ~= "table" then v = {} end + return v +end + +--[[-- + +Returns a formatted version of its variable number of arguments following the description given in its first argument (which must be a string). string.format() alias. + +@param string format +@param mixed ... +@return string + +]] +function format(...) + return string.format(...) +end + +--[[-- + +Creating a copy of an table with fully replicated properties. + +**Usage:** + + -- Creating a reference of an table: + local t1 = {a = 1, b = 2} + local t2 = t1 + t2.b = 3 -- t1 = {a = 1, b = 3} <-- t1.b changed + + -- Createing a copy of an table: + local t1 = {a = 1, b = 2} + local t2 = clone(t1) + t2.b = 3 -- t1 = {a = 1, b = 2} <-- t1.b no change + + +@param mixed object +@return mixed + +]] +function clone(object) + local lookup_table = {} + local function _copy(object) + if type(object) ~= "table" then + return object + elseif lookup_table[object] then + return lookup_table[object] + end + local new_table = {} + lookup_table[object] = new_table + for key, value in pairs(object) do + new_table[_copy(key)] = _copy(value) + end + return setmetatable(new_table, getmetatable(object)) + end + return _copy(object) +end + +--[[-- + +Create an class. + +**Usage:** + + local Shape = class("Shape") + + -- base class + function Shape:ctor(shapeName) + self.shapeName = shapeName + printf("Shape:ctor(%s)", self.shapeName) + end + + function Shape:draw() + printf("draw %s", self.shapeName) + end + + -- + + local Circle = class("Circle", Shape) + + function Circle:ctor() + Circle.super.ctor(self, "circle") -- call super-class method + self.radius = 100 + end + + function Circle:setRadius(radius) + self.radius = radius + end + + function Circle:draw() -- overrideing super-class method + printf("draw %s, raidus = %0.2f", self.shapeName, self.raidus) + end + + -- + + local Rectangle = class("Rectangle", Shape) + + function Rectangle:ctor() + Rectangle.super.ctor(self, "rectangle") + end + + -- + + local circle = Circle.new() -- output: Shape:ctor(circle) + circle:setRaidus(200) + circle:draw() -- output: draw circle, radius = 200.00 + + local rectangle = Rectangle.new() -- output: Shape:ctor(rectangle) + rectangle:draw() -- output: draw rectangle + + +@param string classname +@param table|function super-class +@return table + +]] +function class(classname, super) + local superType = type(super) + local cls + + if superType ~= "function" and superType ~= "table" then + superType = nil + super = nil + end + + if superType == "function" or (super and super.__ctype == 1) then + -- inherited from native C++ Object + cls = {} + + if superType == "table" then + -- copy fields from super + for k,v in pairs(super) do cls[k] = v end + cls.__create = super.__create + cls.super = super + else + cls.__create = super + cls.ctor = function() end + end + + cls.__cname = classname + cls.__ctype = 1 + + function cls.new(...) + local instance = cls.__create(...) + -- copy fields from class to native object + for k,v in pairs(cls) do instance[k] = v end + instance.class = cls + instance:ctor(...) + return instance + end + + else + -- inherited from Lua Object + if super then + cls = clone(super) + cls.super = super + else + cls = {ctor = function() end} + end + + cls.__cname = classname + cls.__ctype = 2 -- lua + cls.__index = cls + + function cls.new(...) + local instance = setmetatable({}, cls) + instance.class = cls + instance:ctor(...) + return instance + end + end + + return cls +end + +--[[-- + +]] +function import(moduleName, currentModuleName) + local currentModuleNameParts + local moduleFullName = moduleName + local offset = 1 + + while true do + if string.byte(moduleName, offset) ~= 46 then -- . + moduleFullName = string.sub(moduleName, offset) + if currentModuleNameParts and #currentModuleNameParts > 0 then + moduleFullName = table.concat(currentModuleNameParts, ".") .. "." .. moduleFullName + end + break + end + offset = offset + 1 + + if not currentModuleNameParts then + if not currentModuleName then + local n,v = debug.getlocal(3, 1) + currentModuleName = v + end + + currentModuleNameParts = string.split(currentModuleName, ".") + end + table.remove(currentModuleNameParts, #currentModuleNameParts) + end + + return require(moduleFullName) +end + +--[[-- + +]] +function handler(target, method) + return function(...) return method(target, ...) end +end + +--[[-- + +]] +function handlerObject(object) + return function(event, ...) + if object[event] then + return object[event](object, ...) + end + end +end + +--[[-- + +Returns a associative table containing the matching values. + +@param table arr +@param table names +@return array + +]] +function export(arr, names) + local args = {} + for k, def in pairs(names) do + if type(k) == "number" then + args[def] = arr[def] + else + args[k] = arr[k] or def + end + end + return args +end + +--[[-- + +hecks if the given key or index exists in the table. + +@param table arr +@param mixed key +@return boolean + +]] +function isset(arr, key) + return type(arr) == "table" and arr[key] ~= nil +end + +--[[-- + +Rounds a float. + +@param number num +@return number(integer) + +]] +function math.round(num) + return math.floor(num + 0.5) +end + +--[[-- + +Checks whether a file exists. + +@param string path +@return boolean + +]] +function io.exists(path) + local file = io.open(path, "r") + if file then + io.close(file) + return true + end + return false +end + +--[[-- + +Reads entire file into a string, or return FALSE on failure. + +@param string path +@return string + +]] +function io.readfile(path) + local file = io.open(path, "r") + if file then + local content = file:read("*a") + io.close(file) + return content + end + return nil +end + +--[[-- + +Write a string to a file, or return FALSE on failure. + +@param string path +@param string content +@param string mode +@return boolean + +### Note: +The mode string can be any of the following: + "r": read mode + "w": write mode; + "a": append mode; + "r+": update mode, all previous data is preserved; + "w+": update mode, all previous data is erased; (the default); + "a+": append update mode, previous data is preserved, writing is only allowed at the end of file. + +]] +function io.writefile(path, content, mode) + mode = mode or "w+" + local file = io.open(path, mode) + if file then + if file:write(content) == nil then return false end + io.close(file) + return true + else + return false + end +end + +--[[-- + +Returns information about a file path. + +**Usage:** + + local path = "/var/app/test/abc.png" + local pathinfo = io.pathinfo(path) + -- pathinfo.dirname = "/var/app/test/" + -- pathinfo.filename = "abc.png" + -- pathinfo.basename = "abc" + -- pathinfo.extname = ".png" + + +@param string path +@return table + +]] +function io.pathinfo(path) + local pos = string.len(path) + local extpos = pos + 1 + while pos > 0 do + local b = string.byte(path, pos) + if b == 46 then -- 46 = char "." + extpos = pos + elseif b == 47 then -- 47 = char "/" + break + end + pos = pos - 1 + end + + local dirname = string.sub(path, 1, pos) + local filename = string.sub(path, pos + 1) + extpos = extpos - pos + local basename = string.sub(filename, 1, extpos - 1) + local extname = string.sub(filename, extpos) + return { + dirname = dirname, + filename = filename, + basename = basename, + extname = extname + } +end + +--[[-- + +Gets file size, or return FALSE on failure. + +@param string path +@return number(integer) + +]] +function io.filesize(path) + local size = false + local file = io.open(path, "r") + if file then + local current = file:seek() + size = file:seek("end") + file:seek("set", current) + io.close(file) + end + return size +end + +--[[-- + +Count all elements in an table. + +@param table t +@return number(integer) + +]] +function table.nums(t) + local count = 0 + for k, v in pairs(t) do + count = count + 1 + end + return count +end + +--[[-- + +Return all the keys or a subset of the keys of an table. + +**Usage:** + + local t = {a = 1, b = 2, c = 3} + local keys = table.keys(t) + -- keys = {"a", "b", "c"} + + +@param table t +@return table + +]] +function table.keys(t) + local keys = {} + for k, v in pairs(t) do + keys[#keys + 1] = k + end + return keys +end + +--[[-- + +Return all the values of an table. + +**Usage:** + + local t = {a = "1", b = "2", c = "3"} + local values = table.values(t) + -- values = {1, 2, 3} + + +@param table t +@return table + +]] +function table.values(t) + local values = {} + for k, v in pairs(t) do + values[#values + 1] = v + end + return values +end + +--[[-- + +Merge tables. + +**Usage:** + + local dest = {a = 1, b = 2} + local src = {c = 3, d = 4} + table.merge(dest, src) + -- dest = {a = 1, b = 2, c = 3, d = 4} + + +@param table dest +@param table src + +]] +function table.merge(dest, src) + for k, v in pairs(src) do + dest[k] = v + end +end + + +--[[-- + +insert list. + +**Usage:** + + local dest = {1, 2, 3} + local src = {4, 5, 6} + table.insertTo(dest, src) + -- dest = {1, 2, 3, 4, 5, 6} + dest = {1, 2, 3} + table.insertTo(dest, src, 5) + -- dest = {1, 2, 3, nil, 4, 5, 6} + + +@param table dest +@param table src +@param table begin insert position for dest +]] +function table.insertTo(dest, src, begin) + begin = tonumber(begin) + if begin == nil then + begin = #dest + 1 + end + + local len = #src + for i = 0, len - 1 do + dest[i + begin] = src[i + 1] + end +end + +function table.maxkey(tb) + local max = 0 + for k, v in pairs(tb) do + if k > max then + max = k + end + end + return max +end + +function table.minkey(tb) + local min = math.huge + for k, v in pairs(tb) do + if k < min then + min = k + end + end + return min +end + +--[[-- + +Convert special characters to HTML entities. + +The translations performed are: + +- '&' (ampersand) becomes '&' +- '"' (double quote) becomes '"' +- "'" (single quote) becomes ''' +- '<' (less than) becomes '<' +- '>' (greater than) becomes '>' + +@param string input +@return string + +]] +function string.htmlspecialchars(input) + for k, v in pairs(string._htmlspecialchars_set) do + input = string.gsub(input, k, v) + end + return input +end +string._htmlspecialchars_set = {} +string._htmlspecialchars_set["&"] = "&" +string._htmlspecialchars_set["\""] = """ +string._htmlspecialchars_set["'"] = "'" +string._htmlspecialchars_set["<"] = "<" +string._htmlspecialchars_set[">"] = ">" + +--[[-- + +Inserts HTML line breaks before all newlines in a string. + +Returns string with '
' inserted before all newlines (\n). + +@param string input +@return string + +]] +function string.nl2br(input) + return string.gsub(input, "\n", "
") +end + +--[[-- + +Returns a HTML entities formatted version of string. + +@param string input +@return string + +]] +function string.text2html(input) + input = string.gsub(input, "\t", " ") + input = string.htmlspecialchars(input) + input = string.gsub(input, " ", " ") + input = string.nl2br(input) + return input +end + +--[[-- + +Split a string by string. + +@param string str +@param string delimiter +@return table + +]] +function string.split(str, delimiter) + if (delimiter=='') then return false end + local pos,arr = 0, {} + -- for each divider found + for st,sp in function() return string.find(str, delimiter, pos, true) end do + table.insert(arr, string.sub(str, pos, st - 1)) + pos = sp + 1 + end + table.insert(arr, string.sub(str, pos)) + return arr +end + +--[[-- + +Strip whitespace (or other characters) from the beginning of a string. + +@param string str +@return string + +]] +function string.ltrim(str) + return string.gsub(str, "^[ \t\n\r]+", "") +end + +--[[-- + +Strip whitespace (or other characters) from the end of a string. + +@param string str +@return string + +]] +function string.rtrim(str) + return string.gsub(str, "[ \t\n\r]+$", "") +end + +--[[-- + +Strip whitespace (or other characters) from the beginning and end of a string. + +@param string str +@return string + +]] +function string.trim(str) + str = string.gsub(str, "^[ \t\n\r]+", "") + return string.gsub(str, "[ \t\n\r]+$", "") +end + +--[[-- + +Make a string's first character uppercase. + +@param string str +@return string + +]] +function string.ucfirst(str) + return string.upper(string.sub(str, 1, 1)) .. string.sub(str, 2) +end + +--[[-- + +@param string str +@return string + +]] +function string.urlencodeChar(char) + return "%" .. string.format("%02X", string.byte(c)) +end + +--[[-- + +URL-encodes string. + +@param string str +@return string + +]] +function string.urlencode(str) + -- convert line endings + str = string.gsub(tostring(str), "\n", "\r\n") + -- escape all characters but alphanumeric, '.' and '-' + str = string.gsub(str, "([^%w%.%- ])", string.urlencodeChar) + -- convert spaces to "+" symbols + return string.gsub(str, " ", "+") +end + +--[[-- + +Get UTF8 string length. + +@param string str +@return int + +]] +function string.utf8len(str) + local len = #str + local left = len + local cnt = 0 + local arr = {0, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc} + while left ~= 0 do + local tmp = string.byte(str, -left) + local i = #arr + while arr[i] do + if tmp >= arr[i] then + left = left - i + break + end + i = i - 1 + end + cnt = cnt + 1 + end + return cnt +end + +--[[-- + +Return formatted string with a comma (",") between every group of thousands. + +**Usage:** + + local value = math.comma("232423.234") -- value = "232,423.234" + + +@param number num +@return string + +]] +function string.formatNumberThousands(num) + local formatted = tostring(tonumber(num)) + while true do + formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2') + if k == 0 then break end + end + return formatted +end + +function table.find(t, item) + return table.keyOfItem(t, item) ~= nil +end + +function table.keyOfItem(t, item) + for k,v in pairs(t) do + if v == item then return k end + end + return nil +end + +function table.removeItem(list, item, removeAll) + local rmCount = 0 + for i = 1, #list do + if list[i - rmCount] == item then + table.remove(list, i - rmCount) + if removeAll then + rmCount = rmCount + 1 + else + break + end + end + end +end + +function table.array2Table(arr) + local ret = {} + for i=1, #arr, 2 do + ret[arr[i]] = arr[i+1] + end + return ret +end + diff --git a/src/shared/init.lua b/src/shared/init.lua new file mode 100644 index 0000000..7b51117 --- /dev/null +++ b/src/shared/init.lua @@ -0,0 +1,5 @@ +require("shared.functions") +require("shared.debug") + +json = require("shared.json") +MsgPack = require "cmsgpack" \ No newline at end of file diff --git a/src/shared/json.lua b/src/shared/json.lua new file mode 100644 index 0000000..edc38e2 --- /dev/null +++ b/src/shared/json.lua @@ -0,0 +1,431 @@ +----------------------------------------------------------------------------- +-- JSON4Lua: JSON encoding / decoding support for the Lua language. +-- json Module. +-- Author: Craig Mason-Jones +-- Homepage: http://github.com/craigmj/json4lua/ +-- Version: 1.0.0 +-- This module is released under the MIT License (MIT). +-- Please see LICENCE.txt for details. +-- +-- USAGE: +-- This module exposes two functions: +-- json.encode(o) +-- Returns the table / string / boolean / number / nil / json.null value as a JSON-encoded string. +-- json.decode(json_string) +-- Returns a Lua object populated with the data encoded in the JSON string json_string. +-- +-- REQUIREMENTS: +-- compat-5.1 if using Lua 5.0 +-- +-- CHANGELOG +-- 0.9.20 Introduction of local Lua functions for private functions (removed _ function prefix). +-- Fixed Lua 5.1 compatibility issues. +-- Introduced json.null to have null values in associative arrays. +-- json.encode() performance improvement (more than 50%) through table.concat rather than .. +-- Introduced decode ability to ignore /**/ comments in the JSON string. +-- 0.9.10 Fix to array encoding / decoding to correctly manage nil/null values in arrays. +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Imports and dependencies +----------------------------------------------------------------------------- +local math = require('math') +local string = require("string") +local table = require("table") + +----------------------------------------------------------------------------- +-- Module declaration +----------------------------------------------------------------------------- +local json = {} -- Public namespace +local json_private = {} -- Private namespace + +-- Public functions + +-- Private functions +local decode_scanArray +local decode_scanComment +local decode_scanConstant +local decode_scanNumber +local decode_scanObject +local decode_scanString +local decode_scanWhitespace +local encodeString +local isArray +local isEncodable + +table.getn = function (t) + if t.n then + return t.n + else + local n = 0 + for i in pairs(t) do + if type(i) == "number" then + n = math.max(n, i) + end + end + return n + end +end + +----------------------------------------------------------------------------- +-- PUBLIC FUNCTIONS +----------------------------------------------------------------------------- +--- Encodes an arbitrary Lua object / variable. +-- @param v The Lua object / variable to be JSON encoded. +-- @return String containing the JSON encoding in internal Lua string format (i.e. not unicode) +function json.encode (v) + -- Handle nil values + if v==nil then + return "null" + end + + local vtype = type(v) + + -- Handle strings + if vtype=='string' then + return '"' .. json_private.encodeString(v) .. '"' -- Need to handle encoding in string + end + + -- Handle booleans + if vtype=='number' or vtype=='boolean' then + return tostring(v) + end + + -- Handle tables + if vtype=='table' then + local rval = {} + -- Consider arrays separately + local bArray, maxCount = isArray(v) + if bArray then + for i = 1,maxCount do + table.insert(rval, json.encode(v[i])) + end + else -- An object, not an array + for i,j in pairs(v) do + if isEncodable(i) and isEncodable(j) then + table.insert(rval, '"' .. json_private.encodeString(i) .. '":' .. json.encode(j)) + end + end + end + if bArray then + return '[' .. table.concat(rval,',') ..']' + else + return '{' .. table.concat(rval,',') .. '}' + end + end + + -- Handle null values + if vtype=='function' and v==null then + return 'null' + end + + assert(false,'encode attempt to encode unsupported type ' .. vtype .. ':' .. tostring(v)) +end + + +--- Decodes a JSON string and returns the decoded value as a Lua data structure / value. +-- @param s The string to scan. +-- @param [startPos] Optional starting position where the JSON string is located. Defaults to 1. +-- @param Lua object, number The object that was scanned, as a Lua table / string / number / boolean or nil, +-- and the position of the first character after +-- the scanned JSON object. +function json.decode(s, startPos) + startPos = startPos and startPos or 1 + startPos = decode_scanWhitespace(s,startPos) + assert(startPos<=string.len(s), 'Unterminated JSON encoded object found at position in [' .. s .. ']') + local curChar = string.sub(s,startPos,startPos) + -- Object + if curChar=='{' then + return decode_scanObject(s,startPos) + end + -- Array + if curChar=='[' then + return decode_scanArray(s,startPos) + end + -- Number + if string.find("+-0123456789.e", curChar, 1, true) then + return decode_scanNumber(s,startPos) + end + -- String + if curChar==[["]] or curChar==[[']] then + return decode_scanString(s,startPos) + end + if string.sub(s,startPos,startPos+1)=='/*' then + return decode(s, decode_scanComment(s,startPos)) + end + -- Otherwise, it must be a constant + return decode_scanConstant(s,startPos) +end + +--- The null function allows one to specify a null value in an associative array (which is otherwise +-- discarded if you set the value with 'nil' in Lua. Simply set t = { first=json.null } +function null() + return null -- so json.null() will also return null ;-) +end +----------------------------------------------------------------------------- +-- Internal, PRIVATE functions. +-- Following a Python-like convention, I have prefixed all these 'PRIVATE' +-- functions with an underscore. +----------------------------------------------------------------------------- + +--- Scans an array from JSON into a Lua object +-- startPos begins at the start of the array. +-- Returns the array and the next starting position +-- @param s The string being scanned. +-- @param startPos The starting position for the scan. +-- @return table, int The scanned array as a table, and the position of the next character to scan. +function decode_scanArray(s,startPos) + local array = {} -- The return value + local stringLen = string.len(s) + assert(string.sub(s,startPos,startPos)=='[','decode_scanArray called but array does not start at position ' .. startPos .. ' in string:\n'..s ) + startPos = startPos + 1 + -- Infinite loop for array elements + repeat + startPos = decode_scanWhitespace(s,startPos) + assert(startPos<=stringLen,'JSON String ended unexpectedly scanning array.') + local curChar = string.sub(s,startPos,startPos) + if (curChar==']') then + return array, startPos+1 + end + if (curChar==',') then + startPos = decode_scanWhitespace(s,startPos+1) + end + assert(startPos<=stringLen, 'JSON String ended unexpectedly scanning array.') + object, startPos = json.decode(s,startPos) + table.insert(array,object) + until false +end + +--- Scans a comment and discards the comment. +-- Returns the position of the next character following the comment. +-- @param string s The JSON string to scan. +-- @param int startPos The starting position of the comment +function decode_scanComment(s, startPos) + assert( string.sub(s,startPos,startPos+1)=='/*', "decode_scanComment called but comment does not start at position " .. startPos) + local endPos = string.find(s,'*/',startPos+2) + assert(endPos~=nil, "Unterminated comment in string at " .. startPos) + return endPos+2 +end + +--- Scans for given constants: true, false or null +-- Returns the appropriate Lua type, and the position of the next character to read. +-- @param s The string being scanned. +-- @param startPos The position in the string at which to start scanning. +-- @return object, int The object (true, false or nil) and the position at which the next character should be +-- scanned. +function decode_scanConstant(s, startPos) + local consts = { ["true"] = true, ["false"] = false, ["null"] = nil } + local constNames = {"true","false","null"} + + for i,k in pairs(constNames) do + if string.sub(s,startPos, startPos + string.len(k) -1 )==k then + return consts[k], startPos + string.len(k) + end + end + assert(nil, 'Failed to scan constant from string ' .. s .. ' at starting position ' .. startPos) +end + +--- Scans a number from the JSON encoded string. +-- (in fact, also is able to scan numeric +- eqns, which is not +-- in the JSON spec.) +-- Returns the number, and the position of the next character +-- after the number. +-- @param s The string being scanned. +-- @param startPos The position at which to start scanning. +-- @return number, int The extracted number and the position of the next character to scan. +function decode_scanNumber(s,startPos) + local endPos = startPos+1 + local stringLen = string.len(s) + local acceptableChars = "+-0123456789.e" + while (string.find(acceptableChars, string.sub(s,endPos,endPos), 1, true) + and endPos<=stringLen + ) do + endPos = endPos + 1 + end + local stringValue = 'return ' .. string.sub(s,startPos, endPos-1) + local stringEval = load(stringValue) + assert(stringEval, 'Failed to scan number [ ' .. stringValue .. '] in JSON string at position ' .. startPos .. ' : ' .. endPos) + return stringEval(), endPos +end + +--- Scans a JSON object into a Lua object. +-- startPos begins at the start of the object. +-- Returns the object and the next starting position. +-- @param s The string being scanned. +-- @param startPos The starting position of the scan. +-- @return table, int The scanned object as a table and the position of the next character to scan. +function decode_scanObject(s,startPos) + local object = {} + local stringLen = string.len(s) + local key, value + assert(string.sub(s,startPos,startPos)=='{','decode_scanObject called but object does not start at position ' .. startPos .. ' in string:\n' .. s) + startPos = startPos + 1 + repeat + startPos = decode_scanWhitespace(s,startPos) + assert(startPos<=stringLen, 'JSON string ended unexpectedly while scanning object.') + local curChar = string.sub(s,startPos,startPos) + if (curChar=='}') then + return object,startPos+1 + end + if (curChar==',') then + startPos = decode_scanWhitespace(s,startPos+1) + end + assert(startPos<=stringLen, 'JSON string ended unexpectedly scanning object.') + -- Scan the key + key, startPos = json.decode(s,startPos) + assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) + startPos = decode_scanWhitespace(s,startPos) + assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) + assert(string.sub(s,startPos,startPos)==':','JSON object key-value assignment mal-formed at ' .. startPos) + startPos = decode_scanWhitespace(s,startPos+1) + assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) + value, startPos = json.decode(s,startPos) + object[key]=value + until false -- infinite loop while key-value pairs are found +end + +-- START SoniEx2 +-- Initialize some things used by decode_scanString +-- You know, for efficiency +local escapeSequences = { + ["\\t"] = "\t", + ["\\f"] = "\f", + ["\\r"] = "\r", + ["\\n"] = "\n", + ["\\b"] = "\b" +} +setmetatable(escapeSequences, {__index = function(t,k) + -- skip "\" aka strip escape + return string.sub(k,2) +end}) +-- END SoniEx2 + +--- Scans a JSON string from the opening inverted comma or single quote to the +-- end of the string. +-- Returns the string extracted as a Lua string, +-- and the position of the next non-string character +-- (after the closing inverted comma or single quote). +-- @param s The string being scanned. +-- @param startPos The starting position of the scan. +-- @return string, int The extracted string as a Lua string, and the next character to parse. +function decode_scanString(s,startPos) + assert(startPos, 'decode_scanString(..) called without start position') + local startChar = string.sub(s,startPos,startPos) + -- START SoniEx2 + -- PS: I don't think single quotes are valid JSON + assert(startChar == [["]] or startChar == [[']],'decode_scanString called for a non-string') + --assert(startPos, "String decoding failed: missing closing " .. startChar .. " for string at position " .. oldStart) + local t = {} + local i,j = startPos,startPos + while string.find(s, startChar, j+1) ~= j+1 do + local oldj = j + i,j = string.find(s, "\\.", j+1) + local x,y = string.find(s, startChar, oldj+1) + if not i or x < i then + i,j = x,y-1 + end + table.insert(t, string.sub(s, oldj+1, i-1)) + if string.sub(s, i, j) == "\\u" then + local a = string.sub(s,j+1,j+4) + j = j + 4 + local n = tonumber(a, 16) + assert(n, "String decoding failed: bad Unicode escape " .. a .. " at position " .. i .. " : " .. j) + -- math.floor(x/2^y) == lazy right shift + -- a % 2^b == bitwise_and(a, (2^b)-1) + -- 64 = 2^6 + -- 4096 = 2^12 (or 2^6 * 2^6) + local x + if n < 0x80 then + x = string.char(n % 0x80) + elseif n < 0x800 then + -- [110x xxxx] [10xx xxxx] + x = string.char(0xC0 + (math.floor(n/64) % 0x20), 0x80 + (n % 0x40)) + else + -- [1110 xxxx] [10xx xxxx] [10xx xxxx] + x = string.char(0xE0 + (math.floor(n/4096) % 0x10), 0x80 + (math.floor(n/64) % 0x40), 0x80 + (n % 0x40)) + end + table.insert(t, x) + else + table.insert(t, escapeSequences[string.sub(s, i, j)]) + end + end + table.insert(t,string.sub(j, j+1)) + assert(string.find(s, startChar, j+1), "String decoding failed: missing closing " .. startChar .. " at position " .. j .. "(for string at position " .. startPos .. ")") + return table.concat(t,""), j+2 + -- END SoniEx2 +end + +--- Scans a JSON string skipping all whitespace from the current start position. +-- Returns the position of the first non-whitespace character, or nil if the whole end of string is reached. +-- @param s The string being scanned +-- @param startPos The starting position where we should begin removing whitespace. +-- @return int The first position where non-whitespace was encountered, or string.len(s)+1 if the end of string +-- was reached. +function decode_scanWhitespace(s,startPos) + local whitespace=" \n\r\t" + local stringLen = string.len(s) + while ( string.find(whitespace, string.sub(s,startPos,startPos), 1, true) and startPos <= stringLen) do + startPos = startPos + 1 + end + return startPos +end + +--- Encodes a string to be JSON-compatible. +-- This just involves back-quoting inverted commas, back-quotes and newlines, I think ;-) +-- @param s The string to return as a JSON encoded (i.e. backquoted string) +-- @return The string appropriately escaped. + +local escapeList = { + ['"'] = '\\"', + ['\\'] = '\\\\', + ['/'] = '\\/', + ['\b'] = '\\b', + ['\f'] = '\\f', + ['\n'] = '\\n', + ['\r'] = '\\r', + ['\t'] = '\\t' +} + +function json_private.encodeString(s) + local s = tostring(s) + return s:gsub(".", function(c) return escapeList[c] end) -- SoniEx2: 5.0 compat +end + +-- Determines whether the given Lua type is an array or a table / dictionary. +-- We consider any table an array if it has indexes 1..n for its n items, and no +-- other data in the table. +-- I think this method is currently a little 'flaky', but can't think of a good way around it yet... +-- @param t The table to evaluate as an array +-- @return boolean, number True if the table can be represented as an array, false otherwise. If true, +-- the second returned value is the maximum +-- number of indexed elements in the array. +function isArray(t) + -- Next we count all the elements, ensuring that any non-indexed elements are not-encodable + -- (with the possible exception of 'n') + local maxIndex = 0 + for k,v in pairs(t) do + if (type(k)=='number' and math.floor(k)==k and 1<=k) then -- k,v is an indexed pair + if (not isEncodable(v)) then return false end -- All array elements must be encodable + maxIndex = math.max(maxIndex,k) + else + if (k=='n') then + if v ~= table.getn(t) then return false end -- False if n does not hold the number of elements + else -- Else of (k=='n') + if isEncodable(v) then return false end + end -- End of (k~='n') + end -- End of k,v not an indexed pair + end -- End of loop across all pairs + return true, maxIndex +end + +--- Determines whether the given Lua object / table / variable can be JSON encoded. The only +-- types that are JSON encodable are: string, boolean, number, nil, table and json.null. +-- In this implementation, all other types are ignored. +-- @param o The object to examine. +-- @return boolean True if the object should be JSON encoded, false if it should be ignored. +function isEncodable(o) + local t = type(o) + return (t=='string' or t=='boolean' or t=='number' or t=='nil' or t=='table') or (t=='function' and o==null) +end + +return json \ No newline at end of file diff --git a/src/shared/redisproxy.lua b/src/shared/redisproxy.lua new file mode 100644 index 0000000..0fe77c5 --- /dev/null +++ b/src/shared/redisproxy.lua @@ -0,0 +1,72 @@ +local skynet = require "skynet" +local harbor = require "skynet.harbor" + +local table_insert = table.insert + +local redisproxy = {} + +setmetatable(redisproxy, { __index = function(t, k) + local cmd = string.upper(k) + local f = function (self, ...) + local ok, result = pcall(skynet.call, redisd, "lua", cmd, ...) + if not ok then + skynet.error(cmd, ..., "\n", debug.traceback(coroutine.running(), nil)) + return + end + return result + end + t[k] = f + return f +end}) + +function redisproxy:runScripts(name, ...) + local RedisScripts = require("rdsscripts/RedisScripts") + + if not RedisScripts[name].sha1 then + local content = io.readfile(RedisScripts[name].file) + RedisScripts[name].sha1 = self:script("LOAD", content) + end + + -- 不存在脚本(系统问题或者需要刷新脚本) + local existScript = self:script("EXISTS", RedisScripts[name].sha1) + if existScript[1] == 0 then + local content = io.readfile(RedisScripts[name].file) + RedisScripts[name].sha1 = self:script("LOAD", content) + end + + return self:evalsha(RedisScripts[name].sha1, ...) +end + +local meta = {__index = function (tab, name) return function (_, ...) tab[#tab+1]={name, ...} end end} +function redisproxy:pipelining(block) + local ops = setmetatable({{"multi"}}, meta) + block(ops) + if #ops == 1 then return end + ops[#ops+1]={"exec"} + return self:pipeline(ops) +end + +function redisproxy:insertEmail(params) + local pms = { + roleId = params.roleId, + emailId = params.emailId, + createtime = params.createtime or skynet.timex(), + con1 = params.con1 or "", + con2 = params.con2 or "", + con3 = params.con3 or "", + att1 = params.att1 or "", + att2 = params.att2 or "", + att3 = params.att3 or "", + title = params.title or "", + content = params.content or "", + attachments = params.attachments or "", + } + self:runScripts("insertEmail", 12, + pms.roleId, pms.emailId, pms.createtime, + pms.con1, pms.con2, pms.con3, + pms.att1, pms.att2, pms.att3, + pms.title, pms.content, pms.attachments) + return true +end + +return redisproxy \ No newline at end of file diff --git a/src/utils/CommonFunc.lua b/src/utils/CommonFunc.lua new file mode 100644 index 0000000..ded3560 --- /dev/null +++ b/src/utils/CommonFunc.lua @@ -0,0 +1,313 @@ +local skynet = require "skynet" +local cjson = require "shared.json" +local md5 = require("md5") +local httpc = require("http.httpc") + +local servId = tonumber(skynet.getenv("servId")) +function getRandomName() + local nameCsv = csvdb["name_combCsv"] + local part1 = nameCsv[1][math.random(1, #nameCsv[1])].name + local part2 = nameCsv[2][math.random(1, #nameCsv[2])].name + local part3 = nameCsv[3][math.random(1, #nameCsv[3])].name + + return table.concat({part1, part2, part3}) +end + +function getActionCode(role) + if not role.uniqueCount then + role.uniqueCount = 0 + end + local action = {role:getProperty("id"), skynet.timex(), role.uniqueCount} + role.uniqueCount = role.uniqueCount + 1 + return table.concat(action, "_") +end + +-- begin 数据库自增字段 +function getNextRoleId() + local roleId = redisproxy:hget("autoincrement_set", "role") + if roleId - servId * MAX_ROLE_NUM >= MAX_ROLE_NUM - 1 then + return + end + return redisproxy:hincrby("autoincrement_set", "role", 1) +end + +function getNextTradeId() + return redisproxy:hincrby("autoincrement_set", "trade", 1) +end + +-- end 数据库自增字段 + +-- 查找上限 +-- [10, 20, 30, 40] 查找15, 返回指向10的元素 +function lowerBoundSeach(data, searchKey) + -- 先排序 + local lastKey = nil + local keys = table.keys(data) + table.sort(keys) + for _, key in ipairs(keys) do + if key > searchKey then + break + end + lastKey = key + end + + return lastKey and data[lastKey] or nil +end + +-- 将201402021800或者20140202的格式转化成unixtime +function toUnixtime(timeStr) + local strLength = string.len(timeStr) + if strLength ~= 8 and strLength ~= 10 then return end + local year = string.sub(timeStr, 1, 4) + local month = string.sub(timeStr, 5, 6) + local day = string.sub(timeStr, 7, 8) + local hour, minute = 0, 0 + if strLength == 10 then + hour = string.sub(timeStr, 9, 10) + --minute = string.sub(timeStr, 11, 12) + end + return os.time{year=year, month=month, day=day, hour=hour, min=minute, sec=0} +end + +-- 判断时间点是不是当天 +function isToday(curTimestamp) + local curTm = os.date("*t", curTimestamp) + local nowTm = os.date("*t", skynet.timex()) + return curTm.year == nowTm.year and curTm.month == nowTm.month and curTm.day == nowTm.day +end + +function withTimeHead(value, now) + now = now or skynet.timex() + return (2000000000-now) .. "." .. value +end + +function cutTimeHead(value) + return tonumber(value:match("%d+%.(%d+)")) +end + +-- 判断是不是同一个星期 +function isCrossWeek(target, now) + now = now or skynet.timex() + return specMonday(target) ~= specMonday(now - RESET_TIME * 3600) +end + +-- 两个时间相隔多少星期 +function crossWeek(target, now) + now = now or skynet.timex() + return math.floor((specMonday(target)-specMonday(now))/604800) +end + +-- 判断是不是同一个月 +function isCrossMonth(target, now) + now = now or skynet.timex() + local tarTm = os.date("*t", target) + local nowTm = os.date("*t", now - RESET_TIME * 3600) + if tarTm.year == nowTm.year and tarTm.month == nowTm.month then + return false + else + return true + end +end + +function isCrossDay(lastTime, now) + if lastTime == 0 then return true end + now = now or skynet.timex() + local today4h = specTime({hour = RESET_TIME}, now - RESET_TIME * 3600) + return lastTime < today4h and now >= today4h +end + +function crossDay(target, now) + now = now or skynet.timex() + + local tarTime = specTime({hour = RESET_TIME}, target) + --local nowTime = specTime({hour = RESET_TIME}, now) --当前时间有没有过一天而不是当前天四点 + return math.floor((now - tarTime)/ 86400) +end + +function crossWeekFromOpen(now) + now = now or skynet.timex() + local openTime = os.time{ + year = tonum(SERV_OPEN:sub(1,4)), + month = tonum(SERV_OPEN:sub(5,6)), + day = tonum(SERV_OPEN:sub(7,8)), + } + return crossWeek(openTime, now) +end + +function crossDayFromOpen(now) + now = now or skynet.timex() + local openTime = os.time{ + year = tonum(SERV_OPEN:sub(1,4)), + month = tonum(SERV_OPEN:sub(5,6)), + day = tonum(SERV_OPEN:sub(7,8)), + hour = RESET_TIME, + } + return crossDay(openTime, now) +end + +-- 30*86400 = 2592000 +function monthLater(now) + now = now or skynet.timex() + now = now - RESET_TIME * 3600 -- 排除0-4点钟 影响 + return specTime({hour = RESET_TIME}, now) + 2592000 +end +-- 一天以后 +function dayLater(now) + now = now or skynet.timex() + now = now - RESET_TIME * 3600 -- 排除0-4点钟 影响 + return specTime({hour = RESET_TIME}, now) + 86400 +end +-- 到下一个时间点的秒数差和下一个时间点的unixtime +function diffTime(params) + params = params or {} + local currentTime = skynet.timex() + + local curTm = os.date("*t", currentTime) + local nextYear = params.year or curTm.year + local nextMonth = params.month or curTm.month + local nextDay = params.day or curTm.day + 1 + local nextHour = params.hour or 0 + local nextMinute = params.min or 0 + local nextSecond = params.sec or 0 + + local nextUnixTime = os.time({ year = nextYear, month = nextMonth, day = nextDay, hour = nextHour, min = nextMinute, sec = nextSecond}) + return os.difftime(nextUnixTime, currentTime), nextUnixTime +end + +-- 取今天特殊时刻时间戳 +function specTime(pms, now) + now = now or skynet.timex() + local tm = os.date("*t", now) + local year = pms.year or tm.year + local month = pms.month or tm.month + local day = pms.day or tm.day + local hour = pms.hour or 0 + local min = pms.min or 0 + local sec = pms.sec or 0 + return os.time({year = year, month = month, day = day, hour = hour, min = min, sec = sec}) +end + +function specMonday(now) + now = now or skynet.timex() + local tm = os.date("*t", now) + local wday = (tm.wday + 6) % 7 + if wday == 0 then wday = 7 end + + local time = os.time({year = tm.year, month = tm.month, day = tm.day}) + return time - (wday - 1) * 86400 +end + +function isSpecTime(startHour, endHour, specday) + local tm = os.date("*t", skynet.timex()) + if specday then + local day = (tm.wday+6)%7 + if day == 0 then day = 7 end + return specday == day and tm.hour >= startHour and tm.hour < endHour + end + return tm.hour >= startHour and tm.hour < endHour +end + +function getSecond(timeStr) + timeStr = timeStr or "0000" + local hour = tonumber(string.sub(timeStr, 1, 2)) + local min = tonumber(string.sub(timeStr, 3, 4)) + return hour * 3600 + min * 60 +end + +function urlencode(str) + if (str) then + str = string.gsub (str, "\n", "\r\n") + str = string.gsub (str, "([^%w ])", + function (c) return string.format ("%%%02X", string.byte(c)) end) + str = string.gsub (str, " ", "+") + end + return str +end + +function isnan(value) + return value ~= value +end + +-- 推送通知 +local serverid = skynet.getenv "serverid" +local secretKey = "467C2221D3A20FE69D23A33E8940C2C5" + +-- 推送该服务器的所有用户 +-- tag都是交集处理 +function notifyClients(msg, otherTags) + local tags = { serverid } + for _, tag in ipairs(otherTags or {}) do + table.insert(tags, tag) + end + + local content = { + ["appid"] = "1000013239", + ["audience"] = { + [otherTags and "tag_and" or "tag"] = tags, + }, + -- ["audience"] = "all", + ["notification"] = { + ["alert"] = msg, + }, + ["options"] = { + ["ttl"] = 60 * 120 + } + } + + local contentJson = cjson.encode(content) + local header = { + ["content-type"] = "application/x-www-form-urlencoded", + ["X-MJPUSH-SIGNATURE"] = md5.sumhexa(urlencode(contentJson .. "&" .. secretKey)) + } + + local status, body = httpc.request("POST", "push.mjyun.com", "/api/push", {}, header, contentJson) + if tonumber(status) ~= 200 then + skynet.error(status, body) + end +end + +-- { uid, msg, scheduleTime} +function notifyClient(params) + params = params or {} + params.key = "zhaolugame20170831" + + local status, body = httpc.get(skynet.getenv("codeurl"), + "/mipush/notify_user?" .. httpGetFormatData(params), {}, {}) + if tonumber(status) ~= 200 then + skynet.error(status, body) + return + end + + return body +end + +-- { uid, msgId} +function deleteNotify(params) + params = params or {} + params.key = "zhaolugame20170831" + + local status, body = httpc.get(skynet.getenv("codeurl"), + "/mipush/delete_notify?" .. httpGetFormatData(params), {}, {}) + if tonumber(status) ~= 200 then + skynet.error(status, body) + return + end + + return body +end + +--http get数据 +function httpGetFormatData(params) + local function escape(s) + return (string.gsub(s, "([^A-Za-z0-9_])", function(c) + return string.format("%%%02X", string.byte(c)) + end)) + end + + local body = {} + for k, v in pairs(params) do + table.insert(body, string.format("%s=%s", escape(k), escape(v))) + end + + return table.concat(body, "&") +end \ No newline at end of file diff --git a/src/utils/MathUtil.lua b/src/utils/MathUtil.lua new file mode 100644 index 0000000..8a8b3bd --- /dev/null +++ b/src/utils/MathUtil.lua @@ -0,0 +1,42 @@ +-- 初始化 +function math.randomInit(seed) + seed = seed or skynet.timex() + math.randomseed(tonumber(tostring(seed):reverse():sub(1,6))) +end + +-- 随机浮点数 +function math.randomFloat(lower, greater) + return math.min(lower, greater) + math.random() * math.abs(greater - lower); +end + +function math.randomInt(lower, greater) + return math.random(math.min(lower, greater), math.max(lower, greater)) +end + +-- 根据权重值从数据集合里面随机出 +-- @param dataset 数据集合 +-- @param field 权重域 +function math.randWeight(dataset, field) + if not dataset then return nil end + + field = field or "weight" + + -- 计算权值总和 + local weightSum = 0 + for key, value in pairs(dataset) do + weightSum = weightSum + tonumber(value[field]) + end + + local randWeight = math.randomFloat(0, weightSum) + for key, value in pairs(dataset) do + if tonumber(value[field]) > 0 then + if randWeight > tonumber(value[field]) then + randWeight = randWeight - tonumber(value[field]) + else + return key + end + end + end + + return nil +end \ No newline at end of file diff --git a/src/utils/StringUtil.lua b/src/utils/StringUtil.lua new file mode 100644 index 0000000..62d0a43 --- /dev/null +++ b/src/utils/StringUtil.lua @@ -0,0 +1,334 @@ +local ipairs = ipairs +local pairs = pairs +local table_insert = table.insert +local table_concat = table.concat +local table_remove = table.remove +local string_format = string.format +local type = type +local tonumber = tonumber +local next = next + +local strh = require "strh" + +function string.setv(str, k, v, delimiter) + delimiter = delimiter or " " + -- 若存在则替换,若无则append + return strh.modify(str, {[tonumber(k)]=tonumber(v)}, false, delimiter) +end + +function string.msetv(str, vs, delimiter) + delimiter = delimiter or " " + if not next(vs) then return str end + return strh.modify(str, vs, false, delimiter) +end + +function string.incrv(str, k, delta, delimiter) + delimiter = delimiter or " " + return strh.modify(str, {[tonumber(k)]=tonumber(delta)}, true, delimiter) +end + +function string.mincrv(str, ds, delimiter) + delimiter = delimiter or " " + if not next(ds) then return str end + return strh.modify(str, ds, true, delimiter) +end + +function string.delk(str, k, delimiter) + delimiter = delimiter or " " + return strh.modify(str, {[tonumber(k)]=""}, false, delimiter) +end + +function string.mdelk(str, ks, delimiter) + delimiter = delimiter or " " + local mod = {} + for _, k in ipairs(ks) do + mod[k] = "" + end + return strh.modify(str, mod, false, delimiter) +end + +function string.getv(str, k, default, delimiter) + default = default or -1 + delimiter = delimiter or " " + return strh.getv(str, k, default, delimiter) +end + +function string.toArray(str, toNum, delimiter) + delimiter = delimiter or " " + return strh.toarray(str, tobool(toNum), delimiter) +end + +function string.value(str, index, default, notNum, delimiter) + delimiter = delimiter or " " + local bfind, value = strh.value(str, index, delimiter) + if not notNum then + value = tonumber(value) + end + return bfind and value or (default or value) +end + +function string.setv_dk(str, k1, k2, v, delimiter) + delimiter = delimiter or " " + local mod = {[k1]={[k2]=v}} + return strh.moddk(str, mod, false, delimiter) +end + +-- {{k1,k2,v}} +function string.msetv_dk(str, vs, delimiter) + delimiter = delimiter or " " + if not next(vs) then return str end + local mod = {} + for _, t in ipairs(vs) do + local k1 = tonumber(t[1]) + local k2 = tonumber(t[2]) + local v = tonumber(t[3]) + if mod[k1] then + mod[k1][k2] = v + else + mod[k1] = {[k2] = v} + end + end + return strh.moddk(str, mod, false, delimiter) +end + +function string.incrv_dk(str, k1, k2, delta, delimiter) + delimiter = delimiter or " " + local mod = {[tonumber(k1)]={[tonumber(k2)]=tonumber(delta)}} + return strh.moddk(str, mod, true, delimiter) +end + +function string.mincrv_dk(str, ds, delimiter) + delimiter = delimiter or " " + if not next(ds) then return str end + local mod = {} + for _, t in ipairs(ds) do + local k1 = tonumber(t[1]) + local k2 = tonumber(t[2]) + local delta = tonumber(t[3]) + if mod[k1] then + mod[k1][k2] = delta + else + mod[k1] = {[k2] = delta} + end + end + return strh.moddk(str, mod, true, delimiter) +end + +function string.delk_dk(str, k1, k2, delimiter) + delimiter = delimiter or " " + local mod = {[tonumber(k1)]={[tonumber(k2)]=""}} + return strh.moddk(str, mod, false, delimiter) +end + +function string.mdelk_dk(str, ks, delimiter) + delimiter = delimiter or " " + local mod = {} + for _, t in ipairs(ks) do + local k1 = tonumber(t[1]) + local k2 = tonumber(t[2]) + if mod[k1] then + mod[k1][k2] = "" + else + mod[k1] = {[k2] = ""} + end + end + return strh.moddk(str, mod, false, delimiter) +end + +function string.getv_dk(str, k1, k2, default, delimiter) + default = default or -1 + delimiter = delimiter or " " + return strh.getvdk(str, k1, k2, default, delimiter) +end + +function string.toNumMap(str, delimiter) + delimiter = delimiter or " " + return strh.tonummap(str, delimiter) +end + +--[[ +from: 1=2=3 +to: {"1","2","3"} +]] +function string.split2(str, pattern) + pattern = pattern or "[%d.=]+" + local tb = {} + for v in str:gmatch(pattern) do + table_insert(tb, v) + end + return tb +end + +--[[ +from: 1=2 3=4 +to: {["1"]="2",["3"]="4"} +]] +function string.tomap(str) + local tb = {} + for k, v in str:gmatch("([%d.]+)=([%d.]+)") do + tb[k] = v + end + return tb +end + +--[[ +from: 1=2 3=4 5=6.1 +to: {{1,2},{3,4},{5,6.1}} +]] +function string.toAttrMap(str, pattern) + pattern = pattern or "([%d.]+)=([%d.]+)" + local tb = {} + for k, v in str:gmatch(pattern) do + tb[#tb+1] = {tonum(k), tonum(v)} + end + return tb +end + +--[[ +from: "1=2=3 4=5=6" +to: {{"1", "2", "3"}, {"4", "5", "6"}} +]] +function string.toTableArray(str, toNum, delimiter, pattern) + pattern = pattern or "[%d.=]+" + delimiter = delimiter or "=" + local tb = {} + for v in str:gmatch(pattern) do + tb[#tb+1] = v:toArray(toNum, delimiter) + end + return tb +end + +function string.toTableArraySec(str, delimiter) + delimiter = delimiter or " " + local array = {} + local tempArray = string.split(string.trim(str), delimiter) + for _, value in ipairs(tempArray) do + local trimValue = string.trim(value) + if trimValue ~= "" then + value = string.split(trimValue, "=") + table_insert(array, value) + end + end + + return array +end + +--[[ +from: "x1=x2=x3=x4 y1=y2=y3=y4" +to: {[x1]={x2,x3,x4},[y1]={y2,y3,y4}} +]] +function string.toAttArray(str) + local tb = {} + for v1, vt in str:gmatch("([%d.]+)=([%d.=]+)") do + tb[tonum(v1)] = vt:toArray(true) + end + return tb +end + +--[[ +x=x...=x x=x...=x +最后一个x为权值 返回权值之外的值 +]] +function string.randWeight(str, bMulti) + if not str or str == "" then return nil end + + local tab, sum = {}, 0 + for index, vstr in ipairs(str:toArray()) do + local tmp = vstr:toArray(true, "=") + sum = sum + tmp[#tmp] + tab[index] = tmp + end + + local weight = math.randomFloat(0, sum) + + for _, v in ipairs(tab) do + local val = v[#v] + if val < weight then + weight = weight - val + else + if bMulti then + table_remove(v) + return v + else + return v[1] + end + end + end +end + +function string.randLine(str) + local num = str:nums("%d+") + local index = math.random(num) + return tonumber(str:value(index)) +end + +--[[ +结构类似如: +v1;v2;v3; +]] +function string.sismember(str, value, delimiter) + delimiter = delimiter or ";" + for _, v in ipairs(str:toArray(true, delimiter)) do + if v == val then + return true + end + end +end + +function string.sadd(str, value, delimiter) + delimiter = delimiter or ";" + if str:sismember(value) then + return str + end + return table_concat({str, value, delimiter}) +end + +function string.srem(str, value, delimiter) + delimiter = delimiter or ";" + local form = table_concat({"([^", delimiter, "]+)", delimiter}) + local str = str:gsub(form, {[tostring(value)]=""}) + return str +end + +--[[ +from: 1 2 3 4 5 +to: 数字的个数 这里是5 +]] +function string.nums(str, format) + format = format or "%d+" + local count = 0 + for _ in str:gmatch(format) do + count = count + 1 + end + return count +end + +-- getbit setbit 支持负索引 +function string.getbit(str, pos) + local len = #str + if pos > len or len+pos < 0 then return 48 end + if pos < 0 and len+pos >= 0 then pos = len+pos+1 end + return str:byte(pos) +end + +function string.bitcnt(str) + local cnt = 0 + for i=1, #str do + cnt = cnt + str:byte(i) - 48 + end + return cnt +end + +function string.setbit(str, pos, yes) + yes = yes or "1" + local len = #str + if pos < 0 then + if len+pos < 0 then return str end + pos = len+pos+1 + end + if len < pos then + return str .. string.rep("0", pos-len-1) .. yes + else + return str:sub(1, pos-1) .. yes .. str:sub(pos+1, -1) + end +end \ No newline at end of file diff --git a/src/utils/init.lua b/src/utils/init.lua new file mode 100644 index 0000000..ba79bdc --- /dev/null +++ b/src/utils/init.lua @@ -0,0 +1,3 @@ +require("utils.CommonFunc") +require("utils.StringUtil") +require "utils.MathUtil" \ No newline at end of file -- libgit2 0.21.2