Client Upload Part 2

RD Core / Commit #5

Commit Client Upload Part 2

Alex Alex committed 19/10/2026 00:06 main Patch
1,244 files +271,215 -0
A RD_Client/modules/corelib/util.lua +381-0 View file
@@ -0,0 +1,381 @@
1+-- @docfuncs @{
2+function print(...)
3+ local msg = ""
4+ local args = { ... }
5+ local appendSpace = #args > 1
6+ for i,v in ipairs(args) do
7+ msg = msg .. tostring(v)
8+ if appendSpace and i < #args then
9+ msg = msg .. ' '
10+ end
11+ end
12+ g_logger.log(LogInfo, msg)
13+end
14+
15+function pinfo(msg)
16+ g_logger.log(LogInfo, msg)
17+end
18+
19+function perror(msg)
20+ g_logger.log(LogError, msg)
21+end
22+
23+function pwarning(msg)
24+ g_logger.log(LogWarning, msg)
25+end
26+
27+function pdebug(msg)
28+ g_logger.log(LogDebug, msg)
29+end
30+
31+function fatal(msg)
32+ g_logger.log(LogFatal, msg)
33+end
34+
35+function exit()
36+ g_app.exit()
37+end
38+
39+function quit()
40+ g_app.exit()
41+end
42+
43+function connect(object, arg1, arg2, arg3)
44+ if not object then
45+ perror("connect: object is nil")
46+ return
47+ end
48+ local signalsAndSlots
49+ local pushFront
50+ if type(arg1) == 'string' then
51+ signalsAndSlots = { [arg1] = arg2 }
52+ pushFront = arg3
53+ else
54+ signalsAndSlots = arg1
55+ pushFront = arg2
56+ end
57+
58+ for signal,slot in pairs(signalsAndSlots) do
59+ if not object[signal] then
60+ local mt = getmetatable(object)
61+ if mt and type(object) == 'userdata' then
62+ object[signal] = function(...)
63+ return signalcall(mt[signal], ...)
64+ end
65+ end
66+ end
67+
68+ if not object[signal] then
69+ object[signal] = slot
70+ elseif type(object[signal]) == 'function' then
71+ object[signal] = { object[signal] }
72+ end
73+
74+ if type(slot) ~= 'function' then
75+ perror(debug.traceback('unable to connect a non function value'))
76+ end
77+
78+ if type(object[signal]) == 'table' then
79+ if pushFront then
80+ table.insert(object[signal], 1, slot)
81+ else
82+ table.insert(object[signal], #object[signal]+1, slot)
83+ end
84+ end
85+ end
86+end
87+
88+function disconnect(object, arg1, arg2)
89+ local signalsAndSlots
90+ if type(arg1) == 'string' then
91+ if arg2 == nil then
92+ object[arg1] = nil
93+ return
94+ end
95+ signalsAndSlots = { [arg1] = arg2 }
96+ elseif type(arg1) == 'table' then
97+ signalsAndSlots = arg1
98+ else
99+ return -- Evitamos error: unable to disconnect
100+ end
101+
102+ for signal,slot in pairs(signalsAndSlots) do
103+ if not object[signal] then
104+ elseif type(object[signal]) == 'function' then
105+ if object[signal] == slot then
106+ object[signal] = nil
107+ end
108+ elseif type(object[signal]) == 'table' then
109+ for k,func in pairs(object[signal]) do
110+ if func == slot then
111+ table.remove(object[signal], k)
112+
113+ if #object[signal] == 1 then
114+ object[signal] = object[signal][1]
115+ end
116+ break
117+ end
118+ end
119+ end
120+ end
121+end
122+
123+function newclass(name)
124+ if not name then
125+ perror(debug.traceback('new class has no name.'))
126+ end
127+
128+ local class = {}
129+ function class.internalCreate()
130+ local instance = {}
131+ for k,v in pairs(class) do
132+ instance[k] = v
133+ end
134+ return instance
135+ end
136+
137+ class.create = class.internalCreate
138+ class.__class = name
139+ class.getClassName = function() return name end
140+ return class
141+end
142+
143+function extends(base, name)
144+ if not name then
145+ perror(debug.traceback('extended class has no name.'))
146+ end
147+
148+ local derived = {}
149+ function derived.internalCreate()
150+ local instance = base.create()
151+ for k,v in pairs(derived) do
152+ instance[k] = v
153+ end
154+ return instance
155+ end
156+
157+ derived.create = derived.internalCreate
158+ derived.__class = name
159+ derived.getClassName = function() return name end
160+ return derived
161+end
162+
163+function runinsandbox(func, ...)
164+ if type(func) == 'string' then
165+ func, err = loadfile(resolvepath(func, 2))
166+ if not func then
167+ error(err)
168+ end
169+ end
170+ local env = { }
171+ local oldenv = getfenv(0)
172+ setmetatable(env, { __index = oldenv } )
173+ setfenv(0, env)
174+ func(...)
175+ setfenv(0, oldenv)
176+ return env
177+end
178+
179+function loadasmodule(name, file)
180+ file = file or resolvepath(name, 2)
181+ if package.loaded[name] then
182+ return package.loaded[name]
183+ end
184+ local env = runinsandbox(file)
185+ package.loaded[name] = env
186+ return env
187+end
188+
189+local function module_loader(modname)
190+ local module = g_modules.getModule(modname)
191+ if not module then
192+ return '\n\tno module \'' .. modname .. '\''
193+ end
194+ return function()
195+ if not module:load() then
196+ error('unable to load required module ' .. modname)
197+ end
198+ return module:getSandbox()
199+ end
200+end
201+table.insert(package.loaders, 1, module_loader)
202+
203+function import(table)
204+ assert(type(table) == 'table')
205+ local env = getfenv(2)
206+ for k,v in pairs(table) do
207+ env[k] = v
208+ end
209+end
210+
211+function export(what, key)
212+ if key ~= nil then
213+ _G[key] = what
214+ else
215+ for k,v in pairs(what) do
216+ _G[k] = v
217+ end
218+ end
219+end
220+
221+function unexport(key)
222+ if type(key) == 'table' then
223+ for _k,v in pairs(key) do
224+ _G[v] = nil
225+ end
226+ else
227+ _G[key] = nil
228+ end
229+end
230+
231+function getfsrcpath(depth)
232+ depth = depth or 2
233+ local info = debug.getinfo(1+depth, "Sn")
234+ local path
235+ if info.short_src then
236+ path = info.short_src:match("(.*)/.*")
237+ end
238+ if not path then
239+ path = '/'
240+ elseif path:sub(0, 1) ~= '/' then
241+ path = '/' .. path
242+ end
243+ return path
244+end
245+
246+function resolvepath(filePath, depth)
247+ if not filePath then return nil end
248+ depth = depth or 1
249+ if filePath then
250+ if filePath:sub(0, 1) ~= '/' then
251+ local basepath = getfsrcpath(depth+1)
252+ if basepath:sub(#basepath) ~= '/' then basepath = basepath .. '/' end
253+ return basepath .. filePath
254+ else
255+ return filePath
256+ end
257+ else
258+ local basepath = getfsrcpath(depth+1)
259+ if basepath:sub(#basepath) ~= '/' then basepath = basepath .. '/' end
260+ return basepath
261+ end
262+end
263+
264+function toboolean(v)
265+ if type(v) == 'string' then
266+ v = v:trim():lower()
267+ if v == '1' or v == 'true' then
268+ return true
269+ end
270+ elseif type(v) == 'number' then
271+ if v == 1 then
272+ return true
273+ end
274+ elseif type(v) == 'boolean' then
275+ return v
276+ end
277+ return false
278+end
279+
280+function fromboolean(boolean)
281+ if boolean then
282+ return 'true'
283+ else
284+ return 'false'
285+ end
286+end
287+
288+function booleantonumber(boolean)
289+ if boolean then
290+ return 1
291+ else
292+ return 0
293+ end
294+end
295+
296+function numbertoboolean(number)
297+ if number ~= 0 then
298+ return true
299+ else
300+ return false
301+ end
302+end
303+
304+function protectedcall(func, ...)
305+ local status, ret = pcall(func, ...)
306+ if status then
307+ return ret
308+ end
309+
310+ perror(ret)
311+ return false
312+end
313+
314+function signalcall(param, ...)
315+ if type(param) == 'function' then
316+ local status, ret = pcall(param, ...)
317+ if status then
318+ return ret
319+ else
320+ perror(ret)
321+ end
322+ elseif type(param) == 'table' then
323+ for k,v in pairs(param) do
324+ local status, ret = pcall(v, ...)
325+ if status then
326+ if ret then return true end
327+ else
328+ perror(ret)
329+ end
330+ end
331+ elseif param ~= nil then
332+ error('attempt to call a non function value')
333+ end
334+ return false
335+end
336+
337+function tr(s, ...)
338+ return string.format(s, ...)
339+end
340+
341+function getOppositeAnchor(anchor)
342+ if anchor == AnchorLeft then
343+ return AnchorRight
344+ elseif anchor == AnchorRight then
345+ return AnchorLeft
346+ elseif anchor == AnchorTop then
347+ return AnchorBottom
348+ elseif anchor == AnchorBottom then
349+ return AnchorTop
350+ elseif anchor == AnchorVerticalCenter then
351+ return AnchorHorizontalCenter
352+ elseif anchor == AnchorHorizontalCenter then
353+ return AnchorVerticalCenter
354+ end
355+ return anchor
356+end
357+
358+function makesingleton(obj)
359+ local singleton = {}
360+ if obj.getClassName then
361+ for key,value in pairs(_G[obj:getClassName()]) do
362+ if type(value) == 'function' then
363+ singleton[key] = function(...) return value(obj, ...) end
364+ end
365+ end
366+ end
367+ return singleton
368+end
369+
370+function comma_value(amount)
371+ local formatted = amount
372+ while true do
373+ formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2')
374+ if (k==0) then
375+ break
376+ end
377+ end
378+ return formatted
379+end
380+
381+-- @}
A RD_Client/modules/crash_reporter/crash_reporter.lua +22-0 View file
@@ -0,0 +1,22 @@
1+local CRASH_FILE = "exception.dmp"
2+
3+function init()
4+ if g_resources.fileExists(CRASH_FILE) then
5+ local crashLog = g_resources.readFileContents(CRASH_FILE)
6+ local clientLog = g_logger.getLastLog()
7+ HTTP.post(Services.crash, {
8+ version = APP_VERSION,
9+ build = g_app.getVersion(),
10+ os = g_app.getOs(),
11+ platform = g_window.getPlatformType(),
12+ crash = base64.encode(crashLog),
13+ log = base64.encode(clientLog)
14+ }, function(data, err)
15+ if err then
16+ return g_logger.error("Error while reporting crash report: " .. err)
17+ end
18+ g_resources.deleteFile(CRASH_FILE)
19+ end)
20+ end
21+end
22+
A RD_Client/modules/crash_reporter/crash_reporter.otmod +8-0 View file
@@ -0,0 +1,8 @@
1+Module
2+ name: crash_reporter
3+ description: Sends crash log to remote server
4+ author: otclient@otclient.ovh
5+ website: otclient.ovh
6+ reloadable: false
7+ scripts: [ crash_reporter ]
8+ @onLoad: init()
A RD_Client/modules/gamelib/const.lua +412-0 View file
@@ -0,0 +1,412 @@
1+-- @docconsts @{
2+
3+FloorHigher = 0
4+FloorLower = 15
5+
6+SkullNone = 0
7+SkullYellow = 1
8+SkullGreen = 2
9+SkullWhite = 3
10+SkullRed = 4
11+SkullBlack = 5
12+SkullOrange = 6
13+
14+ShieldNone = 0
15+ShieldWhiteYellow = 1
16+ShieldWhiteBlue = 2
17+ShieldBlue = 3
18+ShieldYellow = 4
19+ShieldBlueSharedExp = 5
20+ShieldYellowSharedExp = 6
21+ShieldBlueNoSharedExpBlink = 7
22+ShieldYellowNoSharedExpBlink = 8
23+ShieldBlueNoSharedExp = 9
24+ShieldYellowNoSharedExp = 10
25+ShieldGray = 11
26+
27+EmblemNone = 0
28+EmblemGreen = 1
29+EmblemRed = 2
30+EmblemBlue = 3
31+EmblemMember = 4
32+EmblemOther = 5
33+
34+VipIconFirst = 0
35+VipIconLast = 10
36+
37+Directions = {
38+ North = 0,
39+ East = 1,
40+ South = 2,
41+ West = 3,
42+ NorthEast = 4,
43+ SouthEast = 5,
44+ SouthWest = 6,
45+ NorthWest = 7
46+}
47+
48+Skill = {
49+ Fist = 0,
50+ Club = 1,
51+ Sword = 2,
52+ Axe = 3,
53+ Distance = 4,
54+ Shielding = 5,
55+ Fishing = 6,
56+ CriticalChance = 7,
57+ CriticalDamage = 8,
58+ LifeLeechChance = 9,
59+ LifeLeechAmount = 10,
60+ ManaLeechChance = 11,
61+ ManaLeechAmount = 12
62+}
63+
64+North = Directions.North
65+East = Directions.East
66+South = Directions.South
67+West = Directions.West
68+NorthEast = Directions.NorthEast
69+SouthEast = Directions.SouthEast
70+SouthWest = Directions.SouthWest
71+NorthWest = Directions.NorthWest
72+
73+FightOffensive = 1
74+FightBalanced = 2
75+FightDefensive = 3
76+
77+DontChase = 0
78+ChaseOpponent = 1
79+
80+PVPWhiteDove = 0
81+PVPWhiteHand = 1
82+PVPYellowHand = 2
83+PVPRedFist = 3
84+
85+GameProtocolChecksum = 1
86+GameAccountNames = 2
87+GameChallengeOnLogin = 3
88+GamePenalityOnDeath = 4
89+GameNameOnNpcTrade = 5
90+GameDoubleFreeCapacity = 6
91+GameDoubleExperience = 7
92+GameTotalCapacity = 8
93+GameSkillsBase = 9
94+GamePlayerRegenerationTime = 10
95+GameChannelPlayerList = 11
96+GamePlayerMounts = 12
97+GameEnvironmentEffect = 13
98+GameCreatureEmblems = 14
99+GameItemAnimationPhase = 15
100+GameMagicEffectU16 = 16
101+GamePlayerMarket = 17
102+GameSpritesU32 = 18
103+GameTileAddThingWithStackpos = 19
104+GameOfflineTrainingTime = 20
105+GamePurseSlot = 21
106+GameFormatCreatureName = 22
107+GameSpellList = 23
108+GameClientPing = 24
109+GameExtendedClientPing = 25
110+GameDoubleHealth = 28
111+GameDoubleSkills = 29
112+GameChangeMapAwareRange = 30
113+GameMapMovePosition = 31
114+GameAttackSeq = 32
115+GameBlueNpcNameColor = 33
116+GameDiagonalAnimatedText = 34
117+GameLoginPending = 35
118+GameNewSpeedLaw = 36
119+GameForceFirstAutoWalkStep = 37
120+GameMinimapRemove = 38
121+GameDoubleShopSellAmount = 39
122+GameContainerPagination = 40
123+GameThingMarks = 41
124+GameLooktypeU16 = 42
125+GamePlayerStamina = 43
126+GamePlayerAddons = 44
127+GameMessageStatements = 45
128+GameMessageLevel = 46
129+GameNewFluids = 47
130+GamePlayerStateU16 = 48
131+GameNewOutfitProtocol = 49
132+GamePVPMode = 50
133+GameWritableDate = 51
134+GameAdditionalVipInfo = 52
135+GameBaseSkillU16 = 53
136+GameCreatureIcons = 54
137+GameHideNpcNames = 55
138+GameSpritesAlphaChannel = 56
139+GamePremiumExpiration = 57
140+GameBrowseField = 58
141+GameEnhancedAnimations = 59
142+GameOGLInformation = 60
143+GameMessageSizeCheck = 61
144+GamePreviewState = 62
145+GameLoginPacketEncryption = 63
146+GameClientVersion = 64
147+GameContentRevision = 65
148+GameExperienceBonus = 66
149+GameAuthenticator = 67
150+GameUnjustifiedPoints = 68
151+GameSessionKey = 69
152+GameDeathType = 70
153+GameIdleAnimations = 71
154+GameKeepUnawareTiles = 72
155+GameIngameStore = 73
156+GameIngameStoreHighlights = 74
157+GameIngameStoreServiceType = 75
158+GameAdditionalSkills = 76
159+GameDistanceEffectU16 = 77
160+GamePrey = 78
161+GameDoubleMagicLevel = 79
162+
163+GameExtendedOpcode = 80
164+GameMinimapLimitedToSingleFloor = 81
165+GameSendWorldName = 82
166+
167+GameDoubleLevel = 83
168+GameDoubleSoul = 84
169+GameDoublePlayerGoodsMoney = 85
170+GameCreatureWalkthrough = 86 -- add Walkthrough for versions less than 854, unpass = msg->getU8(); in protocolgameparse.cpp
171+GameDoubleTradeMoney = 87
172+GameSequencedPackets = 88
173+GameTibia12Protocol = 89
174+
175+GameNewWalking = 90
176+GameSlowerManualWalking = 91
177+GameItemTooltip = 93
178+
179+GameBot = 95
180+GameBiggerMapCache = 96
181+GameForceLight = 97
182+GameNoDebug = 98
183+GameBotProtection = 99
184+
185+GameCreatureDirectionPassable = 100
186+GameFasterAnimations = 101
187+GameCenteredOutfits = 102
188+GameSendIdentifiers = 103
189+GameWingsAndAura = 104
190+GamePlayerStateU32 = 105
191+GameOutfitShaders = 106
192+GameForceAllowItemHotkeys = 107
193+GameCountU16 = 108
194+GameDrawAuraOnTop = 109
195+
196+GamePacketSizeU32 = 110
197+GamePacketCompression = 111
198+
199+GameItemShader = 112
200+GameCreatureAttachedEffect = 113
201+GameCreatureShader = 114
202+GameEffectsShader = 115
203+GameWingOffset = 116
204+GameAuraFrontAndBack = 115 -- To use that: First layer is bottom/back, second (blend layer) is top/front
205+GameDontCacheFiles = 118 -- doesn't work with encryption and compression
206+GameBigAurasCenter = 119 -- Automatic negative offset for aura bigger than 32x32
207+GameNewUpdateWalk = 120 -- Walk update rate dependant on FPS
208+GameNewCreatureStacking = 121 -- Ignore MAX_THINGS limit while adding to tile
209+GameCreaturesMana = 122 -- get mana from server for creatures other than Player
210+GameDontMergeAnimatedText = 123
211+
212+GameMissionId = 125
213+GameItemCustomAttributes = 126
214+GameAnimatedTextCustomFont = 127
215+GameOutfitAttackData = 128
216+
217+LastGameFeature = 130
218+
219+TextColors = {
220+ red = '#f55e5e', --'#c83200'
221+ orange = '#f36500', --'#c87832'
222+ yellow = '#ffff00', --'#e6c832'
223+ green = '#00EB00', --'#3fbe32'
224+ lightblue = '#5ff7f7',
225+ blue = '#9f9dfd',
226+ --blue1 = '#6e50dc',
227+ --blue2 = '#3264c8',
228+ --blue3 = '#0096c8',
229+ white = '#ffffff', --'#bebebe'
230+}
231+
232+MessageModes = {
233+ None = 0,
234+ Say = 1,
235+ Whisper = 2,
236+ Yell = 3,
237+ PrivateFrom = 4,
238+ PrivateTo = 5,
239+ ChannelManagement = 6,
240+ Channel = 7,
241+ ChannelHighlight = 8,
242+ Spell = 9,
243+ NpcFrom = 10,
244+ NpcTo = 11,
245+ GamemasterBroadcast = 12,
246+ GamemasterChannel = 13,
247+ GamemasterPrivateFrom = 14,
248+ GamemasterPrivateTo = 15,
249+ Login = 16,
250+ Warning = 17,
251+ Game = 18,
252+ Failure = 19,
253+ Look = 20,
254+ DamageDealed = 21,
255+ DamageReceived = 22,
256+ Heal = 23,
257+ Exp = 24,
258+ DamageOthers = 25,
259+ HealOthers = 26,
260+ ExpOthers = 27,
261+ Status = 28,
262+ Loot = 29,
263+ TradeNpc = 30,
264+ Guild = 31,
265+ PartyManagement = 32,
266+ Party = 33,
267+ BarkLow = 34,
268+ BarkLoud = 35,
269+ Report = 36,
270+ HotkeyUse = 37,
271+ TutorialHint = 38,
272+ Thankyou = 39,
273+ Market = 40,
274+ Mana = 41,
275+ BeyondLast = 42,
276+ MonsterYell = 43,
277+ MonsterSay = 44,
278+ Red = 45,
279+ Blue = 46,
280+ RVRChannel = 47,
281+ RVRAnswer = 48,
282+ RVRContinue = 49,
283+ GameHighlight = 50,
284+ NpcFromStartBlock = 51,
285+ Last = 52,
286+ Invalid = 255,
287+}
288+
289+OTSERV_RSA = "1091201329673994292788609605089955415282375029027981291234687579" ..
290+ "3726629149257644633073969600111060390723088861007265581882535850" ..
291+ "3429057592827629436413108566029093628212635953836686562675849720" ..
292+ "6207862794310902180176810615217550567108238764764442605581471797" ..
293+ "07119674283982419152118103759076030616683978566631413"
294+
295+CIPSOFT_RSA = "1321277432058722840622950990822933849527763264961655079678763618" ..
296+ "4334395343554449668205332383339435179772895415509701210392836078" ..
297+ "6959821132214473291575712138800495033169914814069637740318278150" ..
298+ "2907336840325241747827401343576296990629870233111328210165697754" ..
299+ "88792221429527047321331896351555606801473202394175817"
300+
301+-- set to the latest Tibia.pic signature to make otclient compatible with official tibia
302+PIC_SIGNATURE = 0x56C5DDE7
303+
304+OsTypes = {
305+ Linux = 1,
306+ Windows = 2,
307+ Flash = 3,
308+ OtclientLinux = 10,
309+ OtclientWindows = 11,
310+ OtclientMac = 12,
311+}
312+
313+PathFindResults = {
314+ Ok = 0,
315+ Position = 1,
316+ Impossible = 2,
317+ TooFar = 3,
318+ NoWay = 4,
319+}
320+
321+PathFindFlags = {
322+ AllowNullTiles = 1,
323+ AllowCreatures = 2,
324+ AllowNonPathable = 4,
325+ AllowNonWalkable = 8,
326+}
327+
328+VipState = {
329+ Offline = 0,
330+ Online = 1,
331+ Pending = 2,
332+}
333+
334+ExtendedIds = {
335+ Activate = 0,
336+ Locale = 1,
337+ Ping = 2,
338+ Sound = 3,
339+ Game = 4,
340+ Particles = 5,
341+ MapShader = 6,
342+ NeedsUpdate = 7,
343+ Telegraph = 8
344+}
345+
346+PreviewState = {
347+ Default = 0,
348+ Inactive = 1,
349+ Active = 2
350+}
351+
352+Blessings = {
353+ None = 0,
354+ Adventurer = 1,
355+ SpiritualShielding = 2,
356+ EmbraceOfTibia = 4,
357+ FireOfSuns = 8,
358+ WisdomOfSolitude = 16,
359+ SparkOfPhoenix = 32
360+}
361+
362+DeathType = {
363+ Regular = 0,
364+ Blessed = 1
365+}
366+
367+ProductType = {
368+ Other = 0,
369+ NameChange = 1
370+}
371+
372+StoreErrorType = {
373+ NoError = -1,
374+ PurchaseError = 0,
375+ NetworkError = 1,
376+ HistoryError = 2,
377+ TransferError = 3,
378+ Information = 4
379+}
380+
381+StoreState = {
382+ None = 0,
383+ New = 1,
384+ Sale = 2,
385+ Timed = 3
386+}
387+
388+AccountStatus = {
389+ Ok = 0,
390+ Frozen = 1,
391+ Suspended = 2,
392+}
393+
394+SubscriptionStatus = {
395+ Free = 0,
396+ Premium = 1,
397+}
398+
399+ChannelEvent = {
400+ Join = 0,
401+ Leave = 1,
402+ Invite = 2,
403+ Exclude = 3,
404+}
405+
406+-- Helper for item id validation (add at the end of the file)
407+function g_things:isValidItemId(itemId)
408+ -- This assumes g_things:getItemType(itemId) returns nil for invalid ids
409+ return self:getItemType(itemId) ~= nil
410+end
411+
412+-- @}
A RD_Client/modules/gamelib/creature.lua +176-0 View file
@@ -0,0 +1,176 @@
1+-- @docclass Creature
2+
3+-- @docconsts @{
4+
5+SkullNone = 0
6+SkullYellow = 1
7+SkullGreen = 2
8+SkullWhite = 3
9+SkullRed = 4
10+SkullBlack = 5
11+SkullOrange = 6
12+
13+ShieldNone = 0
14+ShieldWhiteYellow = 1
15+ShieldWhiteBlue = 2
16+ShieldBlue = 3
17+ShieldYellow = 4
18+ShieldBlueSharedExp = 5
19+ShieldYellowSharedExp = 6
20+ShieldBlueNoSharedExpBlink = 7
21+ShieldYellowNoSharedExpBlink = 8
22+ShieldBlueNoSharedExp = 9
23+ShieldYellowNoSharedExp = 10
24+
25+EmblemNone = 0
26+EmblemGreen = 1
27+EmblemRed = 2
28+EmblemBlue = 3
29+
30+NpcIconNone = 0
31+NpcIconChat = 1
32+NpcIconTrade = 2
33+NpcIconQuest = 3
34+NpcIconTradeQuest = 4
35+
36+CreatureTypePlayer = 0
37+CreatureTypeMonster = 1
38+CreatureTypeNpc = 2
39+CreatureTypeSummonOwn = 3
40+CreatureTypeSummonOther = 4
41+
42+-- @}
43+
44+function getNextSkullId(skullId)
45+ if skullId == SkullRed or skullId == SkullBlack then
46+ return SkullBlack
47+ end
48+ return SkullRed
49+end
50+
51+function getSkullImagePath(skullId)
52+ local path
53+ if skullId == SkullYellow then
54+ path = '/images/game/skulls/skull_yellow'
55+ elseif skullId == SkullGreen then
56+ path = '/images/game/skulls/skull_green'
57+ elseif skullId == SkullWhite then
58+ path = '/images/game/skulls/skull_white'
59+ elseif skullId == SkullRed then
60+ path = '/images/game/skulls/skull_red'
61+ elseif skullId == SkullBlack then
62+ path = '/images/game/skulls/skull_black'
63+ elseif skullId == SkullOrange then
64+ path = '/images/game/skulls/skull_orange'
65+ end
66+ return path
67+end
68+
69+function getShieldImagePathAndBlink(shieldId)
70+ local path, blink
71+ if shieldId == ShieldWhiteYellow then
72+ path, blink = '/images/game/shields/shield_yellow_white', false
73+ elseif shieldId == ShieldWhiteBlue then
74+ path, blink = '/images/game/shields/shield_blue_white', false
75+ elseif shieldId == ShieldBlue then
76+ path, blink = '/images/game/shields/shield_blue', false
77+ elseif shieldId == ShieldYellow then
78+ path, blink = '/images/game/shields/shield_yellow', false
79+ elseif shieldId == ShieldBlueSharedExp then
80+ path, blink = '/images/game/shields/shield_blue_shared', false
81+ elseif shieldId == ShieldYellowSharedExp then
82+ path, blink = '/images/game/shields/shield_yellow_shared', false
83+ elseif shieldId == ShieldBlueNoSharedExpBlink then
84+ path, blink = '/images/game/shields/shield_blue_not_shared', true
85+ elseif shieldId == ShieldYellowNoSharedExpBlink then
86+ path, blink = '/images/game/shields/shield_yellow_not_shared', true
87+ elseif shieldId == ShieldBlueNoSharedExp then
88+ path, blink = '/images/game/shields/shield_blue_not_shared', false
89+ elseif shieldId == ShieldYellowNoSharedExp then
90+ path, blink = '/images/game/shields/shield_yellow_not_shared', false
91+ elseif shieldId == ShieldGray then
92+ path, blink = '/images/game/shields/shield_gray', false
93+ end
94+ return path, blink
95+end
96+
97+function getEmblemImagePath(emblemId)
98+ local path
99+ if emblemId == EmblemGreen then
100+ path = '/images/game/emblems/emblem_green'
101+ elseif emblemId == EmblemRed then
102+ path = '/images/game/emblems/emblem_red'
103+ elseif emblemId == EmblemBlue then
104+ path = '/images/game/emblems/emblem_blue'
105+ elseif emblemId == EmblemMember then
106+ path = '/images/game/emblems/emblem_member'
107+ elseif emblemId == EmblemOther then
108+ path = '/images/game/emblems/emblem_other'
109+ end
110+ return path
111+end
112+
113+function getTypeImagePath(creatureType)
114+ local path
115+ if creatureType == CreatureTypeSummonOwn then
116+ path = '/images/game/creaturetype/summon_own'
117+ elseif creatureType == CreatureTypeSummonOther then
118+ path = '/images/game/creaturetype/summon_other'
119+ end
120+ return path
121+end
122+
123+function getIconImagePath(iconId)
124+ local path
125+ if iconId == NpcIconChat then
126+ path = '/images/game/npcicons/icon_chat'
127+ elseif iconId == NpcIconTrade then
128+ path = '/images/game/npcicons/icon_trade'
129+ elseif iconId == NpcIconQuest then
130+ path = '/images/game/npcicons/icon_quest'
131+ elseif iconId == NpcIconTradeQuest then
132+ path = '/images/game/npcicons/icon_tradequest'
133+ end
134+ return path
135+end
136+
137+function Creature:onSkullChange(skullId)
138+ local imagePath = getSkullImagePath(skullId)
139+ if imagePath then
140+ self:setSkullTexture(imagePath)
141+ end
142+end
143+
144+function Creature:onShieldChange(shieldId)
145+ local imagePath, blink = getShieldImagePathAndBlink(shieldId)
146+ if imagePath then
147+ self:setShieldTexture(imagePath, blink)
148+ end
149+end
150+
151+function Creature:onEmblemChange(emblemId)
152+ local imagePath = getEmblemImagePath(emblemId)
153+ if imagePath then
154+ self:setEmblemTexture(imagePath)
155+ end
156+end
157+
158+function Creature:onTypeChange(typeId)
159+ local imagePath = getTypeImagePath(typeId)
160+ if imagePath then
161+ self:setTypeTexture(imagePath)
162+ end
163+end
164+
165+function Creature:onIconChange(iconId)
166+ local imagePath = getIconImagePath(iconId)
167+ if imagePath then
168+ self:setIconTexture(imagePath)
169+ end
170+end
171+
172+function Creature:setOutfitShader(shader)
173+ local outfit = self:getOutfit()
174+ outfit.shader = shader
175+ self:setOutfit(outfit)
176+end
A RD_Client/modules/gamelib/game.lua +118-0 View file
@@ -0,0 +1,118 @@
1+function g_game.getRsa()
2+ return G.currentRsa
3+end
4+
5+function g_game.findPlayerItem(itemId, subType)
6+ local localPlayer = g_game.getLocalPlayer()
7+ if localPlayer then
8+ for slot = InventorySlotFirst, InventorySlotLast do
9+ local item = localPlayer:getInventoryItem(slot)
10+ if item and item:getId() == itemId and (subType == -1 or item:getSubType() == subType) then
11+ return item
12+ end
13+ end
14+ end
15+
16+ return g_game.findItemInContainers(itemId, subType)
17+end
18+
19+function g_game.chooseRsa(host)
20+ if G.currentRsa ~= CIPSOFT_RSA and G.currentRsa ~= OTSERV_RSA then return end
21+ if host:ends('.tibia.com') or host:ends('.cipsoft.com') then
22+ g_game.setRsa(CIPSOFT_RSA)
23+
24+ if g_app.getOs() == 'windows' then
25+ g_game.setCustomOs(OsTypes.Windows)
26+ else
27+ g_game.setCustomOs(OsTypes.Linux)
28+ end
29+ else
30+ if G.currentRsa == CIPSOFT_RSA then
31+ g_game.setCustomOs(-1)
32+ end
33+ g_game.setRsa(OTSERV_RSA)
34+ end
35+
36+ -- Hack fix to resolve some 760 login issues
37+ if g_game.getClientVersion() <= 760 then
38+ g_game.setCustomOs(2)
39+ end
40+end
41+
42+function g_game.setRsa(rsa, e)
43+ e = e or '65537'
44+ g_crypt.rsaSetPublicKey(rsa, e)
45+ G.currentRsa = rsa
46+end
47+
48+function g_game.isOfficialTibia()
49+ return G.currentRsa == CIPSOFT_RSA
50+end
51+
52+function g_game.getSupportedClients()
53+ return {
54+ 740, 741, 750, 760, 770, 772,
55+ 780, 781, 782, 790, 792,
56+
57+ 800, 810, 811, 820, 821, 822,
58+ 830, 831, 840, 842, 850, 853,
59+ 854, 855, 857, 860, 861, 862,
60+ 870, 871,
61+
62+ 900, 910, 920, 931, 940, 943,
63+ 944, 951, 952, 953, 954, 960,
64+ 961, 963, 970, 971, 972, 973,
65+ 980, 981, 982, 983, 984, 985,
66+ 986,
67+
68+ 1000, 1001, 1002, 1010, 1011,
69+ 1012, 1013, 1020, 1021, 1022,
70+ 1030, 1031, 1032, 1033, 1034,
71+ 1035, 1036, 1037, 1038, 1039,
72+ 1040, 1041, 1050, 1051, 1052,
73+ 1053, 1054, 1055, 1056, 1057,
74+ 1058, 1059, 1060, 1061, 1062,
75+ 1063, 1064, 1070, 1071, 1072,
76+ 1073, 1074, 1075, 1076, 1080,
77+ 1081, 1082, 1090, 1091, 1092,
78+ 1093, 1094, 1095, 1096, 1097,
79+ 1098, 1099
80+ }
81+end
82+
83+-- The client version and protocol version where
84+-- unsynchronized for some releases, not sure if this
85+-- will be the normal standard.
86+
87+-- Client Version: Publicly given version when
88+-- downloading Cipsoft client.
89+
90+-- Protocol Version: Previously was the same as
91+-- the client version, but was unsychronized in some
92+-- releases, now it needs to be verified and added here
93+-- if it does not match the client version.
94+
95+-- Reason for defining both: The server now requires a
96+-- Client version and Protocol version from the client.
97+
98+-- Important: Use getClientVersion for specific protocol
99+-- features to ensure we are using the proper version.
100+
101+function g_game.getClientProtocolVersion(client)
102+ local clients = {
103+ [980] = 971,
104+ [981] = 973,
105+ [982] = 974,
106+ [983] = 975,
107+ [984] = 976,
108+ [985] = 977,
109+ [986] = 978,
110+ [1001] = 979,
111+ [1002] = 980
112+ }
113+ return clients[client] or client
114+end
115+
116+if not G.currentRsa then
117+ g_game.setRsa(OTSERV_RSA)
118+end
A RD_Client/modules/gamelib/gamelib.otmod +23-0 View file
@@ -0,0 +1,23 @@
1+Module
2+ name: gamelib
3+ description: Contains game related classes
4+ author: OTClient team
5+ website: https://github.com/edubart/otclient
6+
7+ @onLoad: |
8+ dofile 'const'
9+ dofile 'util'
10+ dofile 'protocol'
11+ dofile 'protocollogin'
12+ dofile 'protocolgame'
13+ dofile 'position'
14+ dofile 'game'
15+
16+ dofile 'creature'
17+ dofile 'player'
18+ dofile 'market'
19+ dofile 'textmessages'
20+ dofile 'thing'
21+ dofile 'spells'
22+
23+ dofiles 'ui'
A RD_Client/modules/gamelib/market.lua +202-0 View file
@@ -0,0 +1,202 @@
1+MarketMaxAmount = 2000
2+MarketMaxAmountStackable = 64000
3+MarketMaxPrice = 999999999
4+MarketMaxOffers = 100
5+
6+MarketAction = {
7+ Buy = 0,
8+ Sell = 1
9+}
10+
11+MarketRequest = {
12+ MyOffers = 0xFFFE,
13+ MyHistory = 0xFFFF
14+}
15+
16+MarketOfferState = {
17+ Active = 0,
18+ Cancelled = 1,
19+ Expired = 2,
20+ Accepted = 3,
21+ AcceptedEx = 255
22+}
23+
24+MarketCategory = {
25+ All = 0,
26+ Armors = 1,
27+ Amulets = 2,
28+ Boots = 3,
29+ Containers = 4,
30+ Decoration = 5,
31+ Food = 6,
32+ HelmetsHats = 7,
33+ Legs = 8,
34+ Others = 9,
35+ Potions = 10,
36+ Rings = 11,
37+ Runes = 12,
38+ Shields = 13,
39+ Tools = 14,
40+ Valuables = 15,
41+ Ammunition = 16,
42+ Axes = 17,
43+ Clubs = 18,
44+ DistanceWeapons = 19,
45+ Swords = 20,
46+ WandsRods = 21,
47+ PremiumScrolls = 22,
48+ TibiaCoins = 23,
49+ CreatureProducs = 24,
50+ Unknown1 = 25,
51+ Unknown2 = 26,
52+ StashRetrieve = 27,
53+ Unknown3 = 28,
54+ Unknown4 = 29,
55+ Gold = 30,
56+ Unassigned = 31,
57+ MetaWeapons = 255
58+}
59+
60+MarketCategory.First = MarketCategory.Armors
61+MarketCategory.Last = MarketCategory.Unassigned
62+
63+MarketCategoryWeapons = {
64+ [MarketCategory.Ammunition] = { slots = {255} },
65+ [MarketCategory.Axes] = { slots = {255, InventorySlotOther, InventorySlotLeft} },
66+ [MarketCategory.Clubs] = { slots = {255, InventorySlotOther, InventorySlotLeft} },
67+ [MarketCategory.DistanceWeapons] = { slots = {255, InventorySlotOther, InventorySlotLeft} },
68+ [MarketCategory.Swords] = { slots = {255, InventorySlotOther, InventorySlotLeft} },
69+ [MarketCategory.WandsRods] = { slots = {255, InventorySlotOther, InventorySlotLeft} }
70+}
71+
72+MarketCategoryStrings = {
73+ [0] = 'All',
74+ [1] = 'Armors',
75+ [2] = 'Amulets',
76+ [3] = 'Boots',
77+ [4] = 'Containers',
78+ [5] = 'Decoration',
79+ [6] = 'Food',
80+ [7] = 'Helmets and Hats',
81+ [8] = 'Legs',
82+ [9] = 'Others',
83+ [10] = 'Potions',
84+ [11] = 'Rings',
85+ [12] = 'Runes',
86+ [13] = 'Shields',
87+ [14] = 'Tools',
88+ [15] = 'Valuables',
89+ [16] = 'Ammunition',
90+ [17] = 'Axes',
91+ [18] = 'Clubs',
92+ [19] = 'Distance Weapons',
93+ [20] = 'Swords',
94+ [21] = 'Wands and Rods',
95+ [22] = 'Premium Scrolls',
96+ [23] = 'Tibia Coins',
97+ [24] = 'Creature Products',
98+ [25] = 'Unknown 1',
99+ [26] = 'Unknown 2',
100+ [27] = 'Stash Retrieve',
101+ [28] = 'Unknown 3',
102+ [29] = 'Unknown 4',
103+ [30] = 'Gold',
104+ [31] = 'Unassigned',
105+ [255] = 'Weapons'
106+}
107+
108+function getMarketCategoryName(id)
109+ if table.haskey(MarketCategoryStrings, id) then
110+ return MarketCategoryStrings[id]
111+ end
112+end
113+
114+function getMarketCategoryId(name)
115+ local id = table.find(MarketCategoryStrings, name)
116+ if id then
117+ return id
118+ end
119+end
120+
121+MarketItemDescription = {
122+ Armor = 1,
123+ Attack = 2,
124+ Container = 3,
125+ Defense = 4,
126+ General = 5,
127+ DecayTime = 6,
128+ Combat = 7,
129+ MinLevel = 8,
130+ MinMagicLevel = 9,
131+ Vocation = 10,
132+ Rune = 11,
133+ Ability = 12,
134+ Charges = 13,
135+ WeaponName = 14,
136+ Weight = 15,
137+ Imbuements = 16
138+}
139+
140+MarketItemDescription.First = MarketItemDescription.Armor
141+MarketItemDescription.Last = MarketItemDescription.Weight
142+
143+MarketItemDescriptionStrings = {
144+ [1] = 'Armor',
145+ [2] = 'Attack',
146+ [3] = 'Container',
147+ [4] = 'Defense',
148+ [5] = 'Description',
149+ [6] = 'Use Time',
150+ [7] = 'Combat',
151+ [8] = 'Min Level',
152+ [9] = 'Min Magic Level',
153+ [10] = 'Vocation',
154+ [11] = 'Rune',
155+ [12] = 'Ability',
156+ [13] = 'Charges',
157+ [14] = 'Weapon Type',
158+ [15] = 'Weight',
159+ [16] = 'Imbuements'
160+}
161+
162+function getMarketDescriptionName(id)
163+ if table.haskey(MarketItemDescriptionStrings, id) then
164+ return MarketItemDescriptionStrings[id]
165+ end
166+end
167+
168+function getMarketDescriptionId(name)
169+ local id = table.find(MarketItemDescriptionStrings, name)
170+ if id then
171+ return id
172+ end
173+end
174+
175+MarketSlotFilters = {
176+ [InventorySlotOther] = "Two-Handed",
177+ [InventorySlotLeft] = "One-Handed",
178+ [255] = "Any"
179+}
180+
181+MarketFilters = {
182+ Vocation = 1,
183+ Level = 2,
184+ Depot = 3,
185+ SearchAll = 4
186+}
187+
188+MarketFilters.First = MarketFilters.Vocation
189+MarketFilters.Last = MarketFilters.Depot
190+
191+function getMarketSlotFilterId(name)
192+ local id = table.find(MarketSlotFilters, name)
193+ if id then
194+ return id
195+ end
196+end
197+
198+function getMarketSlotFilterName(id)
199+ if table.haskey(MarketSlotFilters, id) then
200+ return MarketSlotFilters[id]
201+ end
202+end
A RD_Client/modules/gamelib/player.lua +151-0 View file
@@ -0,0 +1,151 @@
1+-- @docclass Player
2+
3+PlayerStates = {
4+ None = 0,
5+ Poison = 1,
6+ Burn = 2,
7+ Energy = 4,
8+ Drunk = 8,
9+ ManaShield = 16,
10+ Paralyze = 32,
11+ Haste = 64,
12+ Swords = 128,
13+ Drowning = 256,
14+ Freezing = 512,
15+ Dazzled = 1024,
16+ Cursed = 2048,
17+ PartyBuff = 4096,
18+ PzBlock = 8192,
19+ Pz = 16384,
20+ Bleeding = 32768,
21+ Hungry = 65536
22+}
23+
24+InventorySlotOther = 0
25+InventorySlotHead = 1
26+InventorySlotNeck = 2
27+InventorySlotBack = 3
28+InventorySlotBody = 4
29+InventorySlotRight = 5
30+InventorySlotLeft = 6
31+InventorySlotLeg = 7
32+InventorySlotFeet = 8
33+InventorySlotFinger = 9
34+InventorySlotAmmo = 10
35+InventorySlotPurse = 11
36+
37+InventorySlotFirst = 1
38+InventorySlotLast = 10
39+
40+function Player:isPartyLeader()
41+ local shield = self:getShield()
42+ return (shield == ShieldWhiteYellow or
43+ shield == ShieldYellow or
44+ shield == ShieldYellowSharedExp or
45+ shield == ShieldYellowNoSharedExpBlink or
46+ shield == ShieldYellowNoSharedExp)
47+end
48+
49+function Player:isPartyMember()
50+ local shield = self:getShield()
51+ return (shield == ShieldWhiteYellow or
52+ shield == ShieldYellow or
53+ shield == ShieldYellowSharedExp or
54+ shield == ShieldYellowNoSharedExpBlink or
55+ shield == ShieldYellowNoSharedExp or
56+ shield == ShieldBlueSharedExp or
57+ shield == ShieldBlueNoSharedExpBlink or
58+ shield == ShieldBlueNoSharedExp or
59+ shield == ShieldBlue)
60+end
61+
62+function Player:isPartySharedExperienceActive()
63+ local shield = self:getShield()
64+ return (shield == ShieldYellowSharedExp or
65+ shield == ShieldYellowNoSharedExpBlink or
66+ shield == ShieldYellowNoSharedExp or
67+ shield == ShieldBlueSharedExp or
68+ shield == ShieldBlueNoSharedExpBlink or
69+ shield == ShieldBlueNoSharedExp)
70+end
71+
72+function Player:hasVip(creatureName)
73+ for id, vip in pairs(g_game.getVips()) do
74+ if (vip[1] == creatureName) then return true end
75+ end
76+ return false
77+end
78+
79+function Player:isMounted()
80+ local outfit = self:getOutfit()
81+ return outfit.mount ~= nil and outfit.mount > 0
82+end
83+
84+function Player:toggleMount()
85+ if g_game.getFeature(GamePlayerMounts) then
86+ g_game.mount(not self:isMounted())
87+ end
88+end
89+
90+function Player:mount()
91+ if g_game.getFeature(GamePlayerMounts) then
92+ g_game.mount(true)
93+ end
94+end
95+
96+function Player:dismount()
97+ if g_game.getFeature(GamePlayerMounts) then
98+ g_game.mount(false)
99+ end
100+end
101+
102+function Player:getItem(itemId, subType)
103+ return g_game.findPlayerItem(itemId, subType or -1)
104+end
105+
106+function Player:getItems(itemId, subType)
107+ local subType = subType or -1
108+
109+ local items = {}
110+ for i=InventorySlotFirst,InventorySlotLast do
111+ local item = self:getInventoryItem(i)
112+ if item and item:getId() == itemId and (subType == -1 or item:getSubType() == subType) then
113+ table.insert(items, item)
114+ end
115+ end
116+
117+ for i, container in pairs(g_game.getContainers()) do
118+ for j, item in pairs(container:getItems()) do
119+ if item:getId() == itemId and (subType == -1 or item:getSubType() == subType) then
120+ item.container = container
121+ table.insert(items, item)
122+ end
123+ end
124+ end
125+ return items
126+end
127+
128+function Player:getItemsCount(itemId)
129+ local items, count = self:getItems(itemId), 0
130+ for i=1,#items do
131+ count = count + items[i]:getCount()
132+ end
133+ return count
134+end
135+
136+function Player:hasState(state, states)
137+ if not states then
138+ states = self:getStates()
139+ end
140+
141+ for i = 1, 32 do
142+ local pow = math.pow(2, i-1)
143+ if pow > states then break end
144+
145+ local states = bit32.band(states, pow)
146+ if states == state then
147+ return true
148+ end
149+ end
150+ return false
151+end
A RD_Client/modules/gamelib/position.lua +37-0 View file
@@ -0,0 +1,37 @@
1+Position = {}
2+
3+function Position.equals(pos1, pos2)
4+ return pos1.x == pos2.x and pos1.y == pos2.y and pos1.z == pos2.z
5+end
6+
7+function Position.greaterThan(pos1, pos2, orEqualTo)
8+ if orEqualTo then
9+ return pos1.x >= pos2.x or pos1.y >= pos2.y or pos1.z >= pos2.z
10+ else
11+ return pos1.x > pos2.x or pos1.y > pos2.y or pos1.z > pos2.z
12+ end
13+end
14+
15+function Position.lessThan(pos1, pos2, orEqualTo)
16+ if orEqualTo then
17+ return pos1.x <= pos2.x or pos1.y <= pos2.y or pos1.z <= pos2.z
18+ else
19+ return pos1.x < pos2.x or pos1.y < pos2.y or pos1.z < pos2.z
20+ end
21+end
22+
23+function Position.isInRange(pos1, pos2, xRange, yRange)
24+ return math.abs(pos1.x-pos2.x) <= xRange and math.abs(pos1.y-pos2.y) <= yRange and pos1.z == pos2.z;
25+end
26+
27+function Position.isValid(pos)
28+ return not (pos.x == 65535 and pos.y == 65535 and pos.z == 255)
29+end
30+
31+function Position.distance(pos1, pos2)
32+ return math.sqrt(math.pow((pos2.x - pos1.x), 2) + math.pow((pos2.y - pos1.y), 2))
33+end
34+
35+function Position.manhattanDistance(pos1, pos2)
36+ return math.abs(pos2.x - pos1.x) + math.abs(pos2.y - pos1.y)
37+end
A RD_Client/modules/gamelib/protocol.lua +206-0 View file
@@ -0,0 +1,206 @@
1+GameServerOpcodes = {
2+ GameServerInitGame = 10,
3+ GameServerGMActions = 11,
4+ GameServerEnterGame = 15,
5+ GameServerLoginError = 20,
6+ GameServerLoginAdvice = 21,
7+ GameServerLoginWait = 22,
8+ GameServerAddCreature = 23,
9+ GameServerPingBack = 29,
10+ GameServerPing = 30,
11+ GameServerChallenge = 31,
12+ GameServerDeath = 40,
13+
14+ -- all in game opcodes must be greater than 50
15+ GameServerFirstGameOpcode = 50,
16+
17+ -- otclient ONLY
18+ GameServerExtendedOpcode = 50,
19+
20+ -- NOTE: add any custom opcodes in this range
21+ -- 51 - 99
22+
23+ -- original tibia ONLY
24+ GameServerFullMap = 100,
25+ GameServerMapTopRow = 101,
26+ GameServerMapRightRow = 102,
27+ GameServerMapBottomRow = 103,
28+ GameServerMapLeftRow = 104,
29+ GameServerUpdateTile = 105,
30+ GameServerCreateOnMap = 106,
31+ GameServerChangeOnMap = 107,
32+ GameServerDeleteOnMap = 108,
33+ GameServerMoveCreature = 109,
34+ GameServerOpenContainer = 110,
35+ GameServerCloseContainer = 111,
36+ GameServerCreateContainer = 112,
37+ GameServerChangeInContainer = 113,
38+ GameServerDeleteInContainer = 114,
39+ GameServerSetInventory = 120,
40+ GameServerDeleteInventory = 121,
41+ GameServerOpenNpcTrade = 122,
42+ GameServerPlayerGoods = 123,
43+ GameServerCloseNpcTrade = 124,
44+ GameServerOwnTrade = 125,
45+ GameServerCounterTrade = 126,
46+ GameServerCloseTrade = 127,
47+ GameServerAmbient = 130,
48+ GameServerGraphicalEffect = 131,
49+ GameServerTextEffect = 132,
50+ GameServerMissleEffect = 133,
51+ GameServerMarkCreature = 134,
52+ GameServerTrappers = 135,
53+ GameServerCreatureHealth = 140,
54+ GameServerCreatureLight = 141,
55+ GameServerCreatureOutfit = 142,
56+ GameServerCreatureSpeed = 143,
57+ GameServerCreatureSkull = 144,
58+ GameServerCreatureParty = 145,
59+ GameServerCreatureUnpass = 146,
60+ GameServerEditText = 150,
61+ GameServerEditList = 151,
62+ GameServerPlayerDataBasic = 159, -- 910
63+ GameServerPlayerData = 160,
64+ GameServerPlayerSkills = 161,
65+ GameServerPlayerState = 162,
66+ GameServerClearTarget = 163,
67+ GameServerSpellDelay = 164, -- 870
68+ GameServerSpellGroupDelay = 165, -- 870
69+ GameServerMultiUseDelay = 166, -- 870
70+ GameServerTalk = 170,
71+ GameServerChannels = 171,
72+ GameServerOpenChannel = 172,
73+ GameServerOpenPrivateChannel = 173,
74+ GameServerRuleViolationChannel = 174,
75+ GameServerRuleViolationRemove = 175,
76+ GameServerRuleViolationCancel = 176,
77+ GameServerRuleViolationLock = 177,
78+ GameServerOpenOwnChannel = 178,
79+ GameServerCloseChannel = 179,
80+ GameServerTextMessage = 180,
81+ GameServerCancelWalk = 181,
82+ GameServerWalkWait = 182,
83+ GameServerFloorChangeUp = 190,
84+ GameServerFloorChangeDown = 191,
85+ GameServerChooseOutfit = 200,
86+ GameServerVipAdd = 210,
87+ GameServerVipLogin = 211,
88+ GameServerVipLogout = 212,
89+ GameServerTutorialHint = 220,
90+ GameServerAutomapFlag = 221,
91+ GameServerCoinBalance = 223, -- 1080
92+ GameServerStoreError = 224, -- 1080
93+ GameServerRequestPurchaseData = 225, -- 1080
94+ GameServerQuestLog = 240,
95+ GameServerQuestLine = 241,
96+ GameServerCoinBalanceUpdating = 242, -- 1080
97+ GameServerChannelEvent = 243, -- 910
98+ GameServerItemInfo = 244, -- 910
99+ GameServerPlayerInventory = 245, -- 910
100+ GameServerMarketEnter = 246, -- 944
101+ GameServerMarketLeave = 247, -- 944
102+ GameServerMarketDetail = 248, -- 944
103+ GameServerMarketBrowse = 249, -- 944
104+ GameServerShowModalDialog = 250, -- 960
105+ GameServerStore = 251, -- 1080
106+ GameServerStoreOffers = 252, -- 1080
107+ GameServerStoreTransactionHistory = 253, -- 1080
108+ GameServerStoreCompletePurchase = 254 -- 1080
109+}
110+
111+ClientOpcodes = {
112+ ClientEnterAccount = 1,
113+ ClientEnterGame = 10,
114+ ClientLeaveGame = 20,
115+ ClientPing = 29,
116+ ClientPingBack = 30,
117+
118+ -- all in game opcodes must be equal or greater than 50
119+ ClientFirstGameOpcode = 50,
120+
121+ -- otclient ONLY
122+ ClientExtendedOpcode = 50,
123+
124+ -- NOTE: add any custom opcodes in this range
125+ -- 51 - 99
126+
127+ -- original tibia ONLY
128+ ClientAutoWalk = 100,
129+ ClientWalkNorth = 101,
130+ ClientWalkEast = 102,
131+ ClientWalkSouth = 103,
132+ ClientWalkWest = 104,
133+ ClientStop = 105,
134+ ClientWalkNorthEast = 106,
135+ ClientWalkSouthEast = 107,
136+ ClientWalkSouthWest = 108,
137+ ClientWalkNorthWest = 109,
138+ ClientTurnNorth = 111,
139+ ClientTurnEast = 112,
140+ ClientTurnSouth = 113,
141+ ClientTurnWest = 114,
142+ ClientEquipItem = 119, -- 910
143+ ClientMove = 120,
144+ ClientInspectNpcTrade = 121,
145+ ClientBuyItem = 122,
146+ ClientSellItem = 123,
147+ ClientCloseNpcTrade = 124,
148+ ClientRequestTrade = 125,
149+ ClientInspectTrade = 126,
150+ ClientAcceptTrade = 127,
151+ ClientRejectTrade = 128,
152+ ClientUseItem = 130,
153+ ClientUseItemWith = 131,
154+ ClientUseOnCreature = 132,
155+ ClientRotateItem = 133,
156+ ClientCloseContainer = 135,
157+ ClientUpContainer = 136,
158+ ClientEditText = 137,
159+ ClientEditList = 138,
160+ ClientLook = 140,
161+ ClientTalk = 150,
162+ ClientRequestChannels = 151,
163+ ClientJoinChannel = 152,
164+ ClientLeaveChannel = 153,
165+ ClientOpenPrivateChannel = 154,
166+ ClientCloseNpcChannel = 158,
167+ ClientChangeFightModes = 160,
168+ ClientAttack = 161,
169+ ClientFollow = 162,
170+ ClientInviteToParty = 163,
171+ ClientJoinParty = 164,
172+ ClientRevokeInvitation = 165,
173+ ClientPassLeadership = 166,
174+ ClientLeaveParty = 167,
175+ ClientShareExperience = 168,
176+ ClientDisbandParty = 169,
177+ ClientOpenOwnChannel = 170,
178+ ClientInviteToOwnChannel = 171,
179+ ClientExcludeFromOwnChannel = 172,
180+ ClientCancelAttackAndFollow = 190,
181+ ClientRefreshContainer = 202,
182+ ClientRequestOutfit = 210,
183+ ClientChangeOutfit = 211,
184+ ClientMount = 212, -- 870
185+ ClientAddVip = 220,
186+ ClientRemoveVip = 221,
187+ ClientBugReport = 230,
188+ ClientRuleViolation = 231,
189+ ClientDebugReport = 232,
190+ ClientTransferCoins = 239, -- 1080
191+ ClientRequestQuestLog = 240,
192+ ClientRequestQuestLine = 241,
193+ ClientNewRuleViolation = 242, -- 910
194+ ClientRequestItemInfo = 243, -- 910
195+ ClientMarketLeave = 244, -- 944
196+ ClientMarketBrowse = 245, -- 944
197+ ClientMarketCreate = 246, -- 944
198+ ClientMarketCancel = 247, -- 944
199+ ClientMarketAccept = 248, -- 944
200+ ClientAnswerModalDialog = 249, -- 960
201+ ClientOpenStore = 250, -- 1080
202+ ClientRequestStoreOffers = 251, -- 1080
203+ ClientBuyStoreOffer = 252, -- 1080
204+ ClientOpenTransactionHistory = 253, -- 1080
205+ ClientRequestTransactionHistory = 254 -- 1080
206+}
A RD_Client/modules/gamelib/protocolgame.lua +144-0 View file
@@ -0,0 +1,144 @@
1+local opcodeCallbacks = {}
2+local extendedCallbacks = {}
3+local extendedJSONCallbacks = {}
4+local extendedJSONData = {}
5+local maxPacketSize = 65000
6+
7+-- Utility function to check if an item id is valid
8+local function isValidItemId(itemId)
9+ -- g_things is usually the global accessor for items
10+ return g_things and g_things:isValidItemId(itemId)
11+end
12+
13+function ProtocolGame:onOpcode(opcode, msg)
14+ for i, callback in pairs(opcodeCallbacks) do
15+ if i == opcode then
16+ callback(self, msg)
17+ return true
18+ end
19+ end
20+ return false
21+end
22+
23+function ProtocolGame:onExtendedOpcode(opcode, buffer)
24+ local callback = extendedCallbacks[opcode]
25+ if callback then
26+ callback(self, opcode, buffer)
27+ end
28+
29+ callback = extendedJSONCallbacks[opcode]
30+ if callback then
31+ local status = buffer:sub(1,1) -- O - just one message, S - start, P - part, E - end
32+ local data = buffer:sub(2)
33+ if status ~= "E" and status ~= "P" then
34+ extendedJSONData[opcode] = ""
35+ end
36+ if status ~= "S" and status ~= "P" and status ~= "E" then
37+ extendedJSONData[opcode] = buffer
38+ else
39+ extendedJSONData[opcode] = extendedJSONData[opcode] .. data
40+ end
41+ if status ~= "S" and status ~= "P" then
42+ local json_status, json_data = pcall(function() return json.decode(extendedJSONData[opcode]) end)
43+ extendedJSONData[opcode] = nil
44+ if not json_status then
45+ error("Invalid data in extended JSON opcode (" .. json_status .. "): " .. json_data)
46+ return
47+ end
48+ callback(self, opcode, json_data)
49+ end
50+ end
51+end
52+
53+function ProtocolGame.registerOpcode(opcode, callback)
54+ if opcodeCallbacks[opcode] then
55+ error('opcode ' .. opcode .. ' already registered will be overriden')
56+ end
57+
58+ opcodeCallbacks[opcode] = callback
59+end
60+
61+function ProtocolGame.unregisterOpcode(opcode)
62+ opcodeCallbacks[opcode] = nil
63+end
64+
65+function ProtocolGame.registerExtendedOpcode(opcode, callback)
66+ if not callback or type(callback) ~= 'function' then
67+ error('Invalid callback.')
68+ end
69+
70+ if opcode < 0 or opcode > 255 then
71+ error('Invalid opcode. Range: 0-255')
72+ end
73+
74+ if extendedCallbacks[opcode] then
75+ error('Opcode is already taken.')
76+ end
77+
78+ extendedCallbacks[opcode] = callback
79+end
80+
81+function ProtocolGame.unregisterExtendedOpcode(opcode)
82+ if opcode < 0 or opcode > 255 then
83+ error('Invalid opcode. Range: 0-255')
84+ end
85+
86+ if not extendedCallbacks[opcode] then
87+ error('Opcode is not registered.')
88+ end
89+
90+ extendedCallbacks[opcode] = nil
91+end
92+
93+function ProtocolGame.registerExtendedJSONOpcode(opcode, callback)
94+ if not callback or type(callback) ~= 'function' then
95+ error('Invalid callback.')
96+ end
97+
98+ if opcode < 0 or opcode > 255 then
99+ error('Invalid opcode. Range: 0-255')
100+ end
101+
102+ if extendedJSONCallbacks[opcode] then
103+ error('Opcode is already taken.')
104+ end
105+
106+ extendedJSONCallbacks[opcode] = callback
107+end
108+
109+function ProtocolGame.unregisterExtendedJSONOpcode(opcode)
110+ if opcode < 0 or opcode > 255 then
111+ error('Invalid opcode. Range: 0-255')
112+ end
113+
114+ if not extendedJSONCallbacks[opcode] then
115+ error('Opcode is not registered.')
116+ end
117+
118+ extendedJSONCallbacks[opcode] = nil
119+end
120+
121+function ProtocolGame:sendExtendedJSONOpcode(opcode, data)
122+ if opcode < 0 or opcode > 255 then
123+ error('Invalid opcode. Range: 0-255')
124+ end
125+
126+ if type(data) ~= "table" then
127+ error('Invalid data type, should be table')
128+ end
129+
130+ local buffer = json.encode(data)
131+ local s = {}
132+ for i=1, #buffer, maxPacketSize do
133+ s[#s+1] = buffer:sub(i,i+maxPacketSize - 1)
134+ end
135+ if #s == 1 then
136+ self:sendExtendedOpcode(opcode, s[1])
137+ return
138+ end
139+ self:sendExtendedOpcode(opcode, "S" .. s[1])
140+ for i=2,#s - 1 do
141+ self:sendExtendedOpcode(opcode, "P" .. s[i])
142+ end
143+ self:sendExtendedOpcode(opcode, "E" .. s[#s])
144+end
A RD_Client/modules/gamelib/protocollogin.lua +321-0 View file
@@ -0,0 +1,321 @@
1+-- @docclass
2+ProtocolLogin = extends(Protocol, "ProtocolLogin")
3+
4+LoginServerError = 10
5+LoginServerTokenSuccess = 12
6+LoginServerTokenError = 13
7+LoginServerUpdate = 17
8+LoginServerMotd = 20
9+LoginServerUpdateNeeded = 30
10+LoginServerSessionKey = 40
11+LoginServerCharacterList = 100
12+LoginServerExtendedCharacterList = 101
13+LoginServerProxyList = 110
14+
15+-- Since 10.76
16+LoginServerRetry = 10
17+LoginServerErrorNew = 11
18+
19+function ProtocolLogin:login(host, port, accountName, accountPassword, authenticatorToken, stayLogged)
20+ if string.len(host) == 0 or port == nil or port == 0 then
21+ signalcall(self.onLoginError, self, tr("You must enter a valid server address and port."))
22+ return
23+ end
24+
25+ self.accountName = accountName
26+ self.accountPassword = accountPassword
27+ self.authenticatorToken = authenticatorToken
28+ self.stayLogged = stayLogged
29+ self.connectCallback = self.sendLoginPacket
30+
31+ self:connect(host, port)
32+end
33+
34+function ProtocolLogin:cancelLogin()
35+ self:disconnect()
36+end
37+
38+function ProtocolLogin:sendLoginPacket()
39+ local msg = OutputMessage.create()
40+ msg:addU8(ClientOpcodes.ClientEnterAccount)
41+ msg:addU16(g_game.getOs())
42+ if g_game.getCustomProtocolVersion() > 0 then
43+ msg:addU16(g_game.getCustomProtocolVersion())
44+ else
45+ msg:addU16(g_game.getProtocolVersion())
46+ end
47+
48+ if g_game.getFeature(GameClientVersion) then
49+ msg:addU32(g_game.getClientVersion())
50+ end
51+
52+ if g_game.getFeature(GameContentRevision) then
53+ msg:addU16(g_things.getContentRevision())
54+ msg:addU16(0)
55+ else
56+ msg:addU32(g_things.getDatSignature())
57+ end
58+ msg:addU32(g_sprites.getSprSignature())
59+ msg:addU32(PIC_SIGNATURE)
60+
61+ if g_game.getFeature(GamePreviewState) then
62+ msg:addU8(0)
63+ end
64+
65+ local offset = msg:getMessageSize()
66+ if g_game.getFeature(GameLoginPacketEncryption) then
67+ -- first RSA byte must be 0
68+ msg:addU8(0)
69+
70+ -- xtea key
71+ self:generateXteaKey()
72+ local xteaKey = self:getXteaKey()
73+ msg:addU32(xteaKey[1])
74+ msg:addU32(xteaKey[2])
75+ msg:addU32(xteaKey[3])
76+ msg:addU32(xteaKey[4])
77+ end
78+
79+ if g_game.getFeature(GameAccountNames) then
80+ msg:addString(self.accountName)
81+ else
82+ msg:addU32(tonumber(self.accountName))
83+ end
84+
85+ msg:addString(self.accountPassword)
86+
87+ if self.getLoginExtendedData then
88+ local data = self:getLoginExtendedData()
89+ msg:addString(data)
90+ else
91+ msg:addString("OTCv8")
92+ local version = g_app.getVersion():split(" ")[1]:gsub("%.", "")
93+ if version:len() == 2 then
94+ version = version .. "0"
95+ end
96+ msg:addU16(tonumber(version))
97+ end
98+
99+ local paddingBytes = g_crypt.rsaGetSize() - (msg:getMessageSize() - offset)
100+ assert(paddingBytes >= 0)
101+ for i = 1, paddingBytes do
102+ msg:addU8(math.random(0, 0xff))
103+ end
104+
105+ if g_game.getFeature(GameLoginPacketEncryption) then
106+ msg:encryptRsa()
107+ end
108+
109+ if g_game.getFeature(GameOGLInformation) then
110+ msg:addU8(1) --unknown
111+ msg:addU8(1) --unknown
112+
113+ if g_game.getClientVersion() >= 1072 then
114+ msg:addString(string.format('%s %s', g_graphics.getVendor(), g_graphics.getRenderer()))
115+ else
116+ msg:addString(g_graphics.getRenderer())
117+ end
118+ msg:addString(g_graphics.getVersion())
119+ end
120+
121+ -- add RSA encrypted auth token
122+ if g_game.getFeature(GameAuthenticator) then
123+ offset = msg:getMessageSize()
124+
125+ -- first RSA byte must be 0
126+ msg:addU8(0)
127+ msg:addString(self.authenticatorToken)
128+
129+ if g_game.getFeature(GameSessionKey) then
130+ msg:addU8(booleantonumber(self.stayLogged))
131+ end
132+
133+ paddingBytes = g_crypt.rsaGetSize() - (msg:getMessageSize() - offset)
134+ assert(paddingBytes >= 0)
135+ for i = 1, paddingBytes do
136+ msg:addU8(math.random(0, 0xff))
137+ end
138+
139+ msg:encryptRsa()
140+ end
141+
142+ if g_game.getFeature(GamePacketSizeU32) then
143+ self:enableBigPackets()
144+ end
145+
146+ if g_game.getFeature(GameProtocolChecksum) then
147+ self:enableChecksum()
148+ end
149+
150+ self:send(msg)
151+ if g_game.getFeature(GameLoginPacketEncryption) then
152+ self:enableXteaEncryption()
153+ end
154+ self:recv()
155+end
156+
157+function ProtocolLogin:onConnect()
158+ self.gotConnection = true
159+ self:connectCallback()
160+ self.connectCallback = nil
161+end
162+
163+function ProtocolLogin:onRecv(msg)
164+ while not msg:eof() do
165+ local opcode = msg:getU8()
166+ if opcode == LoginServerErrorNew then
167+ self:parseError(msg)
168+ elseif opcode == LoginServerError then
169+ self:parseError(msg)
170+ elseif opcode == LoginServerMotd then
171+ self:parseMotd(msg)
172+ elseif opcode == LoginServerUpdateNeeded then
173+ signalcall(self.onLoginError, self, tr("Client needs update."))
174+ elseif opcode == LoginServerTokenSuccess then
175+ local unknown = msg:getU8()
176+ elseif opcode == LoginServerTokenError then
177+ -- TODO: prompt for token here
178+ local unknown = msg:getU8()
179+ signalcall(self.onLoginError, self, tr("Invalid authentification token."))
180+ elseif opcode == LoginServerCharacterList then
181+ self:parseCharacterList(msg)
182+ elseif opcode == LoginServerExtendedCharacterList then
183+ self:parseExtendedCharacterList(msg)
184+ elseif opcode == LoginServerUpdate then
185+ local signature = msg:getString()
186+ signalcall(self.onUpdateNeeded, self, signature)
187+ elseif opcode == LoginServerSessionKey then
188+ self:parseSessionKey(msg)
189+ elseif opcode == LoginServerProxyList then
190+ local proxies = {}
191+ local proxiesCount = msg:getU8()
192+ for i=1, proxiesCount do
193+ local host = msg:getString()
194+ local port = msg:getU16()
195+ local priority = msg:getU16()
196+ table.insert(proxies, {host=host, port=port, priority=priority})
197+ end
198+ signalcall(self.onProxyList, self, proxies)
199+ else
200+ self:parseOpcode(opcode, msg)
201+ end
202+ end
203+ self:disconnect()
204+end
205+
206+function ProtocolLogin:parseError(msg)
207+ local errorMessage = msg:getString()
208+ signalcall(self.onLoginError, self, errorMessage)
209+end
210+
211+function ProtocolLogin:parseMotd(msg)
212+ local motd = msg:getString()
213+ signalcall(self.onMotd, self, motd)
214+end
215+
216+function ProtocolLogin:parseSessionKey(msg)
217+ local sessionKey = msg:getString()
218+ signalcall(self.onSessionKey, self, sessionKey)
219+end
220+
221+function ProtocolLogin:parseCharacterList(msg)
222+ local characters = {}
223+
224+ if g_game.getClientVersion() > 1010 then
225+ local worlds = {}
226+
227+ local worldsCount = msg:getU8()
228+ for i=1, worldsCount do
229+ local world = {}
230+ local worldId = msg:getU8()
231+ world.worldName = msg:getString()
232+ world.worldIp = msg:getString()
233+ world.worldPort = msg:getU16()
234+ world.previewState = msg:getU8()
235+ worlds[worldId] = world
236+ end
237+
238+ local charactersCount = msg:getU8()
239+ for i = 1, charactersCount do
240+ local character = {}
241+ local worldId = msg:getU8()
242+ character.name = msg:getString()
243+ character.level = msg:getU32()
244+ character.vocation = msg:getU16()
245+ character.looktype = msg:getU16()
246+ character.lookhead = msg:getU16()
247+ character.lookbody = msg:getU16()
248+ character.looklegs = msg:getU16()
249+ character.lookfeet = msg:getU16()
250+ character.lookaddons = msg:getU16()
251+ character.lookwings = msg:getU16()
252+ character.lookaura = msg:getU16()
253+ character.lookshader = msg:getString()
254+ character.worldName = worlds[worldId].worldName
255+ character.worldIp = worlds[worldId].worldIp
256+ character.worldPort = worlds[worldId].worldPort
257+ character.previewState = worlds[worldId].previewState
258+ characters[i] = character
259+ end
260+
261+ else
262+ local charactersCount = msg:getU8()
263+ for i=1,charactersCount do
264+ local character = {}
265+ character.name = msg:getString()
266+ character.level = msg:getU32()
267+ character.vocation = msg:getU16()
268+ character.looktype = msg:getU16()
269+ character.lookhead = msg:getU16()
270+ character.lookbody = msg:getU16()
271+ character.looklegs = msg:getU16()
272+ character.lookfeet = msg:getU16()
273+ character.lookaddons = msg:getU16()
274+ character.lookwings = msg:getU16()
275+ character.lookaura = msg:getU16()
276+ character.lookshader = msg:getString()
277+ character.worldName = msg:getString()
278+ character.worldIp = iptostring(msg:getU32())
279+ character.worldPort = msg:getU16()
280+
281+ if g_game.getFeature(GamePreviewState) then
282+ character.previewState = msg:getU8()
283+ end
284+
285+ characters[i] = character
286+ end
287+ end
288+
289+ local account = {}
290+ if g_game.getProtocolVersion() > 1077 then
291+ account.status = msg:getU8()
292+ account.subStatus = msg:getU8()
293+
294+ account.premDays = msg:getU32()
295+ if account.premDays ~= 0 and account.premDays ~= 65535 then
296+ account.premDays = math.floor((account.premDays - os.time()) / 86400)
297+ end
298+ else
299+ account.status = AccountStatus.Ok
300+ account.premDays = msg:getU16()
301+ account.subStatus = account.premDays > 0 and SubscriptionStatus.Premium or SubscriptionStatus.Free
302+ end
303+
304+ signalcall(self.onCharacterList, self, characters, account)
305+end
306+
307+function ProtocolLogin:parseExtendedCharacterList(msg)
308+ local characters = msg:getTable()
309+ local account = msg:getTable()
310+ local otui = msg:getString()
311+ signalcall(self.onCharacterList, self, characters, account, otui)
312+end
313+
314+function ProtocolLogin:parseOpcode(opcode, msg)
315+ signalcall(self.onOpcode, self, opcode, msg)
316+end
317+
318+function ProtocolLogin:onError(msg, code)
319+ local text = translateNetworkError(code, self:isConnecting(), msg)
320+ signalcall(self.onLoginError, self, text)
321+end
A RD_Client/modules/gamelib/spells.lua +441-0 View file
@@ -0,0 +1,441 @@
1+SpelllistSettings = {
2+ ['Default'] = {
3+ iconFile = '/images/game/spells/defaultspells',
4+ iconSize = {width = 32, height = 32},
5+ spellListWidth = 210,
6+ spellWindowWidth = 550,
7+ spellOrder = {'Animate Dead', 'Annihilation', 'Avalanche', 'Berserk', 'Blood Rage', 'Brutal Strike', 'Cancel Invisibility', 'Challenge', 'Chameleon', 'Charge', 'Conjure Arrow', 'Conjure Bolt', 'Conjure Explosive Arrow', 'Conjure Piercing Bolt', 'Conjure Poisoned Arrow', 'Conjure Power Bolt', 'Conjure Sniper Arrow', 'Convince Creature', 'Creature Illusion', 'Cure Bleeding', 'Cure Burning', 'Cure Curse', 'Cure Electrification', 'Cure Poison', 'Cure Poison Rune', 'Curse', 'Death Strike', 'Desintegrate', 'Destroy Field', 'Divine Caldera', 'Divine Healing', 'Divine Missile', 'Electrify', 'Enchant Party', 'Enchant Spear', 'Enchant Staff', 'Energy Beam', 'Energy Field', 'Energy Strike', 'Energy Wall', 'Energy Wave', 'Energybomb', 'Envenom', 'Eternal Winter', 'Ethereal Spear', 'Explosion', 'Fierce Berserk', 'Find Person', 'Fire Field', 'Fire Wall', 'Fire Wave', 'Fireball', 'Firebomb', 'Flame Strike', 'Food', 'Front Sweep', 'Great Energy Beam', 'Great Fireball', 'Great Light', 'Groundshaker', 'Haste', 'Heal Friend', 'Heal Party', 'Heavy Magic Missile', 'Hells Core', 'Holy Flash', 'Holy Missile', 'Ice Strike', 'Ice Wave', 'Icicle', 'Ignite', 'Inflict Wound', 'Intense Healing', 'Intense Healing Rune', 'Intense Recovery', 'Intense Wound Cleansing', 'Invisibility', 'Levitate', 'Light', 'Light Healing', 'Light Magic Missile', 'Lightning', 'Magic Rope', 'Magic Shield', 'Magic Wall', 'Mass Healing', 'Paralyze', 'Physical Strike', 'Poison Bomb', 'Poison Field', 'Poison Wall', 'Protect Party', 'Protector', 'Rage of the Skies', 'Recovery', 'Salvation', 'Sharpshooter', 'Soulfire', 'Stalagmite', 'Stone Shower', 'Strong Energy Strike', 'Strong Ethereal Spear', 'Strong Flame Strike', 'Strong Haste', 'Strong Ice Strike', 'Strong Ice Wave', 'Strong Terra Strike', 'Sudden Death', 'Summon Creature', 'Swift Foot', 'Terra Strike', 'Terra Wave', 'Thunderstorm', 'Train Party', 'Ultimate Energy Strike', 'Ultimate Flame Strike', 'Ultimate Healing', 'Ultimate Healing Rune', 'Ultimate Ice Strike', 'Ultimate Light', 'Ultimate Terra Strike', 'Whirlwind Throw', 'Wild Growth', 'Wound Cleansing', 'Wrath of Nature'}
8+ }--[[,
9+
10+ ['Sample'] = {
11+ iconFile = '/images/game/spells/sample',
12+ iconSize = {width = 64, height = 64},
13+ spellOrder = {'Critical Strike', 'Firefly', 'Fire Breath', 'Moonglaives', 'Wind Walk'}
14+ }]]
15+}
16+
17+SpellInfo = {
18+ ['Default'] = {
19+ ['Death Strike'] = {id = 87, words = 'exori mort', exhaustion = 2000, premium = true, type = 'Instant', icon = 'deathstrike', mana = 20, level = 16, soul = 0, group = {[1] = 2000}, vocations = {1, 5}},
20+ ['Flame Strike'] = {id = 89, words = 'exori flam', exhaustion = 2000, premium = true, type = 'Instant', icon = 'flamestrike', mana = 20, level = 14, soul = 0, group = {[1] = 2000}, vocations = {1, 2, 5, 6}},
21+ ['Strong Flame Strike'] = {id = 150, words = 'exori gran flam', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongflamestrike', mana = 60, level = 70, soul = 0, group = {[1] = 2000, [4] = 8000}, vocations = {1, 5}},
22+ ['Ultimate Flame Strike'] = {id = 154, words = 'exori max flam', exhaustion = 30000, premium = true, type = 'Instant', icon = 'ultimateflamestrike', mana = 100, level = 90, soul = 0, group = {[1] = 4000}, vocations = {1, 5}},
23+ ['Energy Strike'] = {id = 88, words = 'exori vis', exhaustion = 2000, premium = true, type = 'Instant', icon = 'energystrike', mana = 20, level = 12, soul = 0, group = {[1] = 2000}, vocations = {1, 2, 5, 6}},
24+ ['Strong Energy Strike'] = {id = 151, words = 'exori gran vis', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongenergystrike', mana = 60, level = 80, soul = 0, group = {[1] = 2000, [4] = 8000}, vocations = {1, 5}},
25+ ['Ultimate Energy Strike'] = {id = 155, words = 'exori max vis', exhaustion = 30000, premium = true, type = 'Instant', icon = 'ultimateenergystrike', mana = 100, level = 100,soul = 0, group = {[1] = 4000}, vocations = {1, 5}},
26+ ['Whirlwind Throw'] = {id = 107, words = 'exori hur', exhaustion = 6000, premium = true, type = 'Instant', icon = 'whirlwindthrow', mana = 40, level = 28, soul = 0, group = {[1] = 2000}, vocations = {4, 8}},
27+ ['Fire Wave'] = {id = 19, words = 'exevo flam hur', exhaustion = 4000, premium = false, type = 'Instant', icon = 'firewave', mana = 25, level = 18, soul = 0, group = {[1] = 2000}, vocations = {1, 5}},
28+ ['Ethereal Spear'] = {id = 111, words = 'exori con', exhaustion = 2000, premium = true, type = 'Instant', icon = 'etherealspear', mana = 25, level = 23, soul = 0, group = {[1] = 2000}, vocations = {3, 7}},
29+ ['Strong Ethereal Spear'] = {id = 57, words = 'exori gran con', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongetherealspear', mana = 55, level = 90, soul = 0, group = {[1] = 2000}, vocations = {3, 7}},
30+ ['Energy Beam'] = {id = 22, words = 'exevo vis lux', exhaustion = 4000, premium = false, type = 'Instant', icon = 'energybeam', mana = 40, level = 23, soul = 0, group = {[1] = 2000}, vocations = {1, 5}},
31+ ['Great Energy Beam'] = {id = 23, words = 'exevo gran vis lux', exhaustion = 6000, premium = false, type = 'Instant', icon = 'greatenergybeam', mana = 110, level = 29, soul = 0, group = {[1] = 2000}, vocations = {1, 5}},
32+ ['Groundshaker'] = {id = 106, words = 'exori mas', exhaustion = 8000, premium = true, type = 'Instant', icon = 'groundshaker', mana = 160, level = 33, soul = 0, group = {[1] = 2000}, vocations = {4, 8}},
33+ ['Berserk'] = {id = 80, words = 'exori', exhaustion = 4000, premium = true, type = 'Instant', icon = 'berserk', mana = 115, level = 35, soul = 0, group = {[1] = 2000}, vocations = {4, 8}},
34+ ['Annihilation'] = {id = 62, words = 'exori gran ico', exhaustion = 30000, premium = true, type = 'Instant', icon = 'annihilation', mana = 300, level = 110,soul = 0, group = {[1] = 4000}, vocations = {4, 8}},
35+ ['Brutal Strike'] = {id = 61, words = 'exori ico', exhaustion = 6000, premium = true, type = 'Instant', icon = 'brutalstrike', mana = 30, level = 16, soul = 0, group = {[1] = 2000}, vocations = {4, 8}},
36+ ['Front Sweep'] = {id = 59, words = 'exori min', exhaustion = 6000, premium = true, type = 'Instant', icon = 'frontsweep', mana = 200, level = 70, soul = 0, group = {[1] = 2000}, vocations = {4, 8}},
37+ ['Inflict Wound'] = {id = 141, words = 'utori kor', exhaustion = 30000, premium = true, type = 'Instant', icon = 'inflictwound', mana = 30, level = 40, soul = 0, group = {[1] = 2000}, vocations = {4, 8}},
38+ ['Ignite'] = {id = 138, words = 'utori flam', exhaustion = 30000, premium = true, type = 'Instant', icon = 'ignite', mana = 30, level = 26, soul = 0, group = {[1] = 2000}, vocations = {1, 5}},
39+ ['Lightning'] = {id = 149, words = 'exori amp vis', exhaustion = 8000, premium = true, type = 'Instant', icon = 'lightning', mana = 60, level = 55, soul = 0, group = {[1] = 2000, [4] = 8000}, vocations = {1, 5}},
40+ ['Curse'] = {id = 139, words = 'utori mort', exhaustion = 50000, premium = true, type = 'Instant', icon = 'curse', mana = 30, level = 75, soul = 0, group = {[1] = 2000}, vocations = {1, 5}},
41+ ['Electrify'] = {id = 140, words = 'utori vis', exhaustion = 30000, premium = true, type = 'Instant', icon = 'electrify', mana = 30, level = 34, soul = 0, group = {[1] = 2000}, vocations = {1, 5}},
42+ ['Energy Wave'] = {id = 13, words = 'exevo vis hur', exhaustion = 8000, premium = false, type = 'Instant', icon = 'energywave', mana = 170, level = 38, soul = 0, group = {[1] = 2000}, vocations = {1, 5}},
43+ ['Rage of the Skies'] = {id = 119, words = 'exevo gran mas vis', exhaustion = 40000, premium = true, type = 'Instant', icon = 'rageoftheskies', mana = 600, level = 55, soul = 0, group = {[1] = 4000}, vocations = {1, 5}},
44+ ['Fierce Berserk'] = {id = 105, words = 'exori gran', exhaustion = 6000, premium = true, type = 'Instant', icon = 'fierceberserk', mana = 340, level = 90, soul = 0, group = {[1] = 2000}, vocations = {4, 8}},
45+ ['Hells Core'] = {id = 24, words = 'exevo gran mas flam', exhaustion = 40000, premium = true, type = 'Instant', icon = 'hellscore', mana = 1100, level = 60, soul = 0, group = {[1] = 4000}, vocations = {1, 5}},
46+ ['Holy Flash'] = {id = 143, words = 'utori san', exhaustion = 40000, premium = true, type = 'Instant', icon = 'holyflash', mana = 30, level = 70, soul = 0, group = {[1] = 2000}, vocations = {3, 7}},
47+ ['Divine Missile'] = {id = 122, words = 'exori san', exhaustion = 2000, premium = true, type = 'Instant', icon = 'divinemissile', mana = 20, level = 40, soul = 0, group = {[1] = 2000}, vocations = {3, 7}},
48+ ['Divine Caldera'] = {id = 124, words = 'exevo mas san', exhaustion = 4000, premium = true, type = 'Instant', icon = 'divinecaldera', mana = 160, level = 50, soul = 0, group = {[1] = 2000}, vocations = {3, 7}},
49+ ['Physical Strike'] = {id = 148, words = 'exori moe ico', exhaustion = 2000, premium = true, type = 'Instant', icon = 'physicalstrike', mana = 20, level = 16, soul = 0, group = {[1] = 2000}, vocations = {2, 6}},
50+ ['Eternal Winter'] = {id = 118, words = 'exevo gran mas frigo', exhaustion = 40000, premium = true, type = 'Instant', icon = 'eternalwinter', mana = 1050, level = 60, soul = 0, group = {[1] = 4000}, vocations = {2, 6}},
51+ ['Ice Strike'] = {id = 112, words = 'exori frigo', exhaustion = 2000, premium = true, type = 'Instant', icon = 'icestrike', mana = 20, level = 15, soul = 0, group = {[1] = 2000}, vocations = {1, 5, 2, 6}},
52+ ['Strong Ice Strike'] = {id = 152, words = 'exori gran frigo', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongicestrike', mana = 60, level = 80, soul = 0, group = {[1] = 2000, [4] = 8000}, vocations = {2, 6}},
53+ ['Ultimate Ice Strike'] = {id = 156, words = 'exori max frigo', exhaustion = 30000, premium = true, type = 'Instant', icon = 'ultimateicestrike', mana = 100, level = 100,soul = 0, group = {[1] = 4000}, vocations = {2, 6}},
54+ ['Ice Wave'] = {id = 121, words = 'exevo frigo hur', exhaustion = 4000, premium = false, type = 'Instant', icon = 'icewave', mana = 25, level = 18, soul = 0, group = {[1] = 2000}, vocations = {2, 6}},
55+ ['Strong Ice Wave'] = {id = 43, words = 'exevo gran frigo hur', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongicewave', mana = 170, level = 40, soul = 0, group = {[1] = 2000}, vocations = {2, 6}},
56+ ['Envenom'] = {id = 142, words = 'utori pox', exhaustion = 40000, premium = true, type = 'Instant', icon = 'envenom', mana = 30, level = 50, soul = 0, group = {[1] = 2000}, vocations = {2, 6}},
57+ ['Terra Strike'] = {id = 113, words = 'exori tera', exhaustion = 2000, premium = true, type = 'Instant', icon = 'terrastrike', mana = 20, level = 13, soul = 0, group = {[1] = 2000}, vocations = {1, 5, 2, 6}},
58+ ['Strong Terra Strike'] = {id = 153, words = 'exori gran tera', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongterrastrike', mana = 60, level = 70, soul = 0, group = {[1] = 2000, [4] = 8000}, vocations = {2, 6}},
59+ ['Ultimate Terra Strike'] = {id = 157, words = 'exori max tera', exhaustion = 30000, premium = true, type = 'Instant', icon = 'ultimateterrastrike', mana = 100, level = 90, soul = 0, group = {[1] = 4000}, vocations = {2, 6}},
60+ ['Terra Wave'] = {id = 120, words = 'exevo tera hur', exhaustion = 4000, premium = false, type = 'Instant', icon = 'terrawave', mana = 210, level = 38, soul = 0, group = {[1] = 2000}, vocations = {2, 6}},
61+ ['Wrath of Nature'] = {id = 56, words = 'exevo gran mas tera', exhaustion = 40000, premium = true, type = 'Instant', icon = 'wrathofnature', mana = 700, level = 55, soul = 0, group = {[1] = 4000}, vocations = {2, 6}},
62+ ['Light Healing'] = {id = 1, words = 'exura', exhaustion = 1000, premium = false, type = 'Instant', icon = 'lighthealing', mana = 20, level = 9, soul = 0, group = {[2] = 1000}, vocations = {1, 2, 3, 5, 6, 7}},
63+ ['Wound Cleansing'] = {id = 123, words = 'exura ico', exhaustion = 1000, premium = false, type = 'Instant', icon = 'woundcleansing', mana = 40, level = 10, soul = 0, group = {[2] = 1000}, vocations = {4, 8}},
64+ ['Intense Wound Cleansing'] = {id = 158, words = 'exura gran ico', exhaustion = 600000,premium = true, type = 'Instant', icon = 'intensewoundcleansing', mana = 200, level = 80, soul = 0, group = {[2] = 1000}, vocations = {4, 8}},
65+ ['Cure Bleeding'] = {id = 144, words = 'exana kor', exhaustion = 6000, premium = true, type = 'Instant', icon = 'curebleeding', mana = 30, level = 30, soul = 0, group = {[2] = 1000}, vocations = {4, 8}},
66+ ['Cure Electrification'] = {id = 146, words = 'exana vis', exhaustion = 6000, premium = true, type = 'Instant', icon = 'curseelectrification', mana = 30, level = 22, soul = 0, group = {[2] = 1000}, vocations = {2, 6}},
67+ ['Cure Poison'] = {id = 29, words = 'exana pox', exhaustion = 6000, premium = false, type = 'Instant', icon = 'curepoison', mana = 30, level = 10, soul = 0, group = {[2] = 1000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}},
68+ ['Cure Burning'] = {id = 145, words = 'exana flam', exhaustion = 6000, premium = true, type = 'Instant', icon = 'cureburning', mana = 30, level = 30, soul = 0, group = {[2] = 1000}, vocations = {2, 6}},
69+ ['Cure Curse'] = {id = 147, words = 'exana mort', exhaustion = 6000, premium = true, type = 'Instant', icon = 'curecurse', mana = 40, level = 80, soul = 0, group = {[2] = 1000}, vocations = {3, 7}},
70+ ['Recovery'] = {id = 159, words = 'utura', exhaustion = 60000, premium = true, type = 'Instant', icon = 'recovery', mana = 75, level = 50, soul = 0, group = {[2] = 1000}, vocations = {4, 8, 3, 7}},
71+ ['Intense Recovery'] = {id = 160, words = 'utura gran', exhaustion = 60000, premium = true, type = 'Instant', icon = 'intenserecovery', mana = 165, level = 100,soul = 0, group = {[2] = 1000}, vocations = {4, 8, 3, 7}},
72+ ['Salvation'] = {id = 36, words = 'exura gran san', exhaustion = 1000, premium = true, type = 'Instant', icon = 'salvation', mana = 210, level = 60, soul = 0, group = {[2] = 1000}, vocations = {3, 7}},
73+ ['Intense Healing'] = {id = 2, words = 'exura gran', exhaustion = 1000, premium = false, type = 'Instant', icon = 'intensehealing', mana = 70, level = 20, soul = 0, group = {[2] = 1000}, vocations = {1, 2, 3, 5, 6, 7}},
74+ ['Heal Friend'] = {id = 84, words = 'exura sio', exhaustion = 1000, premium = true, type = 'Instant', icon = 'healfriend', mana = 140, level = 18, soul = 0, group = {[2] = 1000}, vocations = {2, 6}},
75+ ['Ultimate Healing'] = {id = 3, words = 'exura vita', exhaustion = 1000, premium = false, type = 'Instant', icon = 'ultimatehealing', mana = 160, level = 30, soul = 0, group = {[2] = 1000}, vocations = {1, 2, 5, 6}},
76+ ['Mass Healing'] = {id = 82, words = 'exura gran mas res', exhaustion = 2000, premium = true, type = 'Instant', icon = 'masshealing', mana = 150, level = 36, soul = 0, group = {[2] = 1000}, vocations = {2, 6}},
77+ ['Divine Healing'] = {id = 125, words = 'exura san', exhaustion = 1000, premium = false, type = 'Instant', icon = 'divinehealing', mana = 160, level = 35, soul = 0, group = {[2] = 1000}, vocations = {3, 7}},
78+ ['Light'] = {id = 10, words = 'utevo lux', exhaustion = 2000, premium = false, type = 'Instant', icon = 'light', mana = 20, level = 8, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}},
79+ ['Find Person'] = {id = 20, words = 'exiva', exhaustion = 2000, premium = false, type = 'Instant', icon = 'findperson', mana = 20, level = 8, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}},
80+ ['Magic Rope'] = {id = 76, words = 'exani tera', exhaustion = 2000, premium = true, type = 'Instant', icon = 'magicrope', mana = 20, level = 9, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}},
81+ ['Levitate'] = {id = 81, words = 'exani hur', exhaustion = 2000, premium = true, type = 'Instant', icon = 'levitate', mana = 50, level = 12, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}},
82+ ['Great Light'] = {id = 11, words = 'utevo gran lux', exhaustion = 2000, premium = false, type = 'Instant', icon = 'greatlight', mana = 60, level = 13, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}},
83+ ['Magic Shield'] = {id = 44, words = 'utamo vita', exhaustion = 2000, premium = false, type = 'Instant', icon = 'magicshield', mana = 50, level = 14, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
84+ ['Haste'] = {id = 6, words = 'utani hur', exhaustion = 2000, premium = true, type = 'Instant', icon = 'haste', mana = 60, level = 14, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}},
85+ ['Charge'] = {id = 131, words = 'utani tempo hur', exhaustion = 2000, premium = true, type = 'Instant', icon = 'charge', mana = 100, level = 25, soul = 0, group = {[3] = 2000}, vocations = {4, 8}},
86+ ['Swift Foot'] = {id = 134, words = 'utamo tempo san', exhaustion = 2000, premium = true, type = 'Instant', icon = 'swiftfoot', mana = 400, level = 55, soul = 0, group = {[1] = 10000, [3] = 2000}, vocations = {3, 7}},
87+ ['Challenge'] = {id = 93, words = 'exeta res', exhaustion = 2000, premium = true, type = 'Instant', icon = 'challenge', mana = 30, level = 20, soul = 0, group = {[3] = 2000}, vocations = {8}},
88+ ['Strong Haste'] = {id = 39, words = 'utani gran hur', exhaustion = 2000, premium = true, type = 'Instant', icon = 'stronghaste', mana = 100, level = 20, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
89+ ['Creature Illusion'] = {id = 38, words = 'utevo res ina', exhaustion = 2000, premium = false, type = 'Instant', icon = 'creatureillusion', mana = 100, level = 23, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
90+ ['Ultimate Light'] = {id = 75, words = 'utevo vis lux', exhaustion = 2000, premium = true, type = 'Instant', icon = 'ultimatelight', mana = 140, level = 26, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
91+ ['Cancel Invisibility'] = {id = 90, words = 'exana ina', exhaustion = 2000, premium = true, type = 'Instant', icon = 'cancelinvisibility', mana = 200, level = 26, soul = 0, group = {[3] = 2000}, vocations = {3, 7}},
92+ ['Invisibility'] = {id = 45, words = 'utana vid', exhaustion = 2000, premium = false, type = 'Instant', icon = 'invisible', mana = 440, level = 35, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
93+ ['Sharpshooter'] = {id = 135, words = 'utito tempo san', exhaustion = 2000, premium = true, type = 'Instant', icon = 'sharpshooter', mana = 450, level = 60, soul = 0, group = {[2] = 10000, [3] = 10000}, vocations = {3, 7}},
94+ ['Protector'] = {id = 132, words = 'utamo tempo', exhaustion = 2000, premium = true, type = 'Instant', icon = 'protector', mana = 200, level = 55, soul = 0, group = {[1] = 10000, [3] = 2000}, vocations = {4, 8}},
95+ ['Blood Rage'] = {id = 133, words = 'utito tempo', exhaustion = 2000, premium = true, type = 'Instant', icon = 'bloodrage', mana = 290, level = 60, soul = 0, group = {[3] = 2000}, vocations = {4, 8}},
96+ ['Train Party'] = {id = 126, words = 'utito mas sio', exhaustion = 2000, premium = true, type = 'Instant', icon = 'trainparty', mana = 'Var.', level = 32, soul = 0, group = {[3] = 2000}, vocations = {8}},
97+ ['Protect Party'] = {id = 127, words = 'utamo mas sio', exhaustion = 2000, premium = true, type = 'Instant', icon = 'protectparty', mana = 'Var.', level = 32, soul = 0, group = {[3] = 2000}, vocations = {7}},
98+ ['Heal Party'] = {id = 128, words = 'utura mas sio', exhaustion = 2000, premium = true, type = 'Instant', icon = 'healparty', mana = 'Var.', level = 32, soul = 0, group = {[3] = 2000}, vocations = {6}},
99+ ['Enchant Party'] = {id = 129, words = 'utori mas sio', exhaustion = 2000, premium = true, type = 'Instant', icon = 'enchantparty', mana = 'Var.', level = 32, soul = 0, group = {[3] = 2000}, vocations = {5}},
100+ ['Summon Creature'] = {id = 9, words = 'utevo res', exhaustion = 2000, premium = false, type = 'Instant', icon = 'summoncreature', mana = 'Var.', level = 25, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
101+ ['Conjure Arrow'] = {id = 51, words = 'exevo con', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'conjurearrow', mana = 100, level = 13, soul = 1, group = {[3] = 2000}, vocations = {3, 7}},
102+ ['Food'] = {id = 42, words = 'exevo pan', exhaustion = 2000, premium = false, type = 'Instant', icon = 'food', mana = 120, level = 14, soul = 1, group = {[3] = 2000}, vocations = {2, 6}},
103+ ['Conjure Poisoned Arrow'] = {id = 48, words = 'exevo con pox', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'poisonedarrow', mana = 130, level = 16, soul = 2, group = {[3] = 2000}, vocations = {3, 7}},
104+ ['Conjure Bolt'] = {id = 79, words = 'exevo con mort', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'conjurebolt', mana = 140, level = 17, soul = 2, group = {[3] = 2000}, vocations = {3, 7}},
105+ ['Conjure Sniper Arrow'] = {id = 108, words = 'exevo con hur', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'sniperarrow', mana = 160, level = 24, soul = 3, group = {[3] = 2000}, vocations = {3, 7}},
106+ ['Conjure Explosive Arrow'] = {id = 49, words = 'exevo con flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'explosivearrow', mana = 290, level = 25, soul = 3, group = {[3] = 2000}, vocations = {3, 7}},
107+ ['Conjure Piercing Bolt'] = {id = 109, words = 'exevo con grav', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'piercingbolt', mana = 180, level = 33, soul = 3, group = {[3] = 2000}, vocations = {3, 7}},
108+ ['Enchant Staff'] = {id = 92, words = 'exeta vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'enchantstaff', mana = 80, level = 41, soul = 0, group = {[3] = 2000}, vocations = {5}},
109+ ['Enchant Spear'] = {id = 110, words = 'exeta con', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'enchantspear', mana = 350, level = 45, soul = 3, group = {[3] = 2000}, vocations = {3, 7}},
110+ ['Conjure Power Bolt'] = {id = 95, words = 'exevo con vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'powerbolt', mana = 800, level = 59, soul = 3, group = {[3] = 2000}, vocations = {7}},
111+ ['Poison Field'] = {id = 26, words = 'adevo grav pox', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'poisonfield', mana = 200, level = 14, soul = 1, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
112+ ['Light Magic Missile'] = {id = 7, words = 'adori min vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'lightmagicmissile', mana = 120, level = 15, soul = 1, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
113+ ['Fire Field'] = {id = 25, words = 'adevo grav flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'firefield', mana = 240, level = 15, soul = 1, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
114+ ['Fireball'] = {id = 15, words = 'adori flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'fireball', mana = 460, level = 27, soul = 3, group = {[3] = 2000}, vocations = {1, 5}},
115+ ['Energy Field'] = {id = 27, words = 'adevo grav vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'energyfield', mana = 320, level = 18, soul = 2, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
116+ ['Stalagmite'] = {id = 77, words = 'adori tera', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'stalagmite', mana = 400, level = 24, soul = 2, group = {[3] = 2000}, vocations = {1, 5, 2, 6}},
117+ ['Great Fireball'] = {id = 16, words = 'adori mas flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'greatfireball', mana = 530, level = 30, soul = 3, group = {[3] = 2000}, vocations = {1, 5}},
118+ ['Heavy Magic Missile'] = {id = 8, words = 'adori vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'heavymagicmissile', mana = 350, level = 25, soul = 2, group = {[3] = 2000}, vocations = {1, 5, 2, 6}},
119+ ['Poison Bomb'] = {id = 91, words = 'adevo mas pox', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'poisonbomb', mana = 520, level = 25, soul = 2, group = {[3] = 2000}, vocations = {2, 6}},
120+ ['Firebomb'] = {id = 17, words = 'adevo mas flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'firebomb', mana = 600, level = 27, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
121+ ['Soulfire'] = {id = 50, words = 'adevo res flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'soulfire', mana = 600, level = 27, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
122+ ['Poison Wall'] = {id = 32, words = 'adevo mas grav pox', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'poisonwall', mana = 640, level = 29, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
123+ ['Explosion'] = {id = 18, words = 'adevo mas hur', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'explosion', mana = 570, level = 31, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
124+ ['Fire Wall'] = {id = 28, words = 'adevo mas grav flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'firewall', mana = 780, level = 33, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
125+ ['Energybomb'] = {id = 55, words = 'adevo mas vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'energybomb', mana = 880, level = 37, soul = 5, group = {[3] = 2000}, vocations = {1, 5}},
126+ ['Energy Wall'] = {id = 33, words = 'adevo mas grav vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'energywall', mana = 1000, level = 41, soul = 5, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
127+ ['Sudden Death'] = {id = 21, words = 'adori gran mort', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'suddendeath', mana = 985, level = 45, soul = 5, group = {[3] = 2000}, vocations = {1, 5}},
128+ ['Cure Poison Rune'] = {id = 31, words = 'adana pox', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'antidote', mana = 200, level = 15, soul = 1, group = {[3] = 2000}, vocations = {2, 6}},
129+ ['Intense Healing Rune'] = {id = 4, words = 'adura gran', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'intensehealingrune', mana = 240, level = 15, soul = 2, group = {[3] = 2000}, vocations = {2, 6}},
130+ ['Ultimate Healing Rune'] = {id = 5, words = 'adura vita', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'ultimatehealingrune', mana = 400, level = 24, soul = 3, group = {[3] = 2000}, vocations = {2, 6}},
131+ ['Convince Creature'] = {id = 12, words = 'adeta sio', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'convincecreature', mana = 200, level = 16, soul = 3, group = {[3] = 2000}, vocations = {2, 6}},
132+ ['Animate Dead'] = {id = 83, words = 'adana mort', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'animatedead', mana = 600, level = 27, soul = 5, group = {[3] = 2000}, vocations = {1, 2, 5, 6}},
133+ ['Chameleon'] = {id = 14, words = 'adevo ina', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'chameleon', mana = 600, level = 27, soul = 2, group = {[3] = 2000}, vocations = {2, 6}},
134+ ['Destroy Field'] = {id = 30, words = 'adito grav', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'destroyfield', mana = 120, level = 17, soul = 2, group = {[3] = 2000}, vocations = {1, 2, 3, 5, 6, 7}},
135+ ['Desintegrate'] = {id = 78, words = 'adito tera', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'desintegrate', mana = 200, level = 21, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 3, 5, 6, 7}},
136+ ['Magic Wall'] = {id = 86, words = 'adevo grav tera', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'magicwall', mana = 750, level = 32, soul = 5, group = {[3] = 2000}, vocations = {1, 5}},
137+ ['Wild Growth'] = {id = 94, words = 'adevo grav vita', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'wildgrowth', mana = 600, level = 27, soul = 5, group = {[3] = 2000}, vocations = {2, 6}},
138+ ['Paralyze'] = {id = 54, words = 'adana ani', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'paralyze', mana = 1400, level = 54, soul = 3, group = {[3] = 2000}, vocations = {2, 6}},
139+ ['Icicle'] = {id = 114, words = 'adori frigo', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'icicle', mana = 460, level = 28, soul = 3, group = {[3] = 2000}, vocations = {2, 6}},
140+ ['Avalanche'] = {id = 115, words = 'adori mas frigo', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'avalanche', mana = 530, level = 30, soul = 3, group = {[3] = 2000}, vocations = {2, 6}},
141+ ['Stone Shower'] = {id = 116, words = 'adori mas tera', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'stoneshower', mana = 430, level = 28, soul = 3, group = {[3] = 2000}, vocations = {2, 6}},
142+ ['Thunderstorm'] = {id = 117, words = 'adori mas vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'thunderstorm', mana = 430, level = 28, soul = 3, group = {[3] = 2000}, vocations = {1, 5}},
143+ ['Holy Missile'] = {id = 130, words = 'adori san', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'holymissile', mana = 350, level = 27, soul = 3, group = {[3] = 2000}, vocations = {3, 7}}
144+ }--[[,
145+
146+ ['Sample'] = {
147+ ['Wind Walk'] = {id = 1, words = 'windwalk', description = 'Run at enormous speed.', exhaustion = 2000, premium = false, type = 'Instant', icon = 1, mana = 50, level = 10, soul = 0, group = {[3] = 2000}, vocations = {1, 2}},
148+ ['Fire Breath'] = {id = 2, words = 'firebreath', description = 'A strong firewave.', exhaustion = 2000, premium = false, type = 'Instant', icon = 2, mana = 350, level = 27, soul = 0, group = {[1] = 2000}, vocations = {4, 8}},
149+ ['Moonglaives'] = {id = 3, words = 'moonglaives', description = 'Throw moonglaives around you.', exhaustion = 2000, premium = false, type = 'Instant', icon = 3, mana = 90, level = 55, soul = 0, group = {[1] = 2000}, vocations = {3, 7}},
150+ ['Critical Strike'] = {id = 4, words = 'criticalstrike', description = 'Land a critical strike.', exhaustion = 2000, premium = false, type = 'Instant', icon = 4, mana = 350, level = 27, soul = 0, group = {[1] = 2000}, vocations = {3, 4, 7, 8}},
151+ ['Firefly'] = {id = 5, words = 'firefly', description = 'Summon a angry firefly', exhaustion = 2000, premium = false, type = 'Instant', icon = 5, mana = 350, level = 27, soul = 0, group = {[1] = 2000}, vocations = {1, 2, 5, 6}}
152+ }]]
153+}
154+
155+-- ['const_name'] = {client_id, TFS_id}
156+-- Conversion from TFS icon id to the id used by client (icons.png order)
157+SpellIcons = {
158+ ['intenserecovery'] = {16, 160},
159+ ['recovery'] = {15, 159},
160+ ['intensewoundcleansing'] = {4, 158},
161+ ['ultimateterrastrike'] = {37, 157},
162+ ['ultimateicestrike'] = {34, 156},
163+ ['ultimateenergystrike'] = {31, 155},
164+ ['ultimateflamestrike'] = {28, 154},
165+ ['strongterrastrike'] = {36, 153},
166+ ['strongicestrike'] = {33, 152},
167+ ['strongenergystrike'] = {30, 151},
168+ ['strongflamestrike'] = {27, 150},
169+ ['lightning'] = {51, 149},
170+ ['physicalstrike'] = {17, 148},
171+ ['curecurse'] = {11, 147},
172+ ['curseelectrification'] = {14, 146},
173+ ['cureburning'] = {13, 145},
174+ ['curebleeding'] = {12, 144},
175+ ['holyflash'] = {53, 143},
176+ ['envenom'] = {58, 142},
177+ ['inflictwound'] = {57, 141},
178+ ['electrify'] = {56, 140},
179+ ['curse'] = {54, 139},
180+ ['ignite'] = {55, 138},
181+ -- [[ 136 / 137 Unknown ]]
182+ ['sharpshooter'] = {121, 135},
183+ ['swiftfoot'] = {119, 134},
184+ ['bloodrage'] = {96, 133},
185+ ['protector'] = {122, 132},
186+ ['charge'] = {98, 131},
187+ ['holymissile'] = {76, 130},
188+ ['enchantparty'] = {113, 129},
189+ ['healparty'] = {126, 128},
190+ ['protectparty'] = {123, 127},
191+ ['trainparty'] = {120, 126},
192+ ['divinehealing'] = {2, 125},
193+ ['divinecaldera'] = {40, 124},
194+ ['woundcleansing'] = {3, 123},
195+ ['divinemissile'] = {39, 122},
196+ ['icewave'] = {45, 121},
197+ ['terrawave'] = {47, 120},
198+ ['rageoftheskies'] = {52, 119},
199+ ['eternalwinter'] = {50, 118},
200+ ['thunderstorm'] = {63, 117},
201+ ['stoneshower'] = {65, 116},
202+ ['avalanche'] = {92, 115},
203+ ['icicle'] = {75, 114},
204+ ['terrastrike'] = {35, 113},
205+ ['icestrike'] = {32, 112},
206+ ['etherealspear'] = {18, 111},
207+ ['enchantspear'] = {104, 110},
208+ ['piercingbolt'] = {110, 109},
209+ ['sniperarrow'] = {112, 108},
210+ ['whirlwindthrow'] = {19, 107},
211+ ['groundshaker'] = {25, 106},
212+ ['fierceberserk'] = {22, 105},
213+ -- [[ 96 - 104 Unknown ]]
214+ ['powerbolt'] = {108, 95},
215+ ['wildgrowth'] = {61, 94},
216+ ['challenge'] = {97, 93},
217+ ['enchantstaff'] = {103, 92},
218+ ['poisonbomb'] = {70, 91},
219+ ['cancelinvisibility'] = {95, 90},
220+ ['flamestrike'] = {26, 89},
221+ ['energystrike'] = {29, 88},
222+ ['deathstrike'] = {38, 87},
223+ ['magicwall'] = {72, 86},
224+ ['healfriend'] = {8, 84},
225+ ['animatedead'] = {93, 83},
226+ ['masshealing'] = {9, 82},
227+ ['levitate'] = {125, 81},
228+ ['berserk'] = {21, 80},
229+ ['conjurebolt'] = {107, 79},
230+ ['desintegrate'] = {88, 78},
231+ ['stalagmite'] = {66, 77},
232+ ['magicrope'] = {105, 76},
233+ ['ultimatelight'] = {115, 75},
234+ -- [[ 71 - 64 TFS House Commands ]]
235+ -- [[ 63 - 70 Unknown ]]
236+ ['annihilation'] = {24, 62},
237+ ['brutalstrike'] = {23, 61},
238+ -- [[ 60 Unknown ]]
239+ ['frontsweep'] = {20, 59},
240+ -- [[ 58 Unknown ]]
241+ ['strongetherealspear'] = {59, 57},
242+ ['wrathofnature'] = {48, 56},
243+ ['energybomb'] = {86, 55},
244+ ['paralyze'] = {71, 54},
245+ -- [[ 53 Unknown ]]
246+ -- [[ 52 TFS Retrieve Friend ]]
247+ ['conjurearrow'] = {106, 51},
248+ ['soulfire'] = {67, 50},
249+ ['explosivearrow'] = {109, 49},
250+ ['poisonedarrow'] = {111, 48},
251+ -- [[ 46 / 47 Unknown ]]
252+ ['invisible'] = {94, 45},
253+ ['magicshield'] = {124, 44},
254+ ['strongicewave'] = {46, 43},
255+ ['food'] = {99, 42},
256+ -- [[ 40 / 41 Unknown ]]
257+ ['stronghaste'] = {102, 39},
258+ ['creatureillusion'] = {100, 38},
259+ -- [[ 37 TFS Move ]]
260+ ['salvation'] = {60, 36},
261+ -- [[ 34 / 35 Unknown ]]
262+ ['energywall'] = {84, 33},
263+ ['poisonwall'] = {68, 32},
264+ ['antidote'] = {10, 31},
265+ ['destroyfield'] = {87, 30},
266+ ['curepoison'] = {10, 29},
267+ ['firewall'] = {80, 28},
268+ ['energyfield'] = {85, 27},
269+ ['poisonfield'] = {69, 26},
270+ ['firefield'] = {81, 25},
271+ ['hellscore'] = {49, 24},
272+ ['greatenergybeam'] = {42, 23},
273+ ['energybeam'] = {41, 22},
274+ ['suddendeath'] = {64, 21},
275+ ['findperson'] = {114, 20},
276+ ['firewave'] = {44, 19},
277+ ['explosion'] = {83, 18},
278+ ['firebomb'] = {82, 17},
279+ ['greatfireball'] = {78, 16},
280+ ['fireball'] = {79, 15},
281+ ['chameleon'] = {91, 14},
282+ ['energywave'] = {43, 13},
283+ ['convincecreature'] = {90, 12},
284+ ['greatlight'] = {116, 11},
285+ ['light'] = {117, 10},
286+ ['summoncreature'] = {118, 9},
287+ ['heavymagicmissile'] = {77, 8},
288+ ['lightmagicmissile'] = {73, 7},
289+ ['haste'] = {101, 6},
290+ ['ultimatehealingrune'] = {62, 5},
291+ ['intensehealingrune'] = {74, 4},
292+ ['ultimatehealing'] = {1, 3},
293+ ['intensehealing'] = {7, 2},
294+ ['lighthealing'] = {6, 1}
295+}
296+
297+VocationNames = {
298+ [1] = 'Sorcerer',
299+ [2] = 'Druid',
300+ [3] = 'Paladin',
301+ [4] = 'Knight',
302+ [5] = 'Master Sorcerer',
303+ [6] = 'Elder Druid',
304+ [7] = 'Royal Paladin',
305+ [8] = 'Elite Knight'
306+}
307+
308+SpellGroups = {
309+ [1] = 'Attack',
310+ [2] = 'Healing',
311+ [3] = 'Support',
312+ [4] = 'Special'
313+}
314+
315+Spells = {}
316+
317+function Spells.getClientId(spellName)
318+ local profile = Spells.getSpellProfileByName(spellName)
319+
320+ local id = SpellInfo[profile][spellName].icon
321+ if not tonumber(id) and SpellIcons[id] then
322+ return SpellIcons[id][1]
323+ end
324+ return tonumber(id)
325+end
326+
327+function Spells.getServerId(spellName)
328+ local profile = Spells.getSpellProfileByName(spellName)
329+
330+ local id = SpellInfo[profile][spellName].icon
331+ if not tonumber(id) and SpellIcons[id] then
332+ return SpellIcons[id][2]
333+ end
334+ return tonumber(id)
335+end
336+
337+function Spells.getSpellByName(name)
338+ return SpellInfo[Spells.getSpellProfileByName(name)][name]
339+end
340+
341+function Spells.getSpellByWords(words)
342+ local words = words:lower():trim()
343+ for profile,data in pairs(SpellInfo) do
344+ for k,spell in pairs(data) do
345+ if spell.words == words then
346+ return spell, profile, k
347+ end
348+ end
349+ end
350+ return nil
351+end
352+
353+function Spells.getSpellByIcon(iconId)
354+ for profile,data in pairs(SpellInfo) do
355+ for k,spell in pairs(data) do
356+ if spell.id == iconId then
357+ return spell, profile, k
358+ end
359+ end
360+ end
361+ return nil
362+end
363+
364+function Spells.getSpellIconIds()
365+ local ids = {}
366+ for profile,data in pairs(SpellInfo) do
367+ for k,spell in pairs(data) do
368+ table.insert(ids, spell.id)
369+ end
370+ end
371+ return ids
372+end
373+
374+function Spells.getSpellProfileById(id)
375+ for profile,data in pairs(SpellInfo) do
376+ for k,spell in pairs(data) do
377+ if spell.id == id then
378+ return profile
379+ end
380+ end
381+ end
382+ return nil
383+end
384+
385+function Spells.getSpellProfileByWords(words)
386+ for profile,data in pairs(SpellInfo) do
387+ for k,spell in pairs(data) do
388+ if spell.words == words then
389+ return profile
390+ end
391+ end
392+ end
393+ return nil
394+end
395+
396+function Spells.getSpellProfileByName(spellName)
397+ for profile,data in pairs(SpellInfo) do
398+ if table.findbykey(data, spellName:trim(), true) then
399+ return profile
400+ end
401+ end
402+ return nil
403+end
404+
405+function Spells.getSpellsByVocationId(vocId)
406+ local spells = {}
407+ for profile,data in pairs(SpellInfo) do
408+ for k,spell in pairs(data) do
409+ if table.contains(spell.vocations, vocId) then
410+ table.insert(spells, spell)
411+ end
412+ end
413+ end
414+ return spells
415+end
416+
417+function Spells.filterSpellsByGroups(spells, groups)
418+ local filtered = {}
419+ for v,spell in pairs(spells) do
420+ local spellGroups = Spells.getGroupIds(spell)
421+ if table.equals(spellGroups, groups) then
422+ table.insert(filtered, spell)
423+ end
424+ end
425+ return filtered
426+end
427+
428+function Spells.getGroupIds(spell)
429+ local groups = {}
430+ for k,_ in pairs(spell.group) do
431+ table.insert(groups, k)
432+ end
433+ return groups
434+end
435+
436+function Spells.getImageClip(id, profile)
437+ return (((id-1)%12)*SpelllistSettings[profile].iconSize.width) .. ' '
438+ .. ((math.ceil(id/12)-1)*SpelllistSettings[profile].iconSize.height) .. ' '
439+ .. SpelllistSettings[profile].iconSize.width .. ' '
440+ .. SpelllistSettings[profile].iconSize.height
441+end
A RD_Client/modules/gamelib/textmessages.lua +34-0 View file
@@ -0,0 +1,34 @@
1+local messageModeCallbacks = {}
2+
3+function g_game.onTextMessage(messageMode, message)
4+ local callbacks = messageModeCallbacks[messageMode]
5+ if not callbacks or #callbacks == 0 then
6+ perror(string.format('Unhandled onTextMessage message mode %i: %s', messageMode, message))
7+ return
8+ end
9+
10+ for _, callback in pairs(callbacks) do
11+ callback(messageMode, message)
12+ end
13+end
14+
15+function registerMessageMode(messageMode, callback)
16+ if not messageModeCallbacks[messageMode] then
17+ messageModeCallbacks[messageMode] = {}
18+ end
19+
20+ table.insert(messageModeCallbacks[messageMode], callback)
21+ return true
22+end
23+
24+function unregisterMessageMode(messageMode, callback)
25+ if not messageModeCallbacks[messageMode] then
26+ return false
27+ end
28+
29+ return table.removevalue(messageModeCallbacks[messageMode], callback)
30+end
31+
32+function onViewChat()
33+ -- Implement the logic for viewing chat here
34+end
A RD_Client/modules/gamelib/thing.lua +49-0 View file
@@ -0,0 +1,49 @@
1+ThingCategoryItem = 0
2+ThingCategoryCreature = 1
3+ThingCategoryEffect = 2
4+ThingCategoryMissile = 3
5+ThingInvalidCategory = 4
6+ThingLastCategory = ThingInvalidCategory
7+
8+ThingAttrGround = 0
9+ThingAttrGroundBorder = 1
10+ThingAttrOnBottom = 2
11+ThingAttrOnTop = 3
12+ThingAttrContainer = 4
13+ThingAttrStackable = 5
14+ThingAttrForceUse = 6
15+ThingAttrMultiUse = 7
16+ThingAttrWritable = 8
17+ThingAttrWritableOnce = 9
18+ThingAttrFluidContainer = 10
19+ThingAttrSplash = 11
20+ThingAttrNotWalkable = 12
21+ThingAttrNotMoveable = 13
22+ThingAttrBlockProjectile = 14
23+ThingAttrNotPathable = 15
24+ThingAttrPickupable = 16
25+ThingAttrHangable = 17
26+ThingAttrHookSouth = 18
27+ThingAttrHookEast = 19
28+ThingAttrRotateable = 20
29+ThingAttrLight = 21
30+ThingAttrDontHide = 22
31+ThingAttrTranslucent = 23
32+ThingAttrDisplacement = 24
33+ThingAttrElevation = 25
34+ThingAttrLyingCorpse = 26
35+ThingAttrAnimateAlways = 27
36+ThingAttrMinimapColor = 28
37+ThingAttrLensHelp = 29
38+ThingAttrFullGround = 30
39+ThingAttrLook = 31
40+ThingAttrCloth = 32
41+ThingAttrMarket = 33
42+ThingAttrNoMoveAnimation = 253 -- >= 1010, value = 16
43+ThingAttrChargeable = 254 -- deprecated
44+ThingLastAttr = 255
45+
46+SpriteMaskRed = 1
47+SpriteMaskGreen = 2
48+SpriteMaskBlue = 3
49+SpriteMaskYellow = 4
A RD_Client/modules/gamelib/ui/uiaccordeonbutton.lua +25-0 View file
@@ -0,0 +1,25 @@
1+-- @docclass
2+UIAccordeonButton = extends(UIButton, "UIAccordeonButton")
3+
4+function UIAccordeonButton.create()
5+ local btn = UIAccordeonButton.internalCreate()
6+ btn:setOn(false)
7+ return btn
8+end
9+
10+function UIAccordeonButton.onClick(self)
11+ local contentPanel = self:getParent():getChildAfter(self)
12+
13+ if not contentPanel or #contentPanel:getChildren() == 0 then -- disable if there is no content to show
14+ return
15+ end
16+
17+ if not self:isOn() then
18+ contentPanel:getLayout():setFitChildren(true)
19+ else
20+ contentPanel:getLayout():setFitChildren(false)
21+ contentPanel:setHeight(0)
22+ end
23+
24+ self:setOn(not self:isOn())
25+end
A RD_Client/modules/gamelib/ui/uiaccordeonlistitem.lua +39-0 View file
@@ -0,0 +1,39 @@
1+-- @docclass
2+UIAccordeonListItem = extends(UIWidget, "UIAccordeonListItem")
3+
4+function UIAccordeonListItem.create()
5+ local panel = UIAccordeonListItem.internalCreate()
6+ return panel
7+end
8+
9+function UIAccordeonListItem:onStyleApply(styleName, styleNode)
10+ for name,value in pairs(styleNode) do
11+ if name == 'title' then
12+ self.title = value -- delayed execution
13+ end
14+ end
15+end
16+
17+function UIAccordeonListItem:onLayoutUpdate()
18+ local title = self:recursiveGetChildById('title')
19+ if title and self.title ~= nil then
20+ title:setText(self.title)
21+ self.title = nil
22+ end
23+
24+ -- couldn't find a better way to do this, but basically move all the added children to content panel automatically
25+ for i=#self:getChildren(), 3, -1 do
26+ self:recursiveGetChildById('content'):addChild(self:getChildren()[i])
27+ end
28+end
29+
30+function UIAccordeonListItem:setTitle(text)
31+ local title = self:recursiveGetChildById('title')
32+ if title then
33+ title:setText(text)
34+ end
35+end
36+
37+function UIAccordeonListItem:clearChildren()
38+ self:getChildById("content"):clearChildren()
39+end
A RD_Client/modules/gamelib/ui/uianim.lua +180-0 View file
@@ -0,0 +1,180 @@
1+-- @docclass
2+UIAnim = extends(UIImageView, "UIAnim")
3+
4+function UIAnim.create()
5+ local widget = UIAnim.internalCreate()
6+ widget.anim = {
7+ opacity = {
8+ value = 1,
9+ dir = 1,
10+ delta = 0.1,
11+ speed = 0
12+ },
13+ sprite = {
14+ playing = true,
15+ path = '',
16+ count = 0,
17+ speed = 0,
18+ index = 0,
19+ startIndex = 1,
20+ defaultIndex = 1,
21+ prefix = '',
22+ }
23+ }
24+ widget.events = {
25+ sprite = nil,
26+ opacity = nil
27+ }
28+ return widget
29+end
30+
31+function UIAnim:onStyleApply(styleName, styleNode)
32+ local updateAnim = false
33+
34+ for name,value in pairs(styleNode) do
35+ if name == 'sprite-path' then
36+ if not value:ends('/') then
37+ value = value .. '/'
38+ end
39+ self.anim.sprite.path = value
40+ updateAnim = true
41+ elseif name == 'sprite-prefix' then
42+ self.anim.sprite.prefix = value
43+ updateAnim = true
44+ elseif name == 'sprite-count' then
45+ self.anim.sprite.count = tonumber(value)
46+ updateAnim = true
47+ elseif name == 'sprite-speed' then
48+ self.anim.sprite.speed = tonumber(value)
49+ updateAnim = true
50+ elseif name == 'sprite-start-index' then
51+ self.anim.sprite.startIndex = tonumber(value)
52+ updateAnim = true
53+ elseif name == 'sprite-default-index' then
54+ self.anim.sprite.defaultIndex = tonumber(value)
55+ updateAnim = true
56+ elseif name == 'opacity-speed' then
57+ self.anim.opacity.speed = tonumber(value)
58+ updateAnim = true
59+ elseif name == 'opacity-delta' then
60+ self.anim.opacity.delta = tonumber(value)
61+ updateAnim = true
62+ end
63+ end
64+
65+ if updateAnim then
66+ self:updateAnim()
67+ end
68+end
69+
70+function UIAnim:onSpriteReset()
71+ local sprite = self.anim.sprite
72+ sprite.index = sprite.defaultIndex
73+
74+ local filename = sprite.path .. sprite.prefix .. sprite.index
75+ self:setImageSource(filename)
76+
77+ -- sprite.event = scheduleEvent(self.onAnim, sprite.speed)
78+end
79+
80+function UIAnim:onSprite()
81+ local sprite = self.anim.sprite
82+ sprite.index = sprite.index + 1
83+ if sprite.index > sprite.count then
84+ sprite.index = sprite.startIndex
85+ end
86+
87+ local filename = sprite.path .. sprite.prefix .. sprite.index
88+ self:setImageSource(filename)
89+
90+ -- sprite.event = scheduleEvent(self.onAnim, sprite.speed)
91+end
92+
93+function UIAnim:onOpacity()
94+ local settings = self.anim.opacity
95+ local opacity = self:getOpacity()
96+ local delta = settings.delta
97+ local dir = settings.dir
98+ opacity = opacity + dir * delta
99+
100+ if opacity < 0 or opacity > 1 then
101+ settings.dir = dir * -1
102+ end
103+
104+ self:setOpacity(opacity)
105+end
106+
107+function UIAnim:updateAnim(name)
108+ -- if not self.sprite then return end
109+
110+ if not name or name == 'sprite' then
111+ if self.events.sprite then
112+ removeEvent(self.events.sprite)
113+ self.events.sprite = nil
114+ end
115+
116+ if self.anim.sprite then
117+ local speed = self.anim.sprite.speed
118+ if speed > 0 then
119+ self.events.sprite = cycleEvent(function() self:onSprite() end, speed)
120+ end
121+ end
122+ end
123+
124+ if not name or name == 'opacity' then
125+ if self.events.opacity then
126+ removeEvent(self.events.opacity)
127+ self.events.opacity = nil
128+ end
129+
130+ if self.anim.opacity then
131+ local settings = self.anim.opacity
132+ if settings.speed > 0 then
133+ local delta = settings.delta
134+ self.events.opacity = cycleEvent(function() self:onOpacity() end, settings.speed)
135+ end
136+ end
137+ end
138+end
139+
140+function UIAnim:stopAnim(name)
141+ if name == 'sprite' then
142+ removeEvent(self.events.sprite)
143+ self.events.sprite = nil
144+ self:onSpriteReset()
145+
146+ elseif name == 'opacity' then
147+ removeEvent(self.events.opacity)
148+ self.events.opacity = nil
149+
150+ elseif not name then
151+ for name, event in pairs(self.events) do
152+ removeEvent(event)
153+ if name == 'sprite' then
154+ self:onSpriteReset()
155+ end
156+ end
157+
158+ self.events = {}
159+ end
160+end
161+
162+function UIAnim:startAnim(name)
163+ self:updateAnim(name)
164+end
165+
166+function UIAnim:onDestroy()
167+ for _, event in pairs(self.events) do
168+ removeEvent(event)
169+ end
170+ self.events = {}
171+end
172+
173+function UIAnim:isPlaying(name)
174+ name = name or 'sprite'
175+ if name == 'sprite' then
176+ return self.events.sprite ~= nil
177+ else
178+ return self.events.opacity ~= nil
179+ end
180+end
Top