First commit

This commit is contained in:
2025-02-26 13:42:34 +01:00
commit f465c9072c
2467 changed files with 426214 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
CONTAINER_POSITION = 0xFFFF

View File

@@ -0,0 +1,49 @@
function Container.isContainer(self)
return true
end
function Container.isItem(self)
return true
end
function Container.isMonster(self)
return false
end
function Container.isCreature(self)
return false
end
function Container.isPlayer(self)
return false
end
function Container.isTeleport(self)
return false
end
function Container.isTile(self)
return false
end
function Container.getItemsById(self, itemId)
local list = {}
for index = 0, (self:getSize() - 1) do
local item = self:getItem(index)
if item then
if item:isContainer() then
local rlist = item:getItemsById(itemId)
if type(rlist) == 'table' then
for _, v in pairs(rlist) do
table.insert(list, v)
end
end
else
if item:getId() == itemId then
table.insert(list, item)
end
end
end
end
return list
end

View File

@@ -0,0 +1,13 @@
dofile('data/lib/core/constants.lua')
dofile('data/lib/core/container.lua')
dofile('data/lib/core/creature.lua')
dofile('data/lib/core/monster.lua')
dofile('data/lib/core/game.lua')
dofile('data/lib/core/item.lua')
dofile('data/lib/core/itemtype.lua')
dofile('data/lib/core/player.lua')
dofile('data/lib/core/position.lua')
dofile('data/lib/core/teleport.lua')
dofile('data/lib/core/tile.lua')
dofile('data/lib/core/vocation.lua')
dofile('data/lib/core/guildwars.lua')

View File

@@ -0,0 +1,116 @@
function Creature.getClosestFreePosition(self, position, extended)
local usePosition = Position(position)
local tiles = { Tile(usePosition) }
local length = extended and 2 or 1
local tile
for y = -length, length do
for x = -length, length do
if x ~= 0 or y ~= 0 then
usePosition.x = position.x + x
usePosition.y = position.y + y
tile = Tile(usePosition)
if tile then
tiles[#tiles + 1] = tile
end
end
end
end
for i = 1, #tiles do
tile = tiles[i]
if tile:getCreatureCount() == 0 and not tile:hasProperty(CONST_PROP_IMMOVABLEBLOCKSOLID) then
return tile:getPosition()
end
end
return Position()
end
function Creature:setMonsterOutfit(monster, time)
local monsterType = MonsterType(monster)
if not monsterType then
return false
end
if self:isPlayer() and not (getPlayerFlagValue(self, PlayerFlag_CanIllusionAll) or monsterType:isIllusionable()) then
return false
end
local condition = Condition(CONDITION_OUTFIT)
condition:setOutfit(monsterType:getOutfit())
condition:setTicks(time)
self:addCondition(condition)
return true
end
function Creature:setItemOutfit(item, time)
local itemType = ItemType(item)
if not itemType then
return false
end
local condition = Condition(CONDITION_OUTFIT)
condition:setOutfit({
lookTypeEx = itemType:getId()
})
condition:setTicks(time)
self:addCondition(condition)
return true
end
function Creature:addSummon(monster)
local summon = Monster(monster)
if not summon then
return false
end
summon:setTarget(nil)
summon:setFollowCreature(nil)
summon:setDropLoot(false)
summon:setMaster(self)
return true
end
function Creature:removeSummon(monster)
local summon = Monster(monster)
if not summon or summon:getMaster() ~= self then
return false
end
summon:setTarget(nil)
summon:setFollowCreature(nil)
summon:setDropLoot(true)
summon:setMaster(nil)
return true
end
function Creature.getPlayer(self)
return self:isPlayer() and self or nil
end
function Creature.isItem(self)
return false
end
function Creature.isMonster(self)
return false
end
function Creature.isNpc(self)
return false
end
function Creature.isPlayer(self)
return false
end
function Creature.isTile(self)
return false
end
function Creature.isContainer(self)
return false
end

View File

@@ -0,0 +1,159 @@
function Game.sendMagicEffect(position, effect)
local pos = Position(position)
pos:sendMagicEffect(effect)
end
function Game.removeItemsOnMap(position)
local tile = Tile(position)
local tileCount = tile:getThingCount()
local i = 0
while i < tileCount do
local tileItem = tile:getThing(i)
if tileItem and tileItem:getType():isMovable() then
tileItem:remove()
else
i = i + 1
end
end
end
function Game.transformItemOnMap(position, itemId, toItemId, subtype)
if not subtype then
subtype = -1
end
local tile = Tile(position)
local item = tile:getItemById(itemId)
item:transform(toItemId, subtype)
item:decay()
return item
end
function Game.removeItemOnMap(position, itemId, subtype)
if not subtype then
subtype = -1
end
local tile = Tile(position)
local item = tile:getItemById(itemId, subtype)
item:remove()
end
function Game.isItemThere(position, itemId)
local tile = Tile(position)
return tile:getItemById(itemId) ~= nil
end
function Game.isPlayerThere(position)
local tile = Tile(position)
local creatures = tile:getCreatures()
for _, creature in ipairs(creatures) do
if creature:isPlayer() then
return true
end
end
return false
end
function Game.isMonsterThere(position, monsterName)
local tile = Tile(position)
local creatures = tile:getCreatures()
for _, creature in ipairs(creatures) do
if creature:isMonster() and creature:getName():lower() == monsterName:lower() then
return creature
end
end
return nil
end
function Game.broadcastMessage(message, messageType)
if messageType == nil then
messageType = MESSAGE_STATUS_WARNING
end
for _, player in ipairs(Game.getPlayers()) do
player:sendTextMessage(messageType, message)
end
end
function Game.convertIpToString(ip)
local band = bit.band
local rshift = bit.rshift
return string.format("%d.%d.%d.%d",
band(ip, 0xFF),
band(rshift(ip, 8), 0xFF),
band(rshift(ip, 16), 0xFF),
rshift(ip, 24)
)
end
function Game.getReverseDirection(direction)
if direction == WEST then
return EAST
elseif direction == EAST then
return WEST
elseif direction == NORTH then
return SOUTH
elseif direction == SOUTH then
return NORTH
elseif direction == NORTHWEST then
return SOUTHEAST
elseif direction == NORTHEAST then
return SOUTHWEST
elseif direction == SOUTHWEST then
return NORTHEAST
elseif direction == SOUTHEAST then
return NORTHWEST
end
return NORTH
end
function Game.getSkillType(weaponType)
if weaponType == WEAPON_CLUB then
return SKILL_CLUB
elseif weaponType == WEAPON_SWORD then
return SKILL_SWORD
elseif weaponType == WEAPON_AXE then
return SKILL_AXE
elseif weaponType == WEAPON_DISTANCE then
return SKILL_DISTANCE
elseif weaponType == WEAPON_SHIELD then
return SKILL_SHIELD
end
return SKILL_FIST
end
if not globalStorageTable then
globalStorageTable = {}
end
function Game.getStorageValue(key)
-- Return from local table if possible
if globalStorageTable[key] ~= nil then
return globalStorageTable[key]
end
-- Else look for it on the DB
local dbData = db.storeQuery("SELECT `value` FROM `global_storage` WHERE `key` = " .. key .. " LIMIT 1;")
if dbData ~= false then
local value = result.getNumber(dbData, "value")
if value ~= nil then
-- Save it to globalStorageTable
globalStorageTable[key] = value
return value
end
end
return nil
end
function Game.setStorageValue(key, value)
globalStorageTable[key] = value
local dbData = db.storeQuery("SELECT `value` FROM `global_storage` WHERE `key` = " .. key .. " LIMIT 1;")
if dbData ~= false then
db.query("UPDATE `global_storage` SET `value`='".. value .."' WHERE `key` = " .. key .. " LIMIT 1;")
else
db.query("INSERT INTO `global_storage` (`key`, `value`) VALUES (" .. key .. ", " .. value .. ");")
end
end

View File

@@ -0,0 +1,199 @@
guildwars = {}
guildwars.__index = guildwars
function guildwars:isInWar(player1, player2)
if not player1:getGuild() or not player2:getGuild() then
return 0
end
if player1:getGuild():getId() == 0 or player2:getGuild():getId() == 0 then
return 0
end
if player1:getGuild():getId() == player2:getGuild():getId() then
return 0
end
return isInWar(player1:getId(), player2:getId())
end
function guildwars:processKill(warId, killer, player)
local fragLimit = self:getFragLimit(warId)
local killerFrags = self:getKills(warId, killer:getGuild():getId()) + 1
local deadFrags = self:getKills(warId, player:getGuild():getId())
local killerMsg = "Opponent " .. player:getName() .. " of the " .. player:getGuild():getName() .. " was killed by " .. killer:getName() .. ". The new score is " .. killerFrags .. ":" .. deadFrags .. " frags (limit " .. fragLimit .. ")."
sendGuildChannelMessage(killer:getGuild():getId(), TALKTYPE_CHANNEL_O, killerMsg)
local deadMsg = "Guild member " .. player:getName() .. " was killed by " .. killer:getName() .. " of the " .. killer:getGuild():getName() .. ". The new score is " .. deadFrags .. ":" .. killerFrags .. " frags (limit " .. fragLimit .. ")."
sendGuildChannelMessage(player:getGuild():getId(), TALKTYPE_CHANNEL_O, deadMsg)
self:insertKill(warId, killer, player)
if killerFrags >= fragLimit then
self:endWar(warId, killer, player, killerFrags)
end
end
function guildwars:getFragLimit(warId)
local resultId = db.storeQuery("SELECT `frag_limit` FROM `guild_wars` WHERE `id` = " .. warId)
if resultId ~= false then
local frag_limit = result.getDataInt(resultId, "frag_limit")
result.free(resultId)
return frag_limit
end
return 0
end
function guildwars:getBounty(warId)
local resultId = db.storeQuery("SELECT `bounty` FROM `guild_wars` WHERE `id` = " .. warId)
if resultId ~= false then
local bounty = result.getDataInt(resultId, "bounty")
result.free(resultId)
return bounty
end
return 0
end
function guildwars:getKills(warId, guildId)
local resultId = db.storeQuery("SELECT COUNT(*) as frags FROM `guildwar_kills` WHERE `warid` = " .. warId .. " and `killerguild` = " .. guildId)
if resultId ~= false then
local frags = result.getDataInt(resultId, "frags")
result.free(resultId)
return frags
end
return 0
end
function guildwars:insertKill(warId, killer, target)
db.asyncQuery("INSERT INTO `guildwar_kills` (`killer`, `target`, `killerguild`, `targetguild`, `warid`, `time`) VALUES (" .. db.escapeString(killer:getName()) .. ", " .. db.escapeString(target:getName()) .. ", " .. killer:getGuild():getId() .. ", " .. target:getGuild():getId() .. ", " .. warId .. ", " .. os.time() .. ")")
end
function guildwars:endWar(warId, killer, player, frags)
local winGuildInternalMessage = "Congratulations! You have won the war against " .. player:getGuild():getName() .. " with " .. frags .. " frags."
sendGuildChannelMessage(killer:getGuild():getId(), TALKTYPE_CHANNEL_O, winGuildInternalMessage)
local loseGuildInternalMessage = "You have lost the war against " .. killer:getGuild():getName() .. ". They have reached the limit of " .. frags .. " frags."
sendGuildChannelMessage(player:getGuild():getId(), TALKTYPE_CHANNEL_O, loseGuildInternalMessage)
broadcastMessage(killer:getGuild():getName() .. " have won the war against " .. player:getGuild():getName() .. " with " .. frags .. " frags.", MESSAGE_EVENT_ADVANCE)
self:updateState(warId, 5)
self:setWarEmblem(killer:getGuild(), player:getGuild())
local bounty = self:getBounty(warId)
if bounty > 0 then
killer:getGuild():increaseBankBalance(bounty * 2)
end
end
function guildwars:setWarEmblem(guild1, guild2)
guild1:setGuildWarEmblem(guild2)
end
function guildwars:updateState(warId, status)
db.query("UPDATE `guild_wars` SET `status` = " .. status .. " WHERE `id` = " .. warId)
end
function guildwars:getPendingInvitation(guild1, guild2)
local resultId = db.storeQuery("SELECT `id`, `bounty` FROM `guild_wars` WHERE `guild1` = " .. guild1 .. " AND `guild2` = " .. guild2 .. " AND `status` = 0")
if resultId then
local id = result.getDataInt(resultId, "id")
local bounty = result.getDataInt(resultId, "bounty")
result.free(resultId)
return id, bounty
end
return 0
end
function guildwars:startWar(player, warId, guild1, guild2, bounty)
if bounty > 0 then
local guildBalance = guild1:getBankBalance()
if guildBalance < bounty then
player:sendCancelMessage("Your guild does not have that much money in the bank account balance to accept this war with the bounty of " .. bounty .. " gold.")
return true
end
if not guild1:decreaseBankBalance(bounty) then
player:sendCancelMessage("Your guild does not have that much money in the bank account balance to accept this war with the bounty of " .. bounty .. " gold.")
return true
end
end
self:updateState(warId, 1)
self:setWarEmblem(guild1, guild2)
broadcastMessage(guild1:getName() .. " has accepted " .. guild2:getName() .. " invitation to war.", MESSAGE_EVENT_ADVANCE)
end
function guildwars:rejectWar(warId, guild1, guild2, bounty)
self:updateState(warId, 2)
broadcastMessage(guild1:getName() .. " has rejected " .. guild2:getName() .. " invitation to war.", MESSAGE_EVENT_ADVANCE)
if bounty > 0 then
guild2:increaseBankBalance(bounty)
end
end
function guildwars:cancelWar(warId, guild1, guild2, bounty)
self:updateState(warId, 3)
broadcastMessage(guild1:getName() .. " has canceled invitation to a war with " .. guild2:getName() .. ".", MESSAGE_EVENT_ADVANCE)
if bounty > 0 then
guild1:increaseBankBalance(bounty)
end
end
function guildwars:invite(player, guild1, guild2, frags, bounty)
local str = ""
local tmpQuery = db.storeQuery("SELECT `guild1`, `status` FROM `guild_wars` WHERE `guild1` IN (" .. guild1:getId() .. "," .. guild2:getId() .. ") AND `guild2` IN (" .. guild2:getId() .. "," .. guild1:getId() .. ") AND `status` IN (0, 1)")
if tmpQuery then
if result.getDataInt(tmpQuery, "status") == 0 then
if result.getDataInt(tmpQuery, "guild1") == guild1:getId() then
str = "You have already invited " .. guild2:getName() .. " to war."
else
str = guild2:getName() .. " have already invited you to war."
end
else
str = "You are already on a war with " .. guild2:getName() .. "."
end
result.free(tmpQuery)
end
if str ~= "" then
player:sendCancelMessage(str)
return true
end
frags = math.max(10, math.min(500, frags))
bounty = math.max(0, math.min(10000000, bounty))
if bounty > 0 then
local guildBalance = guild1:getBankBalance()
if guildBalance < bounty then
player:sendCancelMessage("Your guild does not have that much money in the bank account balance to set this bounty.")
return true
end
if not guild1:decreaseBankBalance(bounty) then
player:sendCancelMessage("Your guild does not have that much money in the bank account balance to set this bounty.")
return true
end
end
db.asyncQuery("INSERT INTO `guild_wars` (`guild1`, `guild2`, `frag_limit`, `bounty`) VALUES (" .. guild1:getId() .. ", " .. guild2:getId() .. ", " .. frags .. ", " .. bounty .. ");")
local message = guild1:getName() .. " has invited " .. guild2:getName() .. " to war for " .. frags .. " frags."
if bounty > 0 then
message = message .. " The bounty reward is set to " .. bounty .. " gold."
end
broadcastMessage(message, MESSAGE_EVENT_ADVANCE)
end

View File

@@ -0,0 +1,31 @@
function Item.getType(self)
return ItemType(self:getId())
end
function Item.isItem(self)
return true
end
function Item.isContainer(self)
return false
end
function Item.isCreature(self)
return false
end
function Item.isMonster(self)
return false
end
function Item.isPlayer(self)
return false
end
function Item.isTeleport(self)
return false
end
function Item.isTile(self)
return false
end

View File

@@ -0,0 +1,16 @@
local slotBits = {
[CONST_SLOT_HEAD] = SLOTP_HEAD,
[CONST_SLOT_NECKLACE] = SLOTP_NECKLACE,
[CONST_SLOT_BACKPACK] = SLOTP_BACKPACK,
[CONST_SLOT_ARMOR] = SLOTP_ARMOR,
[CONST_SLOT_RIGHT] = SLOTP_RIGHT,
[CONST_SLOT_LEFT] = SLOTP_LEFT,
[CONST_SLOT_LEGS] = SLOTP_LEGS,
[CONST_SLOT_FEET] = SLOTP_FEET,
[CONST_SLOT_RING] = SLOTP_RING,
[CONST_SLOT_AMMO] = SLOTP_AMMO
}
function ItemType.usesSlot(self, slot)
return bit.band(self:getSlotPosition(), slotBits[slot] or 0) ~= 0
end

View File

@@ -0,0 +1,27 @@
function Monster.getMonster(self)
return self:isMonster() and self or nil
end
function Monster.isItem(self)
return false
end
function Monster.isMonster(self)
return true
end
function Monster.isNpc(self)
return false
end
function Monster.isPlayer(self)
return false
end
function Monster.isTile(self)
return false
end
function Monster.isContainer(self)
return false
end

View File

@@ -0,0 +1,99 @@
local foodCondition = Condition(CONDITION_REGENERATION, CONDITIONID_DEFAULT)
function Player.feed(self, food)
local condition = self:getCondition(CONDITION_REGENERATION, CONDITIONID_DEFAULT)
if condition then
condition:setTicks(condition:getTicks() + (food * 1000))
else
local vocation = self:getVocation()
if not vocation then
return nil
end
foodCondition:setTicks(food * 1000)
foodCondition:setParameter(CONDITION_PARAM_HEALTHGAIN, vocation:getHealthGainAmount())
foodCondition:setParameter(CONDITION_PARAM_HEALTHTICKS, vocation:getHealthGainTicks() * 1000)
foodCondition:setParameter(CONDITION_PARAM_MANAGAIN, vocation:getManaGainAmount())
foodCondition:setParameter(CONDITION_PARAM_MANATICKS, vocation:getManaGainTicks() * 1000)
self:addCondition(foodCondition)
end
return true
end
function Player.getClosestFreePosition(self, position, extended)
if self:getAccountType() >= ACCOUNT_TYPE_GOD then
return position
end
return Creature.getClosestFreePosition(self, position, extended)
end
function Player.getDepotItems(self, depotId)
return self:getDepotChest(depotId, true):getItemHoldingCount()
end
function Player.isNoVocation(self)
return self:getVocation():getId() == 0
end
function Player.isSorcerer(self)
return self:getVocation():getId() == 1 or self:getVocation():getId() == 5
end
function Player.isDruid(self)
return self:getVocation():getId() == 2 or self:getVocation():getId() == 6
end
function Player.isPaladin(self)
return self:getVocation():getId() == 3 or self:getVocation():getId() == 7
end
function Player.isKnight(self)
return self:getVocation():getId() == 4 or self:getVocation():getId() == 8
end
function Player.isPremium(self)
return self:getPremiumDays() > 0 or configManager.getBoolean(configKeys.FREE_PREMIUM)
end
function Player.sendCancelMessage(self, message)
if type(message) == "number" then
message = Game.getReturnMessage(message)
end
return self:sendTextMessage(MESSAGE_STATUS_SMALL, message)
end
function Player.isUsingOtClient(self)
return self:getClient().os >= CLIENTOS_OTCLIENT_LINUX
end
function Player.sendExtendedOpcode(self, opcode, buffer)
if not self:isUsingOtClient() then
return false
end
local networkMessage = NetworkMessage()
networkMessage:addByte(0x32)
networkMessage:addByte(opcode)
networkMessage:addString(buffer)
networkMessage:sendToPlayer(self)
networkMessage:delete()
return true
end
APPLY_SKILL_MULTIPLIER = true
local addSkillTriesFunc = Player.addSkillTries
function Player.addSkillTries(...)
APPLY_SKILL_MULTIPLIER = false
local ret = addSkillTriesFunc(...)
APPLY_SKILL_MULTIPLIER = true
return ret
end
local addManaSpentFunc = Player.addManaSpent
function Player.addManaSpent(...)
APPLY_SKILL_MULTIPLIER = false
local ret = addManaSpentFunc(...)
APPLY_SKILL_MULTIPLIER = true
return ret
end

View File

@@ -0,0 +1,99 @@
Position.directionOffset = {
[DIRECTION_NORTH] = {x = 0, y = -1},
[DIRECTION_EAST] = {x = 1, y = 0},
[DIRECTION_SOUTH] = {x = 0, y = 1},
[DIRECTION_WEST] = {x = -1, y = 0},
[DIRECTION_SOUTHWEST] = {x = -1, y = 1},
[DIRECTION_SOUTHEAST] = {x = 1, y = 1},
[DIRECTION_NORTHWEST] = {x = -1, y = -1},
[DIRECTION_NORTHEAST] = {x = 1, y = -1}
}
function Position:getNextPosition(direction, steps)
local offset = Position.directionOffset[direction]
if offset then
steps = steps or 1
self.x = self.x + offset.x * steps
self.y = self.y + offset.y * steps
end
end
function Position:moveUpstairs()
local isWalkable = function (position)
local tile = Tile(position)
if not tile then
return false
end
local ground = tile:getGround()
if not ground or ground:hasProperty(CONST_PROP_BLOCKSOLID) then
return false
end
local items = tile:getItems()
for i = 1, tile:getItemCount() do
local item = items[i]
local itemType = item:getType()
if itemType:getType() ~= ITEM_TYPE_MAGICFIELD and not itemType:isMovable() and item:hasProperty(CONST_PROP_BLOCKSOLID) then
return false
end
end
return true
end
local swap = function (lhs, rhs)
lhs.x, rhs.x = rhs.x, lhs.x
lhs.y, rhs.y = rhs.y, lhs.y
lhs.z, rhs.z = rhs.z, lhs.z
end
self.z = self.z - 1
local defaultPosition = self + Position.directionOffset[DIRECTION_SOUTH]
if not isWalkable(defaultPosition) then
for direction = DIRECTION_NORTH, DIRECTION_NORTHEAST do
if direction == DIRECTION_SOUTH then
direction = DIRECTION_WEST
end
local position = self + Position.directionOffset[direction]
if isWalkable(position) then
swap(self, position)
return self
end
end
end
swap(self, defaultPosition)
return self
end
function Position:moveRel(x, y, z)
self.x = self.x + x
self.y = self.y + y
self.z = self.z + z
return self
end
function Position:isInRange(from, to)
-- No matter what corner from and to is, we want to make
-- life easier by calculating north-west and south-east
local zone = {
nW = {
x = (from.x < to.x and from.x or to.x),
y = (from.y < to.y and from.y or to.y),
z = (from.z < to.z and from.z or to.z)
},
sE = {
x = (to.x > from.x and to.x or from.x),
y = (to.y > from.y and to.y or from.y),
z = (to.z > from.z and to.z or from.z)
}
}
if self.x >= zone.nW.x and self.x <= zone.sE.x
and self.y >= zone.nW.y and self.y <= zone.sE.y
and self.z >= zone.nW.z and self.z <= zone.sE.z then
return true
end
return false
end

View File

@@ -0,0 +1,3 @@
function Teleport.isTeleport(self)
return true
end

View File

@@ -0,0 +1,65 @@
function Tile.isItem(self)
return false
end
function Tile.isContainer(self)
return false
end
function Tile.isCreature(self)
return false
end
function Tile.isPlayer(self)
return false
end
function Tile.isTeleport(self)
return false
end
function Tile.isTile(self)
return true
end
function Tile.relocateTo(self, toPosition, pushMove, monsterPosition)
if self:getPosition() == toPosition then
return false
end
if not Tile(toPosition) then
return false
end
for i = self:getThingCount() - 1, 0, -1 do
local thing = self:getThing(i)
if thing then
if thing:isItem() then
if ItemType(thing.itemid):isMovable() then
thing:moveTo(toPosition)
end
elseif thing:isCreature() then
if monsterPosition and thing:isMonster() then
thing:teleportTo(monsterPosition, pushMove)
else
thing:teleportTo(toPosition, pushMove)
end
end
end
end
return true
end
function Tile:getPlayers()
local players = {}
local creatures = self:getCreatures()
if (creatures) then
for i = 1, #creatures do
if (creatures[i]:isPlayer()) then
table.insert(players, creatures[i])
end
end
end
return players
end

View File

@@ -0,0 +1,7 @@
function Vocation.getBase(self)
local base = self
while base:getDemotion() do
base = base:getDemotion()
end
return base
end

View File

@@ -0,0 +1,5 @@
-- Core API functions implemented in Lua
dofile('data/lib/core/core.lua')
-- Compatibility library for our old Lua API
dofile('data/lib/compat/compat.lua')