Commit c384626d845578e857ce20618d5e7ede204dffbe
1 parent
679a1fc5
好友
Showing
12 changed files
with
849 additions
and
16 deletions
Show diff stats
src/GlobalVar.lua
src/ProtocolCode.lua
... | ... | @@ -32,6 +32,8 @@ actionCodes = { |
32 | 32 | Role_taskRpc = 117, |
33 | 33 | Role_taskActiveRpc = 118, |
34 | 34 | Role_achiveRpc = 119, |
35 | + Role_chatRpc = 120, | |
36 | + Role_chat = 121, | |
35 | 37 | |
36 | 38 | Adv_startAdvRpc = 151, |
37 | 39 | Adv_startHangRpc = 152, |
... | ... | @@ -110,6 +112,20 @@ actionCodes = { |
110 | 112 | Car_runeUpRpc = 402, |
111 | 113 | Car_saleEquipRpc = 403, |
112 | 114 | Car_saleRuneRpc = 404, |
115 | + | |
116 | + | |
117 | + Friend_searchRpc = 450, | |
118 | + Friend_applyRpc = 451, | |
119 | + Friend_applyListRpc = 452, | |
120 | + Friend_handleApplyRpc = 453, | |
121 | + Friend_listRpc = 454, | |
122 | + Friend_deleteRpc = 455, | |
123 | + Friend_blockRpc = 456, | |
124 | + Friend_blockListRpc = 457, | |
125 | + Friend_infoRpc = 458, | |
126 | + Friend_pointRpc = 459, | |
127 | + Friend_updateProperty = 460, | |
128 | + Friend_randomRpc = 461, | |
113 | 129 | } |
114 | 130 | |
115 | 131 | rpcResponseBegin = 10000 | ... | ... |
src/RedisKeys.lua
... | ... | @@ -42,8 +42,13 @@ RANK_DINER_INFO = "rank:diner:info" |
42 | 42 | -- BOSS_SET = "boss:%d:%d" |
43 | 43 | -- BOSS_INFO = "boss:battle" |
44 | 44 | |
45 | --- FRIEND_KEY = "role:%d:friend" --哈希表 | |
46 | --- FRIEND_APPLY_KEY = "role:%d:apply" -- set | |
45 | +FRIEND_KEY = "role:%d:friend" --哈希表 好友 | |
46 | +FRIEND_APPLY_KEY = "role:%d:apply" -- sort set 申请列表 | |
47 | +FRIEND_BLACK_KEY = "role:%d:black" -- set 黑名单 | |
48 | +FRIEND_POINT = "role:%d:point" -- set 当天送给我心的人 | |
49 | +FRIEND_RECOMMEND = "friend:recommend" -- sort set 登录排序 获取推荐好友 | |
50 | + | |
51 | + | |
47 | 52 | -- FRIEND_DINER_LIKE_KEY = "role:%d:diner:like" -- list |
48 | 53 | |
49 | 54 | -- UNION_SET = "global:union" | ... | ... |
... | ... | @@ -0,0 +1,630 @@ |
1 | +local ipairs = ipairs | |
2 | +local table = table | |
3 | +local math = math | |
4 | +local string = string | |
5 | +local redisproxy = redisproxy | |
6 | +local MsgPack = MsgPack | |
7 | +local string_format = string.format | |
8 | +local tonumber = tonumber | |
9 | +local table_insert = table.insert | |
10 | +local table_unpack = table.unpack | |
11 | +local table_find = table.find | |
12 | +local table_nums = table.nums | |
13 | +local math_random = math.randomInt | |
14 | + | |
15 | +local _M = {} | |
16 | + | |
17 | +local function formatArray(tb) | |
18 | + tb = tb or {} | |
19 | + local t = {} | |
20 | + for _, objectId in ipairs(tb) do | |
21 | + t[tonumber(objectId)] = 1 | |
22 | + end | |
23 | + return t | |
24 | +end | |
25 | + | |
26 | +local function checkFriendLimit(roleId) | |
27 | + roleId = tonumber(roleId) | |
28 | + local count = redisproxy:hlen(FRIEND_KEY:format(roleId)) | |
29 | + return count < globalCsv.friendListLimit | |
30 | +end | |
31 | + | |
32 | + | |
33 | +local function addAndCheckApplyLimit(roleId, objId) | |
34 | + roleId = tonumber(roleId) | |
35 | + local dbKey = FRIEND_APPLY_KEY:format(roleId) | |
36 | + local redret = redisproxy:pipelining(function (red) | |
37 | + red:zadd(dbKey, skynet.timex(), objId) | |
38 | + red:zremrangebyrank(dbKey, 0, -(globalCsv.friendApplyLimit + 1)) | |
39 | + end) | |
40 | +end | |
41 | + | |
42 | +local function checkBlackLimit(roleId) | |
43 | + roleId = tonumber(roleId) | |
44 | + local count = redisproxy:scard(FRIEND_BLACK_KEY:format(roleId)) | |
45 | + return count < globalCsv.friendListLimit | |
46 | +end | |
47 | + | |
48 | +local function getRoleInfo(roleId) | |
49 | + local online, info = rpcRole(roleId, "friendSInfo") | |
50 | + return online, info | |
51 | +end | |
52 | + | |
53 | +local function getRoleAllInfo(roleId) | |
54 | + local online, info = rpcRole(roleId, "friendInfo") | |
55 | + return online, info | |
56 | +end | |
57 | + | |
58 | +local function table_merge(tab1, tab2, filter) | |
59 | + tab1 = tab1 or {} | |
60 | + tab2 = tab2 or {} | |
61 | + filter = filter or {} | |
62 | + for k_, v_ in pairs(tab2) do | |
63 | + if not filter[k_] then | |
64 | + tab1[k_] = v_ | |
65 | + end | |
66 | + end | |
67 | + return tab1 | |
68 | +end | |
69 | + | |
70 | +function _M.searchRpc(agent, data) | |
71 | + local role = agent.role | |
72 | + local roleId = role:getProperty("id") | |
73 | + local msg = MsgPack.unpack(data) | |
74 | + local key = msg.key | |
75 | + | |
76 | + if not key then return end | |
77 | + | |
78 | + local objIds = {} | |
79 | + local tempId = tonumber(key) | |
80 | + if tempId then | |
81 | + if redisproxy:exists(string_format("role:%d", tempId)) then | |
82 | + objIds[tempId] = 1 | |
83 | + end | |
84 | + end | |
85 | + local tempId = redisproxy:get(string_format("user:%s", string.upper(key))) | |
86 | + if tempId then | |
87 | + objIds[tonumber(tempId)] = 1 | |
88 | + end | |
89 | + objIds[roleId] = nil --不能有自己 | |
90 | + | |
91 | + local searchList = {} | |
92 | + for objId, _ in pairs(objIds) do | |
93 | + local online, info = getRoleInfo(objId) | |
94 | + local redret = redisproxy:pipelining(function (red) | |
95 | + red:hexists(FRIEND_KEY:format(roleId), objId) | |
96 | + red:zscore(FRIEND_APPLY_KEY:format(objId), roleId) | |
97 | + red:sismember(FRIEND_BLACK_KEY:format(roleId), objId) | |
98 | + end) | |
99 | + local isFriend = redret[1] == 1 and 1 or nil | |
100 | + local hadApply = redret[2] and 1 or nil | |
101 | + local inBlack = redret[3] == 1 and 1 or nil | |
102 | + | |
103 | + table.insert(searchList, table_merge({ | |
104 | + roleId = objId, | |
105 | + online = online, | |
106 | + isFriend = isFriend, | |
107 | + hadApply = hadApply, | |
108 | + inBlack = inBlack, | |
109 | + }, info, { | |
110 | + | |
111 | + })) | |
112 | + end | |
113 | + SendPacket(actionCodes.Friend_searchRpc, MsgPack.pack({searchList = searchList})) | |
114 | + return true | |
115 | +end | |
116 | + | |
117 | + | |
118 | +function _M.applyRpc(agent, data) | |
119 | + local role = agent.role | |
120 | + local roleId = role:getProperty("id") | |
121 | + | |
122 | + local msg = MsgPack.unpack(data) | |
123 | + local objectId = msg.roleId | |
124 | + | |
125 | + if objectId == roleId then | |
126 | + return | |
127 | + end | |
128 | + | |
129 | + local result = nil | |
130 | + local redret = redisproxy:pipelining(function (red) | |
131 | + red:exists(string_format("role:%d", objectId)) | |
132 | + red:hexists(FRIEND_KEY:format(roleId), objectId) | |
133 | + red:zscore(FRIEND_APPLY_KEY:format(objectId), roleId) | |
134 | + red:sismember(FRIEND_BLACK_KEY:format(objectId), roleId) | |
135 | + red:sismember(FRIEND_BLACK_KEY:format(roleId), objectId) | |
136 | + red:hlen(FRIEND_KEY:format(roleId)) | |
137 | + red:hlen(FRIEND_KEY:format(objectId)) | |
138 | + end) | |
139 | + -- 玩家id不存在 | |
140 | + if not result and redret[1] ~= 1 then | |
141 | + result = 1 | |
142 | + end | |
143 | + | |
144 | + -- 已经有这个好友 | |
145 | + if not result and redret[2] == 1 then | |
146 | + result = 2 | |
147 | + end | |
148 | + -- 已经申请 | |
149 | + if not result and redret[3] then | |
150 | + result = 3 | |
151 | + end | |
152 | + -- 对方把你拉黑 | |
153 | + if not result and redret[4] == 1 then | |
154 | + result = 4 | |
155 | + end | |
156 | + | |
157 | + -- 你把对方拉黑了 | |
158 | + if not result and redret[4] == 1 then | |
159 | + result = 5 | |
160 | + end | |
161 | + | |
162 | + -- 自己好友已经满 | |
163 | + if not result and redret[6] >= globalCsv.friendListLimit then | |
164 | + result = 6 | |
165 | + end | |
166 | + -- 对方的好友已满 | |
167 | + if not result and redret[7] >= globalCsv.friendListLimit then | |
168 | + result = 7 | |
169 | + end | |
170 | + | |
171 | + if not result then | |
172 | + addAndCheckApplyLimit(objectId, roleId) | |
173 | + local myInfo = role:friendSInfo() | |
174 | + myInfo.roleId = roleId | |
175 | + myInfo.online = true | |
176 | + myInfo.hadApply = true | |
177 | + rpcRole(objectId, "SendPacket", actionCodes.Friend_updateProperty, MsgPack.pack({newApply = 1, info = {myInfo}})) -- 通知对方 | |
178 | + end | |
179 | + SendPacket(actionCodes.Friend_applyRpc, MsgPack.pack({result = result})) | |
180 | + return true | |
181 | +end | |
182 | + | |
183 | +function _M.applyListRpc(agent, data) | |
184 | + local role = agent.role | |
185 | + local roleId = role:getProperty("id") | |
186 | + | |
187 | + local applyList = {} | |
188 | + | |
189 | + local friends = redisproxy:zrange(FRIEND_APPLY_KEY:format(roleId), 0, -1) | |
190 | + for _ , id in pairs(friends) do | |
191 | + id = tonumber(id) | |
192 | + local online, info = getRoleInfo(id) | |
193 | + table.insert(applyList, table_merge({ | |
194 | + roleId = id, online = online | |
195 | + }, info, { | |
196 | + | |
197 | + })) | |
198 | + end | |
199 | + SendPacket(actionCodes.Friend_applyListRpc, MsgPack.pack({list = applyList})) | |
200 | + return true | |
201 | +end | |
202 | + | |
203 | +local function checkHandleApply(roleId, objectId) | |
204 | + local result = nil | |
205 | + local redret = redisproxy:pipelining(function (red) | |
206 | + red:hlen(FRIEND_KEY:format(roleId)) | |
207 | + red:hlen(FRIEND_KEY:format(objectId)) | |
208 | + red:sismember(FRIEND_BLACK_KEY:format(objectId), roleId) | |
209 | + red:sismember(FRIEND_BLACK_KEY:format(roleId), objectId) | |
210 | + end) | |
211 | + --自己好友满了 | |
212 | + if not result and redret[1] >= globalCsv.friendListLimit then | |
213 | + result = 1 | |
214 | + end | |
215 | + -- 对方好友满了 | |
216 | + if not result and redret[2] >= globalCsv.friendListLimit then | |
217 | + result = 2 | |
218 | + end | |
219 | + -- 对方把你拉黑 | |
220 | + if not result and redret[3] == 1 then | |
221 | + result = 3 | |
222 | + end | |
223 | + -- 你把对方拉黑了 | |
224 | + if not result and redret[4] == 1 then | |
225 | + result = 4 | |
226 | + end | |
227 | + return result | |
228 | +end | |
229 | + | |
230 | +function _M.handleApplyRpc(agent, data) | |
231 | + local role = agent.role | |
232 | + local roleId = role:getProperty("id") | |
233 | + | |
234 | + local msg = MsgPack.unpack(data) | |
235 | + local cmd = msg.cmd | |
236 | + | |
237 | + local newTag = MsgPack.pack({ skynet.timex(), 1}) | |
238 | + local result = nil | |
239 | + if cmd == 1 then --同意 | |
240 | + | |
241 | + local objectId = msg.roleId | |
242 | + | |
243 | + if not redisproxy:zscore(FRIEND_APPLY_KEY:format(roleId), objectId) then | |
244 | + return | |
245 | + end | |
246 | + result = checkHandleApply(roleId, objectId) | |
247 | + | |
248 | + if not result then | |
249 | + redisproxy:pipelining(function (red) | |
250 | + red:ZREM(FRIEND_APPLY_KEY:format(roleId), objectId) | |
251 | + red:ZREM(FRIEND_APPLY_KEY:format(objectId), roleId) | |
252 | + red:hsetnx(FRIEND_KEY:format(roleId), objectId, newTag) | |
253 | + red:hsetnx(FRIEND_KEY:format(objectId), roleId, newTag)--告知对放有新好友 | |
254 | + end) | |
255 | + local myInfo = role:friendSInfo() | |
256 | + myInfo.online = true | |
257 | + myInfo.roleId = roleId | |
258 | + myInfo.isFriend = 1 | |
259 | + local redret = redisproxy:pipelining(function (red) | |
260 | + red:sismember(FRIEND_POINT:format(objectId), roleId) | |
261 | + red:sismember(FRIEND_POINT:format(roleId), objectId) | |
262 | + end) | |
263 | + myInfo.pGet = redret[1] == 1 and 1 or nil | |
264 | + rpcRole(objectId, "SendPacket", actionCodes.Friend_updateProperty, MsgPack.pack({newFriend = 1, info = {myInfo}})) -- 通知对方 | |
265 | + local online , otherInfo = getRoleInfo(objectId) | |
266 | + otherInfo.roleId = objectId | |
267 | + otherInfo.online = online | |
268 | + otherInfo.isFriend = true | |
269 | + otherInfo.pGet = redret[2] == 1 and 1 or nil | |
270 | + SendPacket(actionCodes.Friend_updateProperty, MsgPack.pack({newFriend = 1, info = {otherInfo}})) | |
271 | + end | |
272 | + | |
273 | + elseif cmd == 0 then -- 不同意 | |
274 | + local objectId = msg.roleId | |
275 | + if not redisproxy:zscore(FRIEND_APPLY_KEY:format(roleId), objectId) then | |
276 | + return | |
277 | + end | |
278 | + redisproxy:ZREM(FRIEND_APPLY_KEY:format(roleId), objectId) | |
279 | + elseif cmd == 2 then -- 一键拒绝 | |
280 | + redisproxy:del(FRIEND_APPLY_KEY:format(roleId)) | |
281 | + elseif cmd == 3 then -- 一键同意 | |
282 | + local redret = redisproxy:pipelining(function(red) | |
283 | + red:zrange(FRIEND_APPLY_KEY:format(roleId)) | |
284 | + red:SMEMBERS(FRIEND_POINT:format(roleId)) | |
285 | + end) | |
286 | + local allIds = redret[1] | |
287 | + local fpoint = formatArray(redret[2]) | |
288 | + local needAdd = {} | |
289 | + local needAddMy = {} | |
290 | + local needAddInfo = {} | |
291 | + for _, objId in ipairs(allIds) do | |
292 | + objId = tonumber(objId) | |
293 | + local cr = checkHandleApply(roleId, objId) | |
294 | + if not cr then | |
295 | + table.insert(needAdd, objId) | |
296 | + table.insert(needAddMy, objId) | |
297 | + table.insert(needAddMy, newTag) | |
298 | + local online, otherInfo = getRoleInfo(objId) | |
299 | + otherInfo.online = true | |
300 | + otherInfo.roleId = objId | |
301 | + otherInfo.isFriend = true | |
302 | + otherInfo.pGet = fpoint[objId] and 1 or nil | |
303 | + table.insert(needAddInfo, otherInfo) | |
304 | + end | |
305 | + end | |
306 | + | |
307 | + redisproxy:pipelining(function (red) | |
308 | + red:ZREM(FRIEND_APPLY_KEY:format(roleId), table_unpack(needAdd)) | |
309 | + red:hsetnx(FRIEND_KEY:format(roleId), table_unpack(needAddMy)) | |
310 | + for _, objectId in pairs(needAdd) do | |
311 | + red:ZREM(FRIEND_APPLY_KEY:format(objectId), roleId) | |
312 | + red:hsetnx(FRIEND_KEY:format(objectId), roleId, newTag)--告知对放有新好友 | |
313 | + end | |
314 | + end) | |
315 | + local myInfo = role:friendSInfo() | |
316 | + myInfo.roleId = roleId | |
317 | + myInfo.online = true | |
318 | + myInfo.isFriend = 1 | |
319 | + local giveFP = role.dailyData:getProperty("giveFP") | |
320 | + for _, objectId in pairs(needAdd) do | |
321 | + myInfo.pGet = giveFP[objectId] | |
322 | + rpcRole(objectId, "SendPacket", actionCodes.Friend_updateProperty, MsgPack.pack({newFriend = 1, info = {myInfo}})) -- 通知对方 | |
323 | + end | |
324 | + if next(needAdd) then | |
325 | + SendPacket(actionCodes.Friend_updateProperty, MsgPack.pack({newFriend = 1, info = needAddInfo})) | |
326 | + else | |
327 | + result = 1 | |
328 | + end | |
329 | + | |
330 | + else --不存在 | |
331 | + return | |
332 | + end | |
333 | + | |
334 | + SendPacket(actionCodes.Friend_handleApplyRpc, MsgPack.pack({result = result})) | |
335 | + return true | |
336 | +end | |
337 | + | |
338 | +function _M.listRpc(agent, data) | |
339 | + local role = agent.role | |
340 | + local roleId = role:getProperty("id") | |
341 | + | |
342 | + local friendList = {} | |
343 | + local redret = redisproxy:pipelining(function (red) | |
344 | + red:hgetall(FRIEND_KEY:format(roleId)) | |
345 | + red:SMEMBERS(FRIEND_POINT:format(roleId)) | |
346 | + end) | |
347 | + | |
348 | + local friends = redret[1] | |
349 | + local fpoint = formatArray(redret[2]) | |
350 | + local hadGet = role.dailyData:getProperty("getFP") | |
351 | + local hadGive = role.dailyData:getProperty("giveFP") | |
352 | + | |
353 | + local clearRed = {} | |
354 | + for i = 1, #friends, 2 do | |
355 | + local id = friends[i] | |
356 | + local data = friends[i + 1] | |
357 | + local friendInfo = MsgPack.unpack(data) | |
358 | + id = tonumber(id) | |
359 | + local online, info = getRoleInfo(id) | |
360 | + local roleInfo = { | |
361 | + roleId = id, | |
362 | + online = online, | |
363 | + addTime = friendInfo[1], | |
364 | + isNew = friendInfo[2], | |
365 | + pGive = hadGive[id], | |
366 | + pGet = hadGet[id] and -1 or (fpoint[id] and 1 or nil) | |
367 | + } | |
368 | + roleInfo = table_merge(roleInfo, info, {}) | |
369 | + | |
370 | + friendList[#friendList + 1] = roleInfo | |
371 | + | |
372 | + if friendInfo[2] then | |
373 | + friendInfo[2] = nil --清除新好友标记 | |
374 | + clearRed[#clearRed + 1] = id | |
375 | + clearRed[#clearRed + 1] = MsgPack.pack(friendInfo) | |
376 | + end | |
377 | + end | |
378 | + if next(clearRed) then | |
379 | + redisproxy:hmset(FRIEND_KEY:format(roleId), table_unpack(clearRed)) --清除新好友标记 | |
380 | + end | |
381 | + | |
382 | + SendPacket(actionCodes.Friend_listRpc, MsgPack.pack({list = friendList})) | |
383 | + return true | |
384 | +end | |
385 | + | |
386 | +function _M.deleteRpc(agent, data) | |
387 | + local role = agent.role | |
388 | + local roleId = role:getProperty("id") | |
389 | + local msg = MsgPack.unpack(data) | |
390 | + local objectId = msg.roleId | |
391 | + if not redisproxy:exists(string_format("role:%d", objectId)) then | |
392 | + return | |
393 | + end | |
394 | + -- 是否在好友列表中 | |
395 | + if redisproxy:hexists(FRIEND_KEY:format(roleId), objectId) then | |
396 | + redisproxy:pipelining(function (red) | |
397 | + red:hdel(FRIEND_KEY:format(roleId), objectId) | |
398 | + red:hdel(FRIEND_KEY:format(objectId), roleId) | |
399 | + red:ZREM(FRIEND_APPLY_KEY:format(roleId), objectId) | |
400 | + red:ZREM(FRIEND_APPLY_KEY:format(objectId), roleId) | |
401 | + end) | |
402 | + end | |
403 | + rpcRole(objectId, "SendPacket", actionCodes.Friend_updateProperty, MsgPack.pack({deleteFriend = 1, roleId = roleId})) | |
404 | + | |
405 | + SendPacket(actionCodes.Friend_deleteRpc, MsgPack.pack("")) | |
406 | + return true | |
407 | +end | |
408 | + | |
409 | +function _M.blockRpc(agent, data) | |
410 | + local role = agent.role | |
411 | + local roleId = role:getProperty("id") | |
412 | + local msg = MsgPack.unpack(data) | |
413 | + local cmd = msg.cmd | |
414 | + local objectId = msg.roleId | |
415 | + if not redisproxy:exists(string_format("role:%d", objectId)) then | |
416 | + return | |
417 | + end | |
418 | + | |
419 | + local result = nil | |
420 | + if cmd == 1 then | |
421 | + -- 黑名单满了 | |
422 | + if not result and not checkBlackLimit(roleId) then | |
423 | + result = 1 | |
424 | + end | |
425 | + -- 删除好友 | |
426 | + if not result then | |
427 | + redisproxy:pipelining(function (red) | |
428 | + red:hdel(FRIEND_KEY:format(roleId), objectId) | |
429 | + red:hdel(FRIEND_KEY:format(objectId), roleId) | |
430 | + red:sadd(FRIEND_BLACK_KEY:format(roleId), objectId) | |
431 | + end) | |
432 | + end | |
433 | + elseif cmd == 2 then | |
434 | + redisproxy:SREM(FRIEND_BLACK_KEY:format(roleId), objectId) | |
435 | + else | |
436 | + return | |
437 | + end | |
438 | + | |
439 | + SendPacket(actionCodes.Friend_blockRpc, MsgPack.pack({result = result})) | |
440 | + return true | |
441 | +end | |
442 | + | |
443 | +function _M.blockListRpc(agent, data) | |
444 | + local role = agent.role | |
445 | + local roleId = role:getProperty("id") | |
446 | + | |
447 | + local blockList = {} | |
448 | + | |
449 | + local friends = redisproxy:SMEMBERS(FRIEND_BLACK_KEY:format(roleId)) | |
450 | + for _ , id in pairs(friends) do | |
451 | + id = tonumber(id) | |
452 | + local online, info = getRoleInfo(id) | |
453 | + table.insert(blockList, table_merge({ | |
454 | + roleId = id, online = online | |
455 | + }, info, { | |
456 | + | |
457 | + })) | |
458 | + end | |
459 | + SendPacket(actionCodes.Friend_blockListRpc, MsgPack.pack({list = blockList})) | |
460 | + return true | |
461 | +end | |
462 | + | |
463 | +function _M.infoRpc(agent, data) | |
464 | + local role = agent.role | |
465 | + local roleId = role:getProperty("id") | |
466 | + local msg = MsgPack.unpack(data) | |
467 | + local objectId = msg.roleId | |
468 | + if not redisproxy:exists(string_format("role:%d", objectId)) then | |
469 | + return | |
470 | + end | |
471 | + | |
472 | + local online, info = getRoleAllInfo(objectId) | |
473 | + local redret = redisproxy:pipelining(function (red) | |
474 | + red:hexists(FRIEND_KEY:format(roleId), objectId) | |
475 | + red:zscore(FRIEND_APPLY_KEY:format(objectId), roleId) | |
476 | + red:sismember(FRIEND_BLACK_KEY:format(roleId), objectId) | |
477 | + end) | |
478 | + local isFriend = redret[1] == 1 and 1 or nil | |
479 | + local hadApply = redret[2] == 1 and 1 or nil | |
480 | + local inBlack = redret[3] == 1 and 1 or nil | |
481 | + | |
482 | + local objInfo = table_merge({ | |
483 | + roleId = objectId, | |
484 | + online = online, | |
485 | + isFriend = isFriend, | |
486 | + hadApply = hadApply, | |
487 | + inBlack = inBlack, | |
488 | + }, info, { | |
489 | + | |
490 | + }) | |
491 | + SendPacket(actionCodes.Friend_infoRpc, MsgPack.pack({info = objInfo})) | |
492 | + return true | |
493 | +end | |
494 | + | |
495 | +function _M.pointRpc(agent, data) | |
496 | + local role = agent.role | |
497 | + local roleId = role:getProperty("id") | |
498 | + local msg = MsgPack.unpack(data) | |
499 | + local cmd = msg.cmd | |
500 | + local result = nil | |
501 | + local reward = {} | |
502 | + if cmd == 1 then -- 赠送 | |
503 | + local objId = msg.roleId | |
504 | + local giveP = role.dailyData:getProperty("giveFP") | |
505 | + if not result and giveP[objId] then | |
506 | + result = 1 | |
507 | + end | |
508 | + if not result and not redisproxy:hexists(FRIEND_KEY:format(roleId), objId) then | |
509 | + result = 2 | |
510 | + end | |
511 | + if not result then | |
512 | + redisproxy:sadd(FRIEND_POINT:format(objId), roleId) | |
513 | + giveP[objId] = 1 | |
514 | + role.dailyData:updateProperty({field = "giveFP", value = giveP}) | |
515 | + rpcRole(objId, "SendPacket", actionCodes.Friend_updateProperty, MsgPack.pack({newPoint = 1, roleId = roleId})) | |
516 | + end | |
517 | + elseif cmd == 2 then -- 领取 | |
518 | + local objId = msg.roleId | |
519 | + local getP = role.dailyData:getProperty("getFP") | |
520 | + if not result and table.numbers(getP) >= globalCsv.friendPointLimit then | |
521 | + result = 1 | |
522 | + end | |
523 | + if not result and getP[objId] then | |
524 | + result = 2 | |
525 | + end | |
526 | + if not redisproxy:sismember(FRIEND_POINT:format(roleId), objId) then | |
527 | + result = 3 | |
528 | + end | |
529 | + if not result then | |
530 | + getP[objId] = 1 | |
531 | + reward = role:award({[ItemId.FriendPoint] = 1}) | |
532 | + role.dailyData:updateProperty({field = "getFP", value = getP}) | |
533 | + end | |
534 | + elseif cmd == 3 then -- 一键赠送领取 | |
535 | + -- 赠送 | |
536 | + local giveP = role.dailyData:getProperty("giveFP") | |
537 | + local friends = redisproxy:hgetall(FRIEND_KEY:format(roleId)) | |
538 | + local change = false | |
539 | + redisproxy:pipelining(function(red) | |
540 | + for i = 1, #friends , 2 do | |
541 | + local objId = tonumber(friends[i]) | |
542 | + if not giveP[objId] then | |
543 | + giveP[objId] = 1 | |
544 | + change = true | |
545 | + red:sadd(FRIEND_POINT:format(objId), roleId) | |
546 | + rpcRole(objId, "SendPacket", actionCodes.Friend_updateProperty, MsgPack.pack({newPoint = 1, roleId = roleId})) | |
547 | + end | |
548 | + end | |
549 | + end) | |
550 | + if change then | |
551 | + role.dailyData:updateProperty({field = "giveFP", value = giveP}) | |
552 | + else | |
553 | + result = 1 | |
554 | + end | |
555 | + | |
556 | + --领取 | |
557 | + local getP = role.dailyData:getProperty("getFP") | |
558 | + local curCount = table.numbers(getP) | |
559 | + local getCount = 0 | |
560 | + if curCount < globalCsv.friendPointLimit then | |
561 | + for _, objId in pairs(redisproxy:SMEMBERS(FRIEND_POINT:format(roleId))) do | |
562 | + local objId = tonumber(objId) | |
563 | + if not getP[objId] then | |
564 | + getCount = getCount + 1 | |
565 | + curCount = curCount + 1 | |
566 | + getP[objId] = 1 | |
567 | + if curCount >= globalCsv.friendPointLimit then | |
568 | + break | |
569 | + end | |
570 | + end | |
571 | + end | |
572 | + if getCount > 0 then | |
573 | + reward = role:award({[ItemId.FriendPoint] = getCount}) | |
574 | + role.dailyData:updateProperty({field = "getFP", value = getP}) | |
575 | + else | |
576 | + result = result + 2 | |
577 | + end | |
578 | + end | |
579 | + else | |
580 | + return | |
581 | + end | |
582 | + SendPacket(actionCodes.Friend_pointRpc, MsgPack.pack({result = result, reward = reward})) | |
583 | + return true | |
584 | +end | |
585 | +function _M.randomRpc(agent, data) | |
586 | + local role = agent.role | |
587 | + local roleId = role:getProperty("id") | |
588 | + | |
589 | + local redret = redisproxy:pipelining(function (red) | |
590 | + red:hgetall(FRIEND_KEY:format(roleId)) | |
591 | + red:zrevrange(FRIEND_RECOMMEND, 0, globalCsv.friendRecommendLimit + globalCsv.friendListLimit) | |
592 | + end) | |
593 | + | |
594 | + local friends = redret[1] | |
595 | + local newList = redret[2] | |
596 | + local needRoleIds = {} | |
597 | + for _, newId in pairs(newList) do | |
598 | + local numNewId = tonumber(newId) | |
599 | + if numNewId ~= roleId and not friends[newId] then | |
600 | + table.insert(needRoleIds, numNewId) | |
601 | + end | |
602 | + end | |
603 | + | |
604 | + local randomRoles = {} | |
605 | + for _, objId in ipairs(needRoleIds) do | |
606 | + local online, info = getRoleInfo(objId) | |
607 | + local redret = redisproxy:pipelining(function (red) | |
608 | + red:zscore(FRIEND_APPLY_KEY:format(objId), roleId) | |
609 | + red:sismember(FRIEND_BLACK_KEY:format(roleId), objId) | |
610 | + end) | |
611 | + local hadApply = redret[1] and 1 or nil | |
612 | + local inBlack = redret[2] == 1 and 1 or nil | |
613 | + | |
614 | + table.insert(randomRoles, table_merge({ | |
615 | + roleId = objId, | |
616 | + online = online, | |
617 | + hadApply = hadApply, | |
618 | + inBlack = inBlack, | |
619 | + }, info, { | |
620 | + | |
621 | + })) | |
622 | + if #randomRoles >= globalCsv.friendRecommendLimit then break end | |
623 | + end | |
624 | + | |
625 | + SendPacket(actionCodes.Friend_randomRpc, MsgPack.pack({list = randomRoles})) | |
626 | + return true | |
627 | +end | |
628 | + | |
629 | + | |
630 | +return _M | |
0 | 631 | \ No newline at end of file | ... | ... |
src/actions/RoleAction.lua
... | ... | @@ -122,7 +122,7 @@ function _M.loginRpc( agent, data ) |
122 | 122 | role:onCrossDay(now) |
123 | 123 | role:onResetRank(now) |
124 | 124 | role:setProperty("ltime", now) |
125 | - | |
125 | + redisproxy:zadd(FRIEND_RECOMMEND, now, roleId) | |
126 | 126 | |
127 | 127 | for _, name in ipairs({"dailyData", "dinerData"}) do |
128 | 128 | response[name] = role[name]:data() |
... | ... | @@ -161,7 +161,7 @@ function _M.loginRpc( agent, data ) |
161 | 161 | end |
162 | 162 | end |
163 | 163 | |
164 | - response.wave = 1 + heroWave + runeWave | |
164 | + response.wave = 1 + heroWave + runeWave + 1 | |
165 | 165 | |
166 | 166 | SendPacket(actionCodes.Role_loginRpc, MsgPack.pack(response)) |
167 | 167 | |
... | ... | @@ -213,10 +213,24 @@ function _M.loginRpc( agent, data ) |
213 | 213 | start_agent_timer() |
214 | 214 | -- 注册全服广播 |
215 | 215 | local channel = math.randomInt(1, 1) |
216 | + role._channelIdx = channel | |
216 | 217 | local w_channel = datacenter.get( ("MC_W_CHANNEL" .. channel) ) |
217 | 218 | if w_channel then |
218 | 219 | mcast_util.sub_world(w_channel) |
219 | 220 | end |
221 | + | |
222 | + -- 发下缓存的世界消息 | |
223 | + local worldChatResponse = {worldChats = {}} | |
224 | + local ok, msgs = pcall(skynet.call, 'GLOBALD', "lua", "getWorldMsg", channel) | |
225 | + if not ok then | |
226 | + msgs = {} | |
227 | + end | |
228 | + worldChatResponse.worldChats = msgs | |
229 | + worldChatResponse.chatWave = curWave + 1 | |
230 | + curWave = curWave + 1 | |
231 | + | |
232 | + SendPacket(actionCodes.Role_loginRpc, MsgPack.pack(worldChatResponse)) | |
233 | + | |
220 | 234 | return true |
221 | 235 | end |
222 | 236 | |
... | ... | @@ -352,7 +366,7 @@ function _M.openTimeBoxRpc(agent, data) |
352 | 366 | local itemId = msg.itemId |
353 | 367 | if role:getItemCount(itemId) < 1 then return end |
354 | 368 | local itemData = csvdb["itemCsv"][itemId] |
355 | - local randomData = csvdb["item_randomCsv"][tonumber(itemData.use_effect)] | |
369 | + local randomData = csvdb["item_randomCsv"][itemId] | |
356 | 370 | if not randomData or randomData.openTime <= 0 then return end |
357 | 371 | |
358 | 372 | if boxL[slot] then return end |
... | ... | @@ -364,7 +378,7 @@ function _M.openTimeBoxRpc(agent, data) |
364 | 378 | if not boxL[slot] then return end |
365 | 379 | if boxL[slot].gem or boxL[slot].time <= skynet.timex() then return end |
366 | 380 | local itemData = csvdb["itemCsv"][boxL[slot].id] |
367 | - local randomData = csvdb["item_randomCsv"][tonumber(itemData.use_effect)] | |
381 | + local randomData = csvdb["item_randomCsv"][itemId] | |
368 | 382 | local num = randomData[gemId .. "_gem_num"] |
369 | 383 | if not num then return end |
370 | 384 | |
... | ... | @@ -383,7 +397,7 @@ function _M.openTimeBoxRpc(agent, data) |
383 | 397 | end |
384 | 398 | |
385 | 399 | local itemData = csvdb["itemCsv"][boxL[slot].id] |
386 | - local randomData = csvdb["item_randomCsv"][tonumber(itemData.use_effect)] | |
400 | + local randomData = csvdb["item_randomCsv"][itemId] | |
387 | 401 | reward = randomData.gift:toNumMap() -- 固定奖励 |
388 | 402 | -- 随机奖励 |
389 | 403 | local randomGift = randomData.random_gift |
... | ... | @@ -551,5 +565,73 @@ function _M.achiveRpc(agent, data) |
551 | 565 | return true |
552 | 566 | end |
553 | 567 | |
568 | +function _M.chatRpc(agent, data) | |
569 | + local role = agent.role | |
570 | + local roleId = role:getProperty("id") | |
571 | + local msg = MsgPack.unpack(data) | |
572 | + | |
573 | + local cmd = msg.cmd | |
574 | + local content = msg.content | |
575 | + | |
576 | + if not content then return end | |
577 | + | |
578 | + local now = skynet.timex() | |
579 | + -- 判断禁言 | |
580 | + local result = nil | |
581 | + | |
582 | + local SERV = string_format("CHATED%d", math.random(1, 5)) | |
583 | + local legal, mod = skynet.call(SERV, "lua", "check", content) | |
584 | + if not legal then | |
585 | + content = mod or "" | |
586 | + end | |
587 | + | |
588 | + if content == "" then | |
589 | + result = -1 | |
590 | + end | |
591 | + | |
592 | + local response = { | |
593 | + chatType = cmd, | |
594 | + player = { | |
595 | + roleId = role:getProperty("id"), | |
596 | + name = role:getProperty("name"), | |
597 | + level = role:getProperty("level"), | |
598 | + headId = role:getProperty("headId"), | |
599 | + }, | |
600 | + content = content, | |
601 | + time = now, | |
602 | + } | |
603 | + | |
604 | + local check = { | |
605 | + ["world"] = function () | |
606 | + if role:getProperty("silent") > now then --禁言 | |
607 | + result = 1 | |
608 | + return | |
609 | + end | |
610 | + mcast_util.pub_world(actionCodes.Role_chat, MsgPack.pack(response)) | |
611 | + pcall(skynet.call, 'GLOBALD', "lua", "sendWorldMsg", role._channelIdx, response) | |
612 | + end, | |
613 | + ["p2p"] = function () | |
614 | + -- local result | |
615 | + -- local objectId = msg.objectId | |
616 | + -- if redisproxy:hexists(FRIEND_KEY:format(roleId), objectId) then | |
617 | + -- response.to = objectId | |
618 | + -- result = mcast_util.pub_person(agent.client_fd, objectId, actionCodes.Role_chat, MsgPack.pack(response)) | |
619 | + -- else | |
620 | + -- result = SYS_ERR_CHAT_NOT_FRIEND | |
621 | + -- end | |
622 | + -- if result then | |
623 | + -- role:sendSysErrMsg(result) | |
624 | + -- end | |
625 | + end, | |
626 | + } | |
627 | + if not check[cmd] then return end | |
628 | + | |
629 | + if not result then | |
630 | + check[cmd]() | |
631 | + end | |
632 | + SendPacket(actionCodes.Role_chatRpc, MsgPack.pack({result = result})) | |
633 | + return true | |
634 | +end | |
635 | + | |
554 | 636 | |
555 | 637 | return _M |
556 | 638 | \ No newline at end of file | ... | ... |
src/agent.lua
... | ... | @@ -140,6 +140,11 @@ function rpcRole(roleId, funcName, ...) |
140 | 140 | result[#result+1] = v |
141 | 141 | end |
142 | 142 | return false, redisproxy:hmset(rediskey, table_unpack(result)) |
143 | + elseif funcName == "friendSInfo" or funcName == "friendInfo" then | |
144 | + local sRole = require("models.Role").new({key = rediskey}) | |
145 | + sRole:load() | |
146 | + sRole:loadAll() | |
147 | + return false, sRole[funcName] and sRole[funcName](sRole, ...) | |
143 | 148 | end |
144 | 149 | end |
145 | 150 | end | ... | ... |
src/models/Daily.lua
... | ... | @@ -15,7 +15,9 @@ Daily.schema = { |
15 | 15 | advBC = {"number", 0}, -- 冒险次数购买次数(冒险体力购买次数) |
16 | 16 | advElBC = {"number", 0}, -- 无尽次数购买次数(冒险体力购买次数) |
17 | 17 | advWs = {"table", {}}, -- 冒险队工坊 |
18 | - bonusC = {"table", {}} -- 奖励副本 次数 {[type] = {c = 0, b = 0}} | |
18 | + bonusC = {"table", {}}, -- 奖励副本 次数 {[type] = {c = 0, b = 0}} | |
19 | + giveFP = {"table", {}}, -- 给谁送过心心 | |
20 | + getFP = {"table", {}}, -- 领过谁的心心 | |
19 | 21 | } |
20 | 22 | |
21 | 23 | function Daily:updateProperty(params) |
... | ... | @@ -35,6 +37,7 @@ function Daily:updateProperty(params) |
35 | 37 | end |
36 | 38 | |
37 | 39 | function Daily:refreshDailyData(notify) |
40 | + redisproxy:del(FRIEND_POINT:format(self.owner:getProperty("id"))) | |
38 | 41 | for field, schema in pairs(self.schema) do |
39 | 42 | if field ~= "key" then |
40 | 43 | local typ, def = table.unpack(schema) |
... | ... | @@ -56,6 +59,8 @@ function Daily:data() |
56 | 59 | advElBC = self:getProperty("advElBC"), |
57 | 60 | advWs = self:getProperty("advWs"), |
58 | 61 | bonusC = self:getProperty("bonusC"), |
62 | + giveFP = self:getProperty("giveFP"), | |
63 | + getFP = self:getProperty("getFP"), | |
59 | 64 | } |
60 | 65 | end |
61 | 66 | ... | ... |
src/models/Role.lua
... | ... | @@ -41,6 +41,7 @@ Role.schema = { |
41 | 41 | funcLv = {"table", {}}, --功能等级 |
42 | 42 | loveStatus = {"string", ""}, --统计角色的最高 好感度等级 类型相关 -- type=loveL type=loveL |
43 | 43 | crown = {"number", 0}, -- 看伴娘 |
44 | + silent = {"number", 0}, --禁言解禁时间 | |
44 | 45 | |
45 | 46 | bagLimit = {"table", globalCsv.store_limit_max}, |
46 | 47 | ... | ... |
src/models/RolePlugin.lua
... | ... | @@ -19,6 +19,10 @@ function RolePlugin.bind(Role) |
19 | 19 | function Role:reloadWhenLogin() |
20 | 20 | end |
21 | 21 | |
22 | + function Role:SendPacket(...) | |
23 | + SendPacket(...) | |
24 | + end | |
25 | + | |
22 | 26 | function Role:onCrossDay(now, notify) |
23 | 27 | local ltime = self:getProperty("ltime") |
24 | 28 | |
... | ... | @@ -72,7 +76,8 @@ function RolePlugin.bind(Role) |
72 | 76 | end |
73 | 77 | |
74 | 78 | function Role:onOfflineEvent() |
75 | - | |
79 | + -- 设置最新的登录时间 | |
80 | + self:setProperty("ltime", skynet.timex()) | |
76 | 81 | end |
77 | 82 | |
78 | 83 | local function checkItemCount(self, itemId, count) |
... | ... | @@ -613,8 +618,8 @@ function RolePlugin.bind(Role) |
613 | 618 | return result |
614 | 619 | end |
615 | 620 | |
616 | - function Role:getRealBattleValue(heros) -- 获取队伍战斗力 羁绊加成 | |
617 | - local activeRelation = self:getHeroActiveRelation(heros) | |
621 | + function Role:getRealBattleValue(heros, activeRelation) -- 获取队伍战斗力 羁绊加成 | |
622 | + local activeRelation = activeRelation or self:getHeroActiveRelation(heros) | |
618 | 623 | local battleValue = 0 |
619 | 624 | for _, id in pairs(heros) do |
620 | 625 | local hero = self.heros[id] |
... | ... | @@ -719,7 +724,7 @@ function RolePlugin.bind(Role) |
719 | 724 | heros[slot] = { |
720 | 725 | htype = hero:getProperty("type"), |
721 | 726 | lv = hero:getProperty("level"), |
722 | - breakL = hero:getProperty("breakL"), | |
727 | + wakeL = hero:getProperty("wakeL"), | |
723 | 728 | } |
724 | 729 | end |
725 | 730 | return heros |
... | ... | @@ -811,6 +816,57 @@ function RolePlugin.bind(Role) |
811 | 816 | end |
812 | 817 | self:updateProperty({field = "advL", value = advL}) |
813 | 818 | end |
819 | + -- 好友列表简约信息 | |
820 | + function Role:friendSInfo() | |
821 | + local info = { | |
822 | + name = self:getProperty("name"), | |
823 | + level = self:getProperty("level"), | |
824 | + headId = self:getProperty("headId"), | |
825 | + ltime = self:getProperty("ltime"), | |
826 | + battleV = self:getTeamBattleValue(self:getProperty("hangTeam").heros or {}), -- Todo | |
827 | + } | |
828 | + return info | |
829 | + end | |
830 | + | |
831 | + local slotToPos = { | |
832 | + [1] = 6, | |
833 | + [2] = 2, | |
834 | + [3] = 35, | |
835 | + [4] = 32, | |
836 | + [5] = 29, | |
837 | + } | |
838 | + function Role:getTeamBattleInfo(team) | |
839 | + local heros = {} | |
840 | + -- local activeRelation = self:getHeroActiveRelation(team.heros) | |
841 | + | |
842 | + for slot, id in pairs(team.heros or {}) do | |
843 | + local info = {id = id} | |
844 | + local hero = self.heros[info.id] | |
845 | + if not hero then | |
846 | + print("error heroid " .. info.id) | |
847 | + end | |
848 | + -- local attrs = hero:getTotalAttrs({activeRelation = activeRelation}) | |
849 | + -- for k, v in pairs(AttsEnumEx) do | |
850 | + -- info[v] = (attrs[v] or 0) | |
851 | + -- end | |
852 | + -- info.blockLevel = hero:getSkillLevel(4) | |
853 | + -- info.specialLevel = hero:getSkillLevel(1) | |
854 | + | |
855 | + info.type = hero:getProperty("type") | |
856 | + info.level = hero:getProperty("level") | |
857 | + info.wakeL = hero:getProperty("wakeL") | |
858 | + heros[slot] = info | |
859 | + end | |
860 | + return heros | |
861 | + end | |
862 | + | |
863 | + -- 角色详细信息 | |
864 | + function Role:friendInfo() | |
865 | + local info = self:friendSInfo() | |
866 | + local heros = self:getTeamBattleInfo(self:getProperty("hangTeam")) | |
867 | + info.heros = heros | |
868 | + return info | |
869 | + end | |
814 | 870 | end |
815 | 871 | |
816 | 872 | return RolePlugin |
817 | 873 | \ No newline at end of file | ... | ... |
src/services/globald.lua
... | ... | @@ -17,7 +17,6 @@ local pointDataMark = {} |
17 | 17 | local utils = {} |
18 | 18 | |
19 | 19 | local CHECK_MAIL_STATUS_INTERVAL = 60 |
20 | - | |
21 | 20 | local function mailQuene() |
22 | 21 | local delayEmail = tonum(redisproxy:hget("autoincrement_set", "delay_email")) |
23 | 22 | if delayEmail == 0 then |
... | ... | @@ -91,15 +90,40 @@ local function check_mail_queue() |
91 | 90 | skynet.timeout(CHECK_MAIL_STATUS_INTERVAL, check_mail_queue) |
92 | 91 | end |
93 | 92 | |
93 | + | |
94 | + | |
94 | 95 | local CMD = {} |
96 | + | |
97 | +local cacheWorldMsg = {} | |
98 | +local CACHE_WORLD_MSG_COUNT = 50 | |
99 | +function CMD.sendWorldMsg(channel, msg) | |
100 | + cacheWorldMsg[channel] = cacheWorldMsg[channel] or {} | |
101 | + table.insert(cacheWorldMsg[channel], msg) | |
102 | + for i = #cacheWorldMsg[channel] - CACHE_WORLD_MSG_COUNT, 1, -1 do | |
103 | + table.remove(cacheWorldMsg[channel], i) | |
104 | + end | |
105 | +end | |
106 | + | |
107 | + | |
108 | +function CMD.getWorldMsg(channel) | |
109 | + local msgs = cacheWorldMsg[channel] or {} | |
110 | + return msgs | |
111 | +end | |
112 | + | |
95 | 113 | function CMD.start() |
96 | 114 | check_mail_queue() |
97 | 115 | end |
98 | 116 | |
99 | 117 | local function __init__() |
100 | 118 | skynet.dispatch("lua", function(_, _, command, ...) |
101 | - if CMD[command] then | |
102 | - skynet.ret(skynet.pack(CMD[command](...))) | |
119 | + local f = CMD[command] | |
120 | + if f then | |
121 | + if command == "sendWorldMsg" then | |
122 | + skynet.ignoreret() | |
123 | + f(...) | |
124 | + else | |
125 | + skynet.ret(skynet.pack(f(...))) | |
126 | + end | |
103 | 127 | end |
104 | 128 | end) |
105 | 129 | redisd = harbor.queryname("REDIS") | ... | ... |
src/services/named.lua
... | ... | @@ -39,7 +39,7 @@ if mode == "sub" then |
39 | 39 | end) |
40 | 40 | else |
41 | 41 | skynet.start(function() |
42 | - local ok, forbidNames = pcall(require, "csvdata.name_forbid") | |
42 | + local ok, forbidNames = pcall(require, "csvdata.forbid_name") | |
43 | 43 | if not ok then forbidNames = {} end |
44 | 44 | |
45 | 45 | local words = {} | ... | ... |
src/utils/TableUtil.lua
... | ... | @@ -11,4 +11,12 @@ function table.clear(tab) |
11 | 11 | for k, _ in pairs(tab) do |
12 | 12 | tab[k] = nil |
13 | 13 | end |
14 | +end | |
15 | + | |
16 | +function table.numbers(tab) | |
17 | + local count = 0 | |
18 | + for k, _ in pairs(tab) do | |
19 | + count = count + 1 | |
20 | + end | |
21 | + return count | |
14 | 22 | end |
15 | 23 | \ No newline at end of file | ... | ... |