added mounts, auras, wings and shop. Packet compression does not work extendedOpCodes does not work but shop is because of extra configuration

This commit is contained in:
ErikasKontenis 2022-08-08 18:55:47 +03:00
parent b130e66149
commit 1c35d04337
58 changed files with 2909 additions and 77 deletions

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ vc14/.vs/
/SabrehavenOTClient/tibianus.log
/800OTClient/otclientv8.log
/800OTClient/exports
/otclientv8-master

View File

@ -13,30 +13,28 @@ function updateFeatures(version)
end
-- you can add custom features here, list of them is in the modules\gamelib\const.lua
g_game.enableFeature(GamePlayerMarket)
--g_game.enableFeature(GameClientPing)
--g_game.enableFeature(GameExtendedOpcode)
--g_game.enableFeature(GameMinimapLimitedToSingleFloor) -- it will generate minimap only for current floor
--g_game.enableFeature(GameSpritesAlphaChannel)
--if(version >= 770) then
g_game.enableFeature(GameLooktypeU16)
g_game.enableFeature(GameMessageStatements)
g_game.enableFeature(GameLoginPacketEncryption)
--end
--if(version >= 780) then
g_game.enableFeature(GamePlayerAddons)
g_game.enableFeature(GamePlayerStamina)
g_game.enableFeature(GameNewFluids)
g_game.enableFeature(GameMessageLevel)
g_game.enableFeature(GamePlayerStateU16)
g_game.enableFeature(GameNewOutfitProtocol)
--end
--if(version >= 790) then
g_game.enableFeature(GameWritableDate)
--end
-- customs
--g_game.enableFeature(GameIngameStore)
g_game.enableFeature(GamePlayerMarket)
g_game.enableFeature(GamePlayerMounts)
g_game.enableFeature(GameWingsAndAura)
g_game.enableFeature(GameOutfitShaders)
--g_game.enableFeature(GameExtendedOpcode)
--g_game.enableFeature(GamePacketCompression)
g_game.enableFeature(GameClientPing)
g_game.enableFeature(GameExtendedClientPing)
if(version >= 840) then
g_game.enableFeature(GameProtocolChecksum)

View File

@ -12,8 +12,8 @@ function init()
-- outfit shaders
g_shaders.createOutfitShader("outfit_default", "/shaders/outfit_default_vertex", "/shaders/outfit_default_fragment")
g_shaders.createOutfitShader("outfit_rainbow", "/shaders/outfit_rainbow_vertex", "/shaders/outfit_rainbow_fragment")
g_shaders.addTexture("outfit_rainbow", "/images/shaders/rainbow.png")
g_shaders.createOutfitShader("Rainbow Outfit", "/shaders/outfit_rainbow_vertex", "/shaders/outfit_rainbow_fragment")
g_shaders.addTexture("Rainbow Outfit", "/images/shaders/rainbow.png")
-- you can use creature:setOutfitShader("outfit_rainbow") to set shader

View File

@ -21,9 +21,9 @@ local AD = {}
local selectedOffer = {}
local function sendAction(action, data)
if not g_game.getFeature(GameExtendedOpcode) then
return
end
--if not g_game.getFeature(GameExtendedOpcode) then
-- return
--end
local protocolGame = g_game.getProtocolGame()
if data == nil then

File diff suppressed because one or more lines are too long

View File

@ -25,6 +25,7 @@ find_package(GMP REQUIRED)
find_package(PugiXML REQUIRED)
find_package(MySQL)
find_package(Threads)
find_package(ZLIB REQUIRED)
# Selects LuaJIT if user defines or auto-detected
if(DEFINED USE_LUAJIT AND NOT USE_LUAJIT)
@ -49,7 +50,7 @@ add_subdirectory(src)
add_executable(tfs ${tfs_SRC})
include_directories(${MYSQL_INCLUDE_DIR} ${LUA_INCLUDE_DIR} ${Boost_INCLUDE_DIRS} ${PUGIXML_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${Crypto++_INCLUDE_DIR})
target_link_libraries(tfs ${MYSQL_CLIENT_LIBS} ${LUA_LIBRARIES} ${Boost_LIBRARIES} ${Boost_FILESYSTEM_LIBRARY} ${PUGIXML_LIBRARIES} ${GMP_LIBRARIES} ${Crypto++_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
target_link_libraries(tfs ${MYSQL_CLIENT_LIBS} ${LUA_LIBRARIES} ${Boost_LIBRARIES} ${Boost_FILESYSTEM_LIBRARY} ${PUGIXML_LIBRARIES} ${ZLIB_LIBRARY} ${GMP_LIBRARIES} ${Crypto++_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
set_target_properties(tfs PROPERTIES COTIRE_CXX_PREFIX_HEADER_INIT "src/otpch.h")
set_target_properties(tfs PROPERTIES COTIRE_ADD_UNITY_BUILD FALSE)

16
cmake/FindZLIB.cmake Normal file
View File

@ -0,0 +1,16 @@
# Try to find the ZLIB library
# ZLIB_FOUND - system has ZLIB
# ZLIB_INCLUDE_DIR - the ZLIB include directory
# ZLIB_LIBRARY - the ZLIB library
FIND_PATH(ZLIB_INCLUDE_DIR NAMES zlib.h)
SET(_ZLIB_STATIC_LIBS libz.a libzlib.a zlib1.a)
SET(_ZLIB_SHARED_LIBS libz.dll.a zdll zlib zlib1 z)
IF(USE_STATIC_LIBS)
FIND_LIBRARY(ZLIB_LIBRARY NAMES ${_ZLIB_STATIC_LIBS} ${_ZLIB_SHARED_LIBS})
ELSE()
FIND_LIBRARY(ZLIB_LIBRARY NAMES ${_ZLIB_SHARED_LIBS} ${_ZLIB_STATIC_LIBS})
ENDIF()
INCLUDE(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(ZLIB DEFAULT_MSG ZLIB_LIBRARY ZLIB_INCLUDE_DIR)
MARK_AS_ADVANCED(ZLIB_LIBRARY ZLIB_INCLUDE_DIR)

View File

@ -51,7 +51,8 @@ allowClones = false
serverName = "Tibianus"
statusTimeout = 5000
replaceKickOnLogin = true
maxPacketsPerSecond = -1
maxPacketsPerSecond = 50
packetCompression = true
autoStackCumulatives = true
moneyRate = 1

6
data/XML/auras.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<auras>
<!-- <aura id="1" clientid="368" name="Widow Queen" speed="20" />
<aura id="2" clientid="369" name="Racing Bird" speed="20" />
<aura id="3" clientid="370" name="War Bear" speed="20" /> -->
</auras>

106
data/XML/mounts.xml Normal file
View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<mounts>
<mount id="1" clientid="368" name="Widow Queen" speed="20" premium="no" />
<!--
<mount id="2" clientid="369" name="Racing Bird" speed="20" premium="yes" />
<mount id="3" clientid="370" name="War Bear" speed="20" premium="yes" />
<mount id="4" clientid="371" name="Black Sheep" speed="20" premium="yes" />
<mount id="5" clientid="372" name="Midnight Panther" speed="20" premium="yes" />
<mount id="6" clientid="373" name="Draptor" speed="20" premium="yes" />
<mount id="7" clientid="374" name="Titanica" speed="20" premium="yes" />
<mount id="8" clientid="375" name="Tin Lizzard" speed="20" premium="yes" />
<mount id="9" clientid="376" name="Blazebringer" speed="20" premium="yes" />
<mount id="10" clientid="377" name="Rapid Boar" speed="20" premium="yes" />
<mount id="11" clientid="378" name="Stampor" speed="20" premium="yes" />
<mount id="12" clientid="379" name="Undead Cavebear" speed="20" premium="yes" />
<mount id="13" clientid="387" name="Donkey" speed="20" premium="yes" />
<mount id="14" clientid="388" name="Tiger Slug" speed="20" premium="yes" />
<mount id="15" clientid="389" name="Uniwheel" speed="20" premium="yes" />
<mount id="16" clientid="390" name="Crystal Wolf" speed="20" premium="yes" />
<mount id="17" clientid="392" name="War Horse" speed="20" premium="yes" />
<mount id="18" clientid="401" name="Kingly Deer" speed="20" premium="yes" />
<mount id="19" clientid="402" name="Tamed Panda" speed="20" premium="yes" />
<mount id="20" clientid="405" name="Dromedary" speed="20" premium="yes" />
<mount id="21" clientid="406" name="Scorpion King" speed="20" premium="yes" />
<mount id="22" clientid="421" name="Rented Horse" speed="20" premium="no" />
<mount id="23" clientid="426" name="Armoured War Horse" speed="20" premium="no" />
<mount id="24" clientid="427" name="Shadow Draptor" speed="20" premium="yes" />
<mount id="25" clientid="437" name="Rented Horse" speed="20" premium="no" />
<mount id="26" clientid="438" name="Rented Horse" speed="20" premium="no" />
<mount id="27" clientid="447" name="Lady Bug" speed="20" premium="yes" />
<mount id="28" clientid="450" name="Manta Ray" speed="20" premium="yes" />
<mount id="29" clientid="502" name="Ironblight" speed="20" premium="yes" />
<mount id="30" clientid="503" name="Magma Crawler" speed="20" premium="yes" />
<mount id="31" clientid="506" name="Dragonling" speed="20" premium="yes" />
<mount id="32" clientid="515" name="Gnarlhound" speed="20" premium="yes" />
<mount id="33" clientid="521" name="Crimson Ray" speed="20" premium="no" />
<mount id="34" clientid="522" name="Steelbeak" speed="20" premium="no" />
<mount id="35" clientid="526" name="Water Buffalo" speed="20" premium="yes" />
<mount id="36" clientid="546" name="Tombstinger" speed="20" premium="no" />
<mount id="37" clientid="547" name="Platesaurian" speed="20" premium="no" />
<mount id="38" clientid="548" name="Ursagrodon" speed="20" premium="yes" />
<mount id="39" clientid="559" name="The Hellgrip" speed="20" premium="yes" />
<mount id="40" clientid="571" name="Noble Lion" speed="20" premium="yes" />
<mount id="41" clientid="572" name="Desert King" speed="20" premium="no" />
<mount id="42" clientid="580" name="Shock Head" speed="20" premium="yes" />
<mount id="43" clientid="606" name="Walker" speed="20" premium="yes" />
<mount id="44" clientid="621" name="Azudocus" speed="20" premium="no" />
<mount id="45" clientid="622" name="Carpacosaurus" speed="20" premium="no" />
<mount id="46" clientid="624" name="Death Crawler" speed="20" premium="no" />
<mount id="47" clientid="626" name="Flamesteed" speed="20" premium="no" />
<mount id="48" clientid="627" name="Jade Lion" speed="20" premium="no" />
<mount id="49" clientid="628" name="Jade Pincer" speed="20" premium="no" />
<mount id="50" clientid="629" name="Nethersteed" speed="20" premium="no" />
<mount id="51" clientid="630" name="Tempest" speed="20" premium="no" />
<mount id="52" clientid="631" name="Winter King" speed="20" premium="no" />
<mount id="53" clientid="644" name="Doombringer" speed="20" premium="no" />
<mount id="54" clientid="647" name="Woodland Prince" speed="20" premium="no" />
<mount id="55" clientid="648" name="Hailstorm Fury" speed="20" premium="no" />
<mount id="56" clientid="649" name="Siegebreaker" speed="20" premium="no" />
<mount id="57" clientid="650" name="Poisonbane" speed="20" premium="no" />
<mount id="58" clientid="651" name="Blackpelt" speed="20" premium="no" />
<mount id="59" clientid="669" name="Golden Dragonfly" speed="20" premium="no" />
<mount id="60" clientid="670" name="Steel Bee" speed="20" premium="no" />
<mount id="61" clientid="671" name="Copper Fly" speed="20" premium="no" />
<mount id="62" clientid="672" name="Tundra Rambler" speed="20" premium="no" />
<mount id="63" clientid="673" name="Highland Yak" speed="20" premium="no" />
<mount id="64" clientid="674" name="Glacier Vagabond" speed="20" premium="no" />
<mount id="65" clientid="688" name="Flying Divan" speed="20" premium="no" />
<mount id="66" clientid="689" name="Magic Carpet" speed="20" premium="no" />
<mount id="67" clientid="690" name="Floating Kashmir" speed="20" premium="no" />
<mount id="68" clientid="691" name="Ringtail Waccoon" speed="20" premium="no" />
<mount id="69" clientid="692" name="Night Waccoon" speed="20" premium="no" />
<mount id="70" clientid="693" name="Emerald Waccoon" speed="20" premium="no" />
<mount id="71" clientid="682" name="Glooth Glider" speed="20" premium="yes" />
<mount id="72" clientid="685" name="Shadow Hart" speed="20" premium="no" />
<mount id="73" clientid="686" name="Black Stag" speed="20" premium="no" />
<mount id="74" clientid="687" name="Emperor Deer" speed="20" premium="no" />
<mount id="75" clientid="726" name="Flitterkatzen" speed="20" premium="no" />
<mount id="76" clientid="727" name="Venompaw" speed="20" premium="no" />
<mount id="77" clientid="728" name="Batcat" speed="20" premium="no" />
<mount id="78" clientid="734" name="Sea Devil" speed="20" premium="no" />
<mount id="79" clientid="735" name="Coralripper" speed="20" premium="no" />
<mount id="80" clientid="736" name="Plumfish" speed="20" premium="no" />
<mount id="81" clientid="738" name="Gorongra" speed="20" premium="no" />
<mount id="82" clientid="739" name="Noctungra" speed="20" premium="no" />
<mount id="83" clientid="740" name="Silverneck" speed="20" premium="no" />
<mount id="84" clientid="761" name="Slagsnare" speed="20" premium="no" />
<mount id="85" clientid="762" name="Nightstinger" speed="20" premium="no" />
<mount id="86" clientid="763" name="Razorcreep" speed="20" premium="no" />
<mount id="87" clientid="848" name="Rift Runner" speed="20" premium="yes" />
<mount id="88" clientid="849" name="Nightdweller" speed="20" premium="no" />
<mount id="89" clientid="850" name="Frostflare" speed="20" premium="no" />
<mount id="90" clientid="851" name="Cinderhoof" speed="20" premium="no" />
<mount id="91" clientid="868" name="Mouldpincer" speed="20" premium="no" />
<mount id="92" clientid="869" name="Bloodcurl" speed="20" premium="no" />
<mount id="93" clientid="870" name="Leafscuttler" speed="20" premium="no" />
<mount id="94" clientid="883" name="Sparkion" speed="20" premium="yes" />
<mount id="95" clientid="886" name="Swamp Snapper" speed="20" premium="no" />
<mount id="96" clientid="887" name="Mould Shell" speed="20" premium="no" />
<mount id="97" clientid="888" name="Reed Lurker" speed="20" premium="no" />
<mount id="98" clientid="889" name="Neon Sparkid" speed="20" premium="yes" />
<mount id="99" clientid="890" name="Vortexion" speed="20" premium="yes" />
<mount id="100" clientid="901" name="Ivory Fang" speed="20" premium="no" />
<mount id="101" clientid="902" name="Shadow Claw" speed="20" premium="no" />
<mount id="102" clientid="903" name="Snow Pelt" speed="20" premium="no" /> -->
</mounts>

4
data/XML/shaders.xml Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<shaders>
<shader id="1" name="Rainbow Outfit" premium="no" />
</shaders>

6
data/XML/wings.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<wings>
<!-- <wing id="1" clientid="368" name="Widow Queen" speed="20" />
<wing id="2" clientid="369" name="Racing Bird" speed="20" />
<wing id="3" clientid="370" name="War Bear" speed="20" /> -->
</wings>

View File

@ -16,4 +16,6 @@
<!-- Svargrond Arena: Killing a boss -->
<event type="kill" name="SvargrondArenaKill" script="arena_kill.lua" />
<event type="extendedopcode" name="Shop" script="shop.lua" />
</creaturescripts>

View File

@ -324,6 +324,7 @@ function onLogin(player)
player:registerEvent("InquisitionUngreez")
player:registerEvent("InquisitionBosses")
player:registerEvent("SvargrondArenaKill")
player:registerEvent("Shop")
return true
end

View File

@ -0,0 +1,362 @@
-- BETA VERSION, net tested yet
-- Instruction:
-- creaturescripts.xml <event type="extendedopcode" name="Shop" script="shop.lua" />
-- and in login.lua player:registerEvent("Shop")
-- create sql table shop_history
-- set variables
-- set up function init(), add there items and categories, follow examples
-- set up callbacks at the bottom to add player item/outfit/whatever you want
local SHOP_EXTENDED_OPCODE = 201
local SHOP_OFFERS = {}
local SHOP_CALLBACKS = {}
local SHOP_CATEGORIES = nil
local SHOP_BUY_URL = "http://otland.net" -- can be empty
local SHOP_AD = { -- can be nil
image = "https://s3.envato.com/files/62273611/PNG%20Blue/Banner%20blue%20468x60.png",
url = "http://otclient.ovh",
text = ""
}
local MAX_PACKET_SIZE = 50000
--[[ SQL TABLE
CREATE TABLE `shop_history` (
`id` int(11) NOT NULL,
`account` int(11) NOT NULL,
`player` int(11) NOT NULL,
`date` datetime NOT NULL,
`title` varchar(100) NOT NULL,
`cost` int(11) NOT NULL,
`details` varchar(500) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `shop_history`
ADD PRIMARY KEY (`id`);
ALTER TABLE `shop_history`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
]]--
function init()
-- print(json.encode(g_game.getLocalPlayer():getOutfit())) -- in console in otclient, will print current outfit and mount
SHOP_CATEGORIES = {}
local category1 = addCategory({
type="item",
item=ItemType(6561):getId(),
count=1,
name="Items"
})
local category2 = addCategory({
type="outfit",
name="Outfits",
outfit={
mount=0,
feet=0,
legs=0,
body=0,
type=136,
auxType=0,
addons=0,
head=0,
rotating=false
}
})
local category3 = addCategory({
type="outfit",
name="Mounts",
outfit={
mount=0,
feet=0,
legs=0,
body=0,
type=368,
auxType=0,
addons=0,
head=0,
rotating=false
}
})
local category4 = addCategory({
type="item",
item=ItemType(5919):getId(),
count=1,
name="Addon Items"
})
local category5 = addCategory({
type="item",
item=ItemType(4835):getId(),
count=1,
name="Quest Items"
})
local category6 = addCategory({
type="item",
item=ItemType(3734):getId(),
count=1,
name="Decorations"
})
-- local category4 = addCategory({
-- type="image",
-- image="/data/images/game/states/electrified.png",
-- name="Category with local image"
-- })
category1.addItem(50, 6561, 1, "Ceremonial Ankh", "gives you all blessings")
category1.addItem(20, 5908, 1, "Obsidian Knife", "")
category1.addItem(200, 5797, 1, "Dice", "")
category1.addItem(100, 6549, 1, "Green Djinn Access", "The magical powder will bless you with the power to convince the green djinns")
category1.addItem(100, 6551, 1, "Blue Djinn Access", "The magical powder will bless you with the power to convince the blue djinns")
category1.addItem(100, 3252, 1, "Postman Access", "The magical horn will grant you the trustworthy postman rank")
category2.addOutfit(150, {
mount=0,
feet=0,
legs=0,
body=0,
type=162,
auxType=0,
addons=0,
head=0,
rotating=true
}, "Monk", "")
category3.addOutfit(75, {
mount=1,
feet=0,
legs=0,
body=0,
type=368,
auxType=0,
addons=0,
head=0,
rotating=true
}, "Widow Queen", "")
category4.addItem(150, 5919, 1, "Dragon Claw", "It is the claw of Demodras")
category5.addItem(30, 4835, 1, "Snake Destroyer", "")
category6.addItem(10, 3734, 10, "10x Blood Herb", "")
--category4.addImage(10000, "/data/images/game/states/haste.png", "Offer with local image", "another local image\n/data/images/game/states/haste.png")
--category4.addImage(10000, "http://otclient.ovh/images/freezing.png", "Offer with remote image and custom buy action", "blalasdasd image\nhttp://otclient.ovh/images/freezing.png", customImageBuyAction)
end
function addCategory(data)
data['offers'] = {}
table.insert(SHOP_CATEGORIES, data)
table.insert(SHOP_CALLBACKS, {})
local index = #SHOP_CATEGORIES
return {
addItem = function(cost, itemId, count, title, description, callback)
if not callback then
callback = defaultItemBuyAction
end
table.insert(SHOP_CATEGORIES[index]['offers'], {
cost=cost,
type="item",
item=ItemType(itemId):getId(), -- displayed
itemId=itemId,
count=count,
title=title,
description=description
})
table.insert(SHOP_CALLBACKS[index], callback)
end,
addOutfit = function(cost, outfit, title, description, callback)
if not callback then
callback = defaultOutfitBuyAction
end
table.insert(SHOP_CATEGORIES[index]['offers'], {
cost=cost,
type="outfit",
outfit=outfit,
title=title,
description=description
})
table.insert(SHOP_CALLBACKS[index], callback)
end,
addImage = function(cost, image, title, description, callback)
if not callback then
callback = defaultImageBuyAction
end
table.insert(SHOP_CATEGORIES[index]['offers'], {
cost=cost,
type="image",
image=image,
title=title,
description=description
})
table.insert(SHOP_CALLBACKS[index], callback)
end
}
end
function getPoints(player)
local points = 0
local resultId = db.storeQuery("SELECT `points` FROM `znote_accounts` WHERE `id` = " .. player:getAccountId())
if resultId ~= false then
points = result.getDataInt(resultId, "points")
result.free(resultId)
end
return points
end
function getStatus(player)
local status = {
ad = SHOP_AD,
points = getPoints(player),
buyUrl = SHOP_BUY_URL
}
return status
end
function sendJSON(player, action, data, forceStatus)
local status = nil
if not player:getStorageValue(1150001) or player:getStorageValue(1150001) + 10 < os.time() or forceStatus then
status = getStatus(player)
end
player:setStorageValue(1150001, os.time())
local buffer = json.encode({action = action, data = data, status = status})
local s = {}
for i=1, #buffer, MAX_PACKET_SIZE do
s[#s+1] = buffer:sub(i,i+MAX_PACKET_SIZE - 1)
end
local msg = NetworkMessage()
if #s == 1 then
msg:addByte(50)
msg:addByte(SHOP_EXTENDED_OPCODE)
msg:addString(s[1])
msg:sendToPlayer(player)
return
end
-- split message if too big
msg:addByte(50)
msg:addByte(SHOP_EXTENDED_OPCODE)
msg:addString("S" .. s[1])
msg:sendToPlayer(player)
for i=2,#s - 1 do
msg = NetworkMessage()
msg:addByte(50)
msg:addByte(SHOP_EXTENDED_OPCODE)
msg:addString("P" .. s[i])
msg:sendToPlayer(player)
end
msg = NetworkMessage()
msg:addByte(50)
msg:addByte(SHOP_EXTENDED_OPCODE)
msg:addString("E" .. s[#s])
msg:sendToPlayer(player)
end
function sendMessage(player, title, msg, forceStatus)
sendJSON(player, "message", {title=title, msg=msg}, forceStatus)
end
function onExtendedOpcode(player, opcode, buffer)
if opcode ~= SHOP_EXTENDED_OPCODE then
return false
end
local status, json_data = pcall(function() return json.decode(buffer) end)
if not status then
return false
end
local action = json_data['action']
local data = json_data['data']
if not action or not data then
return false
end
if SHOP_CATEGORIES == nil then
init()
end
if action == 'init' then
sendJSON(player, "categories", SHOP_CATEGORIES)
elseif action == 'buy' then
processBuy(player, data)
elseif action == "history" then
sendHistory(player)
end
return true
end
function processBuy(player, data)
local categoryId = tonumber(data["category"])
local offerId = tonumber(data["offer"])
local offer = SHOP_CATEGORIES[categoryId]['offers'][offerId]
local callback = SHOP_CALLBACKS[categoryId][offerId]
if not offer or not callback or data["title"] ~= offer["title"] or data["cost"] ~= offer["cost"] then
sendJSON(player, "categories", SHOP_CATEGORIES) -- refresh categories, maybe invalid
return sendMessage(player, "Error!", "Invalid offer")
end
local points = getPoints(player)
if not offer['cost'] or offer['cost'] > points or points < 1 then
return sendMessage(player, "Error!", "You don't have enough points to buy " .. offer['title'] .."!", true)
end
local status = callback(player, offer)
if status == true then
db.query("UPDATE `znote_accounts` set `points` = `points` - " .. offer['cost'] .. " WHERE `id` = " .. player:getAccountId())
db.asyncQuery("INSERT INTO `shop_history` (`account`, `player`, `date`, `title`, `cost`, `details`) VALUES ('" .. player:getAccountId() .. "', '" .. player:getGuid() .. "', NOW(), " .. db.escapeString(offer['title']) .. ", " .. db.escapeString(offer['cost']) .. ", " .. db.escapeString(json.encode(offer)) .. ")")
return sendMessage(player, "Success!", "You bought " .. offer['title'] .."!", true)
end
if status == nil or status == false then
status = "Unknown error while buying " .. offer['title']
end
sendMessage(player, "Error!", status)
end
function sendHistory(player)
if player:getStorageValue(1150002) and player:getStorageValue(1150002) + 10 > os.time() then
return -- min 10s delay
end
player:setStorageValue(1150002, os.time())
local history = {}
local resultId = db.storeQuery("SELECT * FROM `shop_history` WHERE `account` = " .. player:getAccountId() .. " order by `id` DESC")
if resultId ~= false then
repeat
local details = result.getDataString(resultId, "details")
local status, json_data = pcall(function() return json.decode(details) end)
if not status then
json_data = {
type = "image",
title = result.getDataString(resultId, "title"),
cost = result.getDataInt(resultId, "cost")
}
end
table.insert(history, json_data)
history[#history]["description"] = "Bought on " .. result.getDataString(resultId, "date") .. " for " .. result.getDataInt(resultId, "cost") .. " points."
until not result.next(resultId)
result.free(resultId)
end
sendJSON(player, "history", history)
end
-- BUY CALLBACKS
-- May be useful: print(json.encode(offer))
function defaultItemBuyAction(player, offer)
-- todo: check if has capacity
if player:addItem(offer["itemId"], offer["count"], false) then
return true
end
return "Can't add item! Do you have enough space?"
end
function defaultOutfitBuyAction(player, offer)
return "default outfit buy action is not implemented"
end
function defaultImageBuyAction(player, offer)
return "default image buy action is not implemented"
end
function customImageBuyAction(player, offer)
return "custom image buy action is not implemented. Offer: " .. offer['title']
end

View File

@ -185,7 +185,8 @@ function Player:onLook(thing, position, distance)
if thing:isCreature() then
if thing:isPlayer() then
description = string.format("%s\nIP: %s.", description, Game.convertIpToString(thing:getIp()))
local client = thing:getClient()
description = string.format("%s\nIP: %s PING: %i FPS: %i.", description, Game.convertIpToString(thing:getIp()), client.ping, client.fps)
end
end
end
@ -208,7 +209,8 @@ function Player:onLookInBattleList(creature, distance)
)
if creature:isPlayer() then
description = string.format("%s\nIP: %s", description, Game.convertIpToString(creature:getIp()))
local client = thing:getClient()
description = string.format("%s\nIP: %s PING: %i FPS: %i.", description, Game.convertIpToString(thing:getIp()), client.ping, client.fps)
end
end
self:sendTextMessage(MESSAGE_INFO_DESCR, description)

View File

@ -12,3 +12,4 @@ dofile('data/lib/core/tile.lua')
dofile('data/lib/core/vocation.lua')
dofile('data/lib/core/guildwars.lua')
dofile('data/lib/core/svargrondArenaQuest.lua')
dofile('data/lib/core/json.lua')

399
data/lib/core/json.lua Normal file
View File

@ -0,0 +1,399 @@
-- add to lib/core, later add dofile in lib/core/core.lua
--
-- json.lua
--
-- Copyright (c) 2018 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
json = { _version = "0.1.1" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\\\",
[ "\"" ] = "\\\"",
[ "\b" ] = "\\b",
[ "\f" ] = "\\f",
[ "\n" ] = "\\n",
[ "\r" ] = "\\r",
[ "\t" ] = "\\t",
}
local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if val[1] ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local has_unicode_escape = false
local has_surrogate_escape = false
local has_escape = false
local last
for j = i + 1, #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
end
if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string")
end
if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true
else
has_unicode_escape = true
end
else
local c = string.char(x)
if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end
has_escape = true
end
last = nil
elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end
if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape)
end
if has_escape then
s = s:gsub("\\.", escape_char_map_inv)
end
return s, j + 1
else
last = x
end
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end

View File

@ -25,6 +25,16 @@ local reloadTypes = {
["monster"] = { targetType = RELOAD_TYPE_MONSTERS, name = "monsters" },
["monsters"] = { targetType = RELOAD_TYPE_MONSTERS, name = "monsters" },
["mount"] = RELOAD_TYPE_MOUNTS,
["aura"] = RELOAD_TYPE_AURAS,
["auras"] = RELOAD_TYPE_AURAS,
["wing"] = RELOAD_TYPE_WINGS,
["wings"] = RELOAD_TYPE_WINGS,
["shader"] = RELOAD_TYPE_SHADERS,
["shaders"] = RELOAD_TYPE_SHADERS,
["move"] = { targetType = RELOAD_TYPE_MOVEMENTS, name = "movements" },
["movement"] = { targetType = RELOAD_TYPE_MOVEMENTS, name = "movements" },

View File

@ -1,6 +1,7 @@
set(tfs_SRC
${CMAKE_CURRENT_LIST_DIR}/otpch.cpp
${CMAKE_CURRENT_LIST_DIR}/actions.cpp
${CMAKE_CURRENT_LIST_DIR}/auras.cpp
${CMAKE_CURRENT_LIST_DIR}/ban.cpp
${CMAKE_CURRENT_LIST_DIR}/baseevents.cpp
${CMAKE_CURRENT_LIST_DIR}/bed.cpp
@ -38,6 +39,7 @@ set(tfs_SRC
${CMAKE_CURRENT_LIST_DIR}/map.cpp
${CMAKE_CURRENT_LIST_DIR}/monster.cpp
${CMAKE_CURRENT_LIST_DIR}/monsters.cpp
${CMAKE_CURRENT_LIST_DIR}/mounts.cpp
${CMAKE_CURRENT_LIST_DIR}/movement.cpp
${CMAKE_CURRENT_LIST_DIR}/networkmessage.cpp
${CMAKE_CURRENT_LIST_DIR}/npc.cpp
@ -56,6 +58,7 @@ set(tfs_SRC
${CMAKE_CURRENT_LIST_DIR}/scheduler.cpp
${CMAKE_CURRENT_LIST_DIR}/scriptmanager.cpp
${CMAKE_CURRENT_LIST_DIR}/server.cpp
${CMAKE_CURRENT_LIST_DIR}/shaders.cpp
${CMAKE_CURRENT_LIST_DIR}/spawn.cpp
${CMAKE_CURRENT_LIST_DIR}/spells.cpp
${CMAKE_CURRENT_LIST_DIR}/script.cpp
@ -68,6 +71,7 @@ set(tfs_SRC
${CMAKE_CURRENT_LIST_DIR}/vocation.cpp
${CMAKE_CURRENT_LIST_DIR}/waitlist.cpp
${CMAKE_CURRENT_LIST_DIR}/wildcardtree.cpp
${CMAKE_CURRENT_LIST_DIR}/wings.cpp
${CMAKE_CURRENT_LIST_DIR}/xtea.cpp
${CMAKE_CURRENT_LIST_DIR}/quests.cpp
PARENT_SCOPE)

View File

@ -324,7 +324,6 @@ ReturnValue Actions::internalUseItem(Player* player, const Position& pos, uint8_
bool Actions::useItem(Player* player, const Position& pos, uint8_t index, Item* item, bool isHotkey)
{
player->setNextAction(OTSYS_TIME() + g_config.getNumber(ConfigManager::ACTIONS_DELAY_INTERVAL));
player->stopWalk();
if (item->getID() == ITEM_MARKET)
{
@ -360,7 +359,6 @@ bool Actions::useItemEx(Player* player, const Position& fromPos, const Position&
uint8_t toStackPos, Item* item, bool isHotkey, Creature* creature/* = nullptr*/)
{
player->setNextAction(OTSYS_TIME() + g_config.getNumber(ConfigManager::EX_ACTIONS_DELAY_INTERVAL));
player->stopWalk();
Action* action = getAction(item);
if (!action) {

63
src/auras.cpp Normal file
View File

@ -0,0 +1,63 @@
#include "otpch.h"
#include "auras.h"
#include "pugicast.h"
#include "tools.h"
bool Auras::reload()
{
auras.clear();
return loadFromXml();
}
bool Auras::loadFromXml()
{
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file("data/XML/auras.xml");
if (!result) {
printXMLError("Error - Auras::loadFromXml", "data/XML/auras.xml", result);
return false;
}
for (auto auraNode : doc.child("auras").children()) {
auras.emplace_back(
static_cast<uint8_t>(pugi::cast<uint16_t>(auraNode.attribute("id").value())),
pugi::cast<uint16_t>(auraNode.attribute("clientid").value()),
auraNode.attribute("name").as_string(),
pugi::cast<int32_t>(auraNode.attribute("speed").value()),
auraNode.attribute("premium").as_bool()
);
}
auras.shrink_to_fit();
return true;
}
Aura* Auras::getAuraByID(uint8_t id)
{
auto it = std::find_if(auras.begin(), auras.end(), [id](const Aura& aura) {
return aura.id == id;
});
return it != auras.end() ? &*it : nullptr;
}
Aura* Auras::getAuraByName(const std::string& name) {
auto auraName = name.c_str();
for (auto& it : auras) {
if (strcasecmp(auraName, it.name.c_str()) == 0) {
return &it;
}
}
return nullptr;
}
Aura* Auras::getAuraByClientID(uint16_t clientId)
{
auto it = std::find_if(auras.begin(), auras.end(), [clientId](const Aura& aura) {
return aura.clientId == clientId;
});
return it != auras.end() ? &*it : nullptr;
}

33
src/auras.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef FS_AURAS_H
#define FS_AURAS_H
struct Aura
{
Aura(uint8_t id, uint16_t clientId, std::string name, int32_t speed, bool premium) :
name(std::move(name)), speed(speed), clientId(clientId), id(id), premium(premium) {}
std::string name;
int32_t speed;
uint16_t clientId;
uint8_t id;
bool premium;
};
class Auras
{
public:
bool reload();
bool loadFromXml();
Aura* getAuraByID(uint8_t id);
Aura* getAuraByName(const std::string& name);
Aura* getAuraByClientID(uint16_t clientId);
const std::vector<Aura>& getAuras() const {
return auras;
}
private:
std::vector<Aura> auras;
};
#endif

View File

@ -87,6 +87,7 @@ bool ConfigManager::load()
boolean[ROPE_SPOT_BLOCK] = getGlobalBoolean(L, "ropeSpotBlock", false);
boolean[DROP_ITEMS] = getGlobalBoolean(L, "dropItems", false);
boolean[DISTANCE_WEAPONS_DROP_ON_GROUND] = getGlobalBoolean(L, "distanceWeaponsDropOnGround", true);
boolean[PACKET_COMPRESSION] = getGlobalBoolean(L, "packetCompression", true);
string[DEFAULT_PRIORITY] = getGlobalString(L, "defaultPriority", "high");
string[SERVER_NAME] = getGlobalString(L, "serverName", "");

View File

@ -53,6 +53,7 @@ class ConfigManager
DISTANCE_WEAPONS_DROP_ON_GROUND,
CORPSE_OWNER_ENABLED,
ROPE_SPOT_BLOCK,
PACKET_COMPRESSION,
LAST_BOOLEAN_CONFIG /* this must be the last one */
};

View File

@ -20,7 +20,7 @@
#ifndef FS_CONST_H_0A49B5996F074465BF44B90F4F780E8B
#define FS_CONST_H_0A49B5996F074465BF44B90F4F780E8B
static constexpr int32_t NETWORKMESSAGE_MAXSIZE = 24590;
static constexpr int32_t NETWORKMESSAGE_MAXSIZE = 65500;
static constexpr int32_t MIN_MARKET_FEE = 20;
static constexpr int32_t MAX_MARKET_FEE = 100000;
@ -378,6 +378,7 @@ enum PlayerFlags : uint64_t {
enum ReloadTypes_t : uint8_t {
RELOAD_TYPE_ALL,
RELOAD_TYPE_ACTIONS,
RELOAD_TYPE_AURAS,
RELOAD_TYPE_CHAT,
RELOAD_TYPE_COMMANDS,
RELOAD_TYPE_CONFIG,
@ -393,8 +394,129 @@ enum ReloadTypes_t : uint8_t {
RELOAD_TYPE_QUESTS,
RELOAD_TYPE_RAIDS,
RELOAD_TYPE_SPELLS,
RELOAD_TYPE_SHADERS,
RELOAD_TYPE_TALKACTIONS,
RELOAD_TYPE_WEAPONS,
RELOAD_TYPE_WINGS,
};
// OTCv8 features (from src/client/const.h)
enum GameFeature {
GameProtocolChecksum = 1,
GameAccountNames = 2,
GameChallengeOnLogin = 3,
GamePenalityOnDeath = 4,
GameNameOnNpcTrade = 5,
GameDoubleFreeCapacity = 6,
GameDoubleExperience = 7,
GameTotalCapacity = 8,
GameSkillsBase = 9,
GamePlayerRegenerationTime = 10,
GameChannelPlayerList = 11,
GamePlayerMounts = 12,
GameEnvironmentEffect = 13,
GameCreatureEmblems = 14,
GameItemAnimationPhase = 15,
GameMagicEffectU16 = 16,
GamePlayerMarket = 17,
GameSpritesU32 = 18,
GameTileAddThingWithStackpos = 19,
GameOfflineTrainingTime = 20,
GamePurseSlot = 21,
GameFormatCreatureName = 22,
GameSpellList = 23,
GameClientPing = 24,
GameExtendedClientPing = 25,
GameDoubleHealth = 28,
GameDoubleSkills = 29,
GameChangeMapAwareRange = 30,
GameMapMovePosition = 31,
GameAttackSeq = 32,
GameBlueNpcNameColor = 33,
GameDiagonalAnimatedText = 34,
GameLoginPending = 35,
GameNewSpeedLaw = 36,
GameForceFirstAutoWalkStep = 37,
GameMinimapRemove = 38,
GameDoubleShopSellAmount = 39,
GameContainerPagination = 40,
GameThingMarks = 41,
GameLooktypeU16 = 42,
GamePlayerStamina = 43,
GamePlayerAddons = 44,
GameMessageStatements = 45,
GameMessageLevel = 46,
GameNewFluids = 47,
GamePlayerStateU16 = 48,
GameNewOutfitProtocol = 49,
GamePVPMode = 50,
GameWritableDate = 51,
GameAdditionalVipInfo = 52,
GameBaseSkillU16 = 53,
GameCreatureIcons = 54,
GameHideNpcNames = 55,
GameSpritesAlphaChannel = 56,
GamePremiumExpiration = 57,
GameBrowseField = 58,
GameEnhancedAnimations = 59,
GameOGLInformation = 60,
GameMessageSizeCheck = 61,
GamePreviewState = 62,
GameLoginPacketEncryption = 63,
GameClientVersion = 64,
GameContentRevision = 65,
GameExperienceBonus = 66,
GameAuthenticator = 67,
GameUnjustifiedPoints = 68,
GameSessionKey = 69,
GameDeathType = 70,
GameIdleAnimations = 71,
GameKeepUnawareTiles = 72,
GameIngameStore = 73,
GameIngameStoreHighlights = 74,
GameIngameStoreServiceType = 75,
GameAdditionalSkills = 76,
GameDistanceEffectU16 = 77,
GamePrey = 78,
GameDoubleMagicLevel = 79,
GameExtendedOpcode = 80,
GameMinimapLimitedToSingleFloor = 81,
GameSendWorldName = 82,
GameDoubleLevel = 83,
GameDoubleSoul = 84,
GameDoublePlayerGoodsMoney = 85,
GameCreatureWalkthrough = 86,
GameDoubleTradeMoney = 87,
GameSequencedPackets = 88,
GameTibia12Protocol = 89,
// 90-99 otclientv8 features
GameNewWalking = 90,
GameSlowerManualWalking = 91,
GameItemTooltip = 93,
GameBot = 95,
GameBiggerMapCache = 96,
GameForceLight = 97,
GameNoDebug = 98,
GameBotProtection = 99,
// Custom features for customer
GameFasterAnimations = 101,
GameCenteredOutfits = 102,
GameSendIdentifiers = 103,
GameWingsAndAura = 104,
GamePlayerStateU32 = 105,
GameOutfitShaders = 106,
// advanced features
GamePacketSizeU32 = 110,
GamePacketCompression = 111,
LastGameFeature = 120
};
enum ClientVersion_t : uint16_t {
@ -417,6 +539,22 @@ static constexpr int32_t PSTRG_RESERVED_RANGE_SIZE = 10000000;
//[1000 - 1500];
static constexpr int32_t PSTRG_OUTFITS_RANGE_START = (PSTRG_RESERVED_RANGE_START + 1000);
static constexpr int32_t PSTRG_OUTFITS_RANGE_SIZE = 500;
//[2001 - 2011];
static constexpr int32_t PSTRG_MOUNTS_RANGE_START = (PSTRG_RESERVED_RANGE_START + 2001);
static constexpr int32_t PSTRG_MOUNTS_RANGE_SIZE = 10;
static constexpr int32_t PSTRG_MOUNTS_CURRENTMOUNT = (PSTRG_MOUNTS_RANGE_START + 10);
//[2012 - 2022];
static constexpr int32_t PSTRG_WINGS_RANGE_START = (PSTRG_RESERVED_RANGE_START + 2012);
static constexpr int32_t PSTRG_WINGS_RANGE_SIZE = 10;
static constexpr int32_t PSTRG_WINGS_CURRENTWINGS = (PSTRG_WINGS_RANGE_START + 10);
//[2023 - 2033];
static constexpr int32_t PSTRG_AURAS_RANGE_START = (PSTRG_RESERVED_RANGE_START + 2023);
static constexpr int32_t PSTRG_AURAS_RANGE_SIZE = 10;
static constexpr int32_t PSTRG_AURAS_CURRENTAURA = (PSTRG_AURAS_RANGE_START + 10);
//[2034 - 2044];
static constexpr int32_t PSTRG_SHADERS_RANGE_START = (PSTRG_RESERVED_RANGE_START + 2034);
static constexpr int32_t PSTRG_SHADERS_RANGE_SIZE = 10;
static constexpr int32_t PSTRG_SHADERS_CURRENTSHADER = (PSTRG_SHADERS_RANGE_START + 10);
#define IS_IN_KEYRANGE(key, range) (key >= PSTRG_##range##_START && ((key - PSTRG_##range##_START) <= PSTRG_##range##_SIZE))

View File

@ -56,6 +56,7 @@ enum itemAttrTypes : uint32_t {
ITEM_ATTRIBUTE_DOORQUESTVALUE = 1 << 24,
ITEM_ATTRIBUTE_DOORLEVEL = 1 << 25,
ITEM_ATTRIBUTE_CHESTQUESTNUMBER = 1 << 26,
ITEM_ATTRIBUTE_AUTOOPEN = 1 << 27,
};
enum VipStatus_t : uint8_t {
@ -93,6 +94,16 @@ enum OperatingSystem_t : uint8_t {
CLIENTOS_OTCLIENT_LINUX = 10,
CLIENTOS_OTCLIENT_WINDOWS = 11,
CLIENTOS_OTCLIENT_MAC = 12,
// by default OTCv8 uses CLIENTOS_WINDOWS for backward compatibility
// for correct value enable g_game.enableFeature(GameExtendedOpcode)
// in modules/game_features/features.lua
CLIENTOS_OTCLIENTV8_LINUX = 20,
CLIENTOS_OTCLIENTV8_WINDOWS = 21,
CLIENTOS_OTCLIENTV8_MAC = 22,
CLIENTOS_OTCLIENTV8_ANDROID = 23,
CLIENTOS_OTCLIENTV8_IOS = 24,
CLIENTOS_OTCLIENTV8_WEB = 25
};
enum AccountType_t : uint8_t {
@ -379,6 +390,10 @@ enum ReturnValue {
struct Outfit_t {
uint16_t lookType = 0;
uint16_t lookTypeEx = 0;
uint16_t lookMount = 0;
uint16_t lookWings = 0;
uint16_t lookAura = 0;
uint16_t lookShader = 0;
uint8_t lookHead = 0;
uint8_t lookBody = 0;
uint8_t lookLegs = 0;

View File

@ -106,6 +106,10 @@ void Game::setGameState(GameState_t newState)
raids.startup();
quests.loadFromXml();
mounts.loadFromXml();
auras.loadFromXml();
wings.loadFromXml();
shaders.loadFromXml();
loadMotdNum();
loadPlayersRecord();
@ -1866,6 +1870,18 @@ void Game::playerReceivePingBack(uint32_t playerId)
player->sendPingBack();
}
void Game::playerReceiveNewPing(uint32_t playerId, uint16_t ping, uint16_t fps)
{
Player* player = getPlayerByID(playerId);
if (!player) {
return;
}
player->receivePing();
player->setLocalPing(ping);
player->setFPS(fps);
}
void Game::playerAutoWalk(uint32_t playerId, const std::forward_list<Direction>& listDir)
{
Player* player = getPlayerByID(playerId);
@ -2906,6 +2922,17 @@ void Game::playerRequestOutfit(uint32_t playerId)
player->sendOutfitWindow();
}
void Game::playerToggleOutfitExtension(uint32_t playerId, int mount, int wings, int aura, int shader)
{
Player* player = getPlayerByID(playerId);
if (!player) {
return;
}
if (mount != -1)
player->toggleMount(mount == 1);
}
void Game::playerChangeOutfit(uint32_t playerId, Outfit_t outfit)
{
if (!g_config.getBoolean(ConfigManager::ALLOW_CHANGEOUTFIT)) {
@ -2917,6 +2944,74 @@ void Game::playerChangeOutfit(uint32_t playerId, Outfit_t outfit)
return;
}
const Outfit* playerOutfit = Outfits::getInstance().getOutfitByLookType(player->getSex(), outfit.lookType);
if (!playerOutfit) {
outfit.lookMount = 0;
outfit.lookWings = 0;
outfit.lookAura = 0;
outfit.lookShader = 0;
}
if (outfit.lookMount != 0) {
Mount* mount = mounts.getMountByClientID(outfit.lookMount);
if (!mount) {
return;
}
if (!player->hasMount(mount)) {
return;
}
if (player->isMounted()) {
Mount* prevMount = mounts.getMountByID(player->getCurrentMount());
if (prevMount) {
changeSpeed(player, mount->speed - prevMount->speed);
}
player->setCurrentMount(mount->id);
}
else {
player->setCurrentMount(mount->id);
outfit.lookMount = 0;
}
}
else if (player->isMounted()) {
player->dismount();
}
if (outfit.lookWings != 0) {
Wing* wing = wings.getWingByID(outfit.lookWings);
if (!wing) {
return;
}
if (!player->hasWing(wing)) {
return;
}
}
if (outfit.lookAura != 0) {
Aura* aura = auras.getAuraByID(outfit.lookAura);
if (!aura) {
return;
}
if (!player->hasAura(aura)) {
return;
}
}
if (outfit.lookShader) {
Shader* shader = shaders.getShaderByID(outfit.lookShader);
if (!shader) {
return;
}
if (!player->hasShader(shader)) {
return;
}
}
if (player->canWear(outfit.lookType, outfit.lookAddons)) {
player->defaultOutfit = outfit;
@ -5099,6 +5194,7 @@ bool Game::reload(ReloadTypes_t reloadType)
{
switch (reloadType) {
case RELOAD_TYPE_ACTIONS: return g_actions->reload();
case RELOAD_TYPE_AURAS: return auras.reload();
case RELOAD_TYPE_CHAT: return g_chat->load();
case RELOAD_TYPE_CONFIG: return g_config.reload();
case RELOAD_TYPE_CREATURESCRIPTS: return g_creatureEvents->reload();
@ -5106,6 +5202,7 @@ bool Game::reload(ReloadTypes_t reloadType)
case RELOAD_TYPE_GLOBALEVENTS: return g_globalEvents->reload();
case RELOAD_TYPE_ITEMS: return Item::items.reload();
case RELOAD_TYPE_MONSTERS: return g_monsters.reload();
case RELOAD_TYPE_MOUNTS: return mounts.reload();
case RELOAD_TYPE_MOVEMENTS: return g_moveEvents->reload();
case RELOAD_TYPE_NPCS: {
Npcs::reload();
@ -5114,7 +5211,7 @@ bool Game::reload(ReloadTypes_t reloadType)
case RELOAD_TYPE_QUESTS: return quests.reload();
case RELOAD_TYPE_RAIDS: return raids.reload() && raids.startup();
case RELOAD_TYPE_SHADERS: return shaders.reload();
case RELOAD_TYPE_SPELLS: {
if (!g_spells->reload()) {
std::cout << "[Error - Game::reload] Failed to reload spells." << std::endl;
@ -5128,6 +5225,7 @@ bool Game::reload(ReloadTypes_t reloadType)
}
case RELOAD_TYPE_TALKACTIONS: return g_talkActions->reload();
case RELOAD_TYPE_WINGS: return wings.reload();
default: {
if (!g_spells->reload()) {
@ -5151,6 +5249,10 @@ bool Game::reload(ReloadTypes_t reloadType)
g_talkActions->reload();
Item::items.reload();
quests.reload();
auras.reload();
mounts.reload();
wings.reload();
shaders.reload();
g_globalEvents->reload();
g_events->load();
g_chat->load();
@ -5158,3 +5260,14 @@ bool Game::reload(ReloadTypes_t reloadType)
}
}
}
void Game::startProgressbar(Creature* creature, uint32_t duration, bool ltr)
{
SpectatorVec spectators;
map.getSpectators(spectators, creature->getPosition(), false, true);
for (Creature* spectator : spectators) {
if (Player* tmpPlayer = spectator->getPlayer()) {
tmpPlayer->sendProgressbar(creature->getID(), duration, ltr);
}
}
}

View File

@ -32,6 +32,7 @@
#include "npc.h"
#include "wildcardtree.h"
#include "quests.h"
#include "shaders.h"
class ServiceManager;
class Creature;
@ -360,6 +361,7 @@ class Game
void playerOpenPrivateChannel(uint32_t playerId, std::string& receiver);
void playerReceivePing(uint32_t playerId);
void playerReceivePingBack(uint32_t playerId);
void playerReceiveNewPing(uint32_t playerId, uint16_t ping, uint16_t fps);
void playerAutoWalk(uint32_t playerId, const std::forward_list<Direction>& listDir);
void playerStopAutoWalk(uint32_t playerId);
void playerUseItemEx(uint32_t playerId, const Position& fromPos, uint8_t fromStackPos,
@ -399,6 +401,7 @@ class Game
void playerPassPartyLeadership(uint32_t playerId, uint32_t newLeaderId);
void playerLeaveParty(uint32_t playerId);
void playerEnableSharedPartyExperience(uint32_t playerId, bool sharedExpActive);
void playerToggleOutfitExtension(uint32_t playerId, int mount, int wings, int aura, int shader);
void playerProcessRuleViolationReport(uint32_t playerId, const std::string& name);
void playerCloseRuleViolationReport(uint32_t playerId, const std::string& name);
void playerCancelRuleViolationReport(uint32_t playerId);
@ -503,10 +506,16 @@ class Game
void setBedSleeper(BedItem* bed, uint32_t guid);
void removeBedSleeper(uint32_t guid);
bool reload(ReloadTypes_t reloadType);
void startProgressbar(Creature* creature, uint32_t duration, bool ltr = true);
Auras auras;
Groups groups;
Map map;
Mounts mounts;
Raids raids;
Quests quests;
Wings wings;
Shaders shaders;
protected:
bool playerSaySpell(Player* player, SpeakClasses type, const std::string& text);

View File

@ -525,7 +525,7 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result)
return true;
}
bool IOLoginData::saveItems(const Player* player, const ItemBlockList& itemList, DBInsert& query_insert, PropWriteStream& propWriteStream)
bool IOLoginData::saveItems(const Player* player, const ItemBlockList& itemList, DBInsert& query_insert, PropWriteStream& propWriteStream, std::map<Container*, int>& openContainers)
{
std::ostringstream ss;
@ -540,6 +540,17 @@ bool IOLoginData::saveItems(const Player* player, const ItemBlockList& itemList,
Item* item = it.second;
++runningId;
if (Container* container = item->getContainer()) {
auto it = openContainers.find(container);
if (it == openContainers.end()) {
container->resetAutoOpen();
}
else {
container->setAutoOpen(it->second);
}
queue.emplace_back(container, runningId);
}
propWriteStream.clear();
item->serializeAttr(propWriteStream);
@ -550,10 +561,6 @@ bool IOLoginData::saveItems(const Player* player, const ItemBlockList& itemList,
if (!query_insert.addRow(ss)) {
return false;
}
if (Container* container = item->getContainer()) {
queue.emplace_back(container, runningId);
}
}
while (!queue.empty()) {
@ -567,6 +574,14 @@ bool IOLoginData::saveItems(const Player* player, const ItemBlockList& itemList,
Container* subContainer = item->getContainer();
if (subContainer) {
auto it = openContainers.find(subContainer);
if (it == openContainers.end()) {
subContainer->resetAutoOpen();
}
else {
subContainer->setAutoOpen(it->second);
}
queue.emplace_back(subContainer, runningId);
}
@ -749,6 +764,11 @@ bool IOLoginData::savePlayer(Player* player)
//item saving
query.str(std::string());
std::map<Container*, int> openContainers;
for (auto container : player->getOpenContainers()) {
if (!container.second.container) continue;
openContainers[container.second.container] = container.first;
}
query << "DELETE FROM `player_items` WHERE `player_id` = " << player->getGUID();
if (!db->executeQuery(query.str())) {
return false;
@ -764,7 +784,7 @@ bool IOLoginData::savePlayer(Player* player)
}
}
if (!saveItems(player, itemList, itemsQuery, propWriteStream)) {
if (!saveItems(player, itemList, itemsQuery, propWriteStream, openContainers)) {
return false;
}
@ -783,7 +803,7 @@ bool IOLoginData::savePlayer(Player* player)
itemList.emplace_back(it.first, it.second);
}
if (!saveItems(player, itemList, depotQuery, propWriteStream)) {
if (!saveItems(player, itemList, depotQuery, propWriteStream, openContainers)) {
return false;
}

View File

@ -65,7 +65,7 @@ class IOLoginData
typedef std::map<uint32_t, std::pair<Item*, uint32_t>> ItemMap;
static void loadItems(ItemMap& itemMap, DBResult_ptr result);
static bool saveItems(const Player* player, const ItemBlockList& itemList, DBInsert& query_insert, PropWriteStream& stream);
static bool saveItems(const Player* player, const ItemBlockList& itemList, DBInsert& query_insert, PropWriteStream& stream, std::map<Container*, int>& openContainers);
};
#endif

View File

@ -551,6 +551,17 @@ Attr_ReadValue Item::readAttr(AttrTypes_t attr, PropStream& propStream)
break;
}
case ATTR_AUTOOPEN:
{
int8_t autoOpen;
if (!propStream.read<int8_t>(autoOpen)) {
return ATTR_READ_ERROR;
}
setIntAttr(ITEM_ATTRIBUTE_AUTOOPEN, autoOpen);
break;
}
case ATTR_KEYNUMBER: {
uint16_t keyNumber;
if (!propStream.read<uint16_t>(keyNumber)) {
@ -788,6 +799,11 @@ void Item::serializeAttr(PropWriteStream& propWriteStream) const
propWriteStream.write<uint8_t>(getIntAttr(ITEM_ATTRIBUTE_SHOOTRANGE));
}
if (hasAttribute(ITEM_ATTRIBUTE_AUTOOPEN)) {
propWriteStream.write<uint8_t>(ATTR_AUTOOPEN);
propWriteStream.write<int8_t>(getIntAttr(ITEM_ATTRIBUTE_AUTOOPEN));
}
if (hasAttribute(ITEM_ATTRIBUTE_KEYNUMBER)) {
propWriteStream.write<uint8_t>(ATTR_KEYNUMBER);
propWriteStream.write<uint16_t>(getIntAttr(ITEM_ATTRIBUTE_KEYNUMBER));

View File

@ -103,6 +103,7 @@ enum AttrTypes_t {
ATTR_DEFENSE = 35,
ATTR_ARMOR = 36,
ATTR_SHOOTRANGE = 37,
ATTR_AUTOOPEN = 38
};
enum Attr_ReadValue {
@ -818,6 +819,22 @@ class Item : virtual public Thing
return !parent || parent->isRemoved();
}
int8_t getAutoOpen()
{
if (hasAttribute(ITEM_ATTRIBUTE_AUTOOPEN)) {
return getIntAttr(ITEM_ATTRIBUTE_AUTOOPEN);
}
return -1;
}
void setAutoOpen(int8_t value)
{
setIntAttr(ITEM_ATTRIBUTE_AUTOOPEN, value);
}
void resetAutoOpen()
{
removeAttribute(ITEM_ATTRIBUTE_AUTOOPEN);
}
protected:
std::string getWeightDescription(uint32_t weight) const;

View File

@ -731,6 +731,9 @@ Position LuaScriptInterface::getPosition(lua_State* L, int32_t arg)
Outfit_t LuaScriptInterface::getOutfit(lua_State* L, int32_t arg)
{
Outfit_t outfit;
outfit.lookMount = getField<uint16_t>(L, arg, "lookMount");
outfit.lookWings = getField<uint16_t>(L, arg, "lookWings");
outfit.lookAura = getField<uint16_t>(L, arg, "lookAura");
outfit.lookAddons = getField<uint8_t>(L, arg, "lookAddons");
outfit.lookFeet = getField<uint8_t>(L, arg, "lookFeet");
@ -741,7 +744,9 @@ Outfit_t LuaScriptInterface::getOutfit(lua_State* L, int32_t arg)
outfit.lookTypeEx = getField<uint8_t>(L, arg, "lookTypeEx");
outfit.lookType = getField<uint8_t>(L, arg, "lookType");
lua_pop(L, 6);
outfit.lookShader = getField<uint16_t>(L, arg, "lookShader");
lua_pop(L, 8);
return outfit;
}
@ -878,7 +883,7 @@ void LuaScriptInterface::pushPosition(lua_State* L, const Position& position, in
void LuaScriptInterface::pushOutfit(lua_State* L, const Outfit_t& outfit)
{
lua_createtable(L, 0, 6);
lua_createtable(L, 0, 8);
setField(L, "lookType", outfit.lookType);
setField(L, "lookTypeEx", outfit.lookTypeEx);
setField(L, "lookHead", outfit.lookHead);
@ -886,6 +891,10 @@ void LuaScriptInterface::pushOutfit(lua_State* L, const Outfit_t& outfit)
setField(L, "lookLegs", outfit.lookLegs);
setField(L, "lookFeet", outfit.lookFeet);
setField(L, "lookAddons", outfit.lookAddons);
setField(L, "lookMount", outfit.lookMount);
setField(L, "lookWings", outfit.lookWings);
setField(L, "lookAura", outfit.lookAura);
setField(L, "lookShader", outfit.lookShader);
}
#define registerEnum(value) { std::string enumName = #value; registerGlobalVariable(enumName.substr(enumName.find_last_of(':') + 1), value); }
@ -1636,6 +1645,7 @@ void LuaScriptInterface::registerFunctions()
registerEnum(RELOAD_TYPE_ALL)
registerEnum(RELOAD_TYPE_ACTIONS)
registerEnum(RELOAD_TYPE_AURAS)
registerEnum(RELOAD_TYPE_CHAT)
registerEnum(RELOAD_TYPE_COMMANDS)
registerEnum(RELOAD_TYPE_CONFIG)
@ -1651,8 +1661,10 @@ void LuaScriptInterface::registerFunctions()
registerEnum(RELOAD_TYPE_QUESTS)
registerEnum(RELOAD_TYPE_RAIDS)
registerEnum(RELOAD_TYPE_SPELLS)
registerEnum(RELOAD_TYPE_SHADERS)
registerEnum(RELOAD_TYPE_TALKACTIONS)
registerEnum(RELOAD_TYPE_WEAPONS)
registerEnum(RELOAD_TYPE_WINGS)
// _G
registerGlobalVariable("INDEX_WHEREEVER", INDEX_WHEREEVER);
@ -2015,6 +2027,8 @@ void LuaScriptInterface::registerFunctions()
registerMethod("Creature", "getPathTo", LuaScriptInterface::luaCreatureGetPathTo);
registerMethod("Creature", "sendProgressbar", LuaScriptInterface::luaCreatureSetProgressbar);
// Player
registerClass("Player", "Creature", LuaScriptInterface::luaPlayerCreate);
registerMetaMethod("Player", "__eq", LuaScriptInterface::luaUserdataCompare);
@ -2138,6 +2152,10 @@ void LuaScriptInterface::registerFunctions()
registerMethod("Player", "hasOutfit", LuaScriptInterface::luaPlayerHasOutfit);
registerMethod("Player", "sendOutfitWindow", LuaScriptInterface::luaPlayerSendOutfitWindow);
registerMethod("Player", "addMount", LuaScriptInterface::luaPlayerAddMount);
registerMethod("Player", "removeMount", LuaScriptInterface::luaPlayerRemoveMount);
registerMethod("Player", "hasMount", LuaScriptInterface::luaPlayerHasMount);
registerMethod("Player", "getPremiumDays", LuaScriptInterface::luaPlayerGetPremiumDays);
registerMethod("Player", "addPremiumDays", LuaScriptInterface::luaPlayerAddPremiumDays);
registerMethod("Player", "removePremiumDays", LuaScriptInterface::luaPlayerRemovePremiumDays);
@ -8597,6 +8615,79 @@ int LuaScriptInterface::luaPlayerSendOutfitWindow(lua_State* L)
return 1;
}
int LuaScriptInterface::luaPlayerAddMount(lua_State* L) {
// player:addMount(mountId or mountName)
Player* player = getUserdata<Player>(L, 1);
if (!player) {
lua_pushnil(L);
return 1;
}
uint8_t mountId;
if (isNumber(L, 2)) {
mountId = getNumber<uint8_t>(L, 2);
}
else {
Mount* mount = g_game.mounts.getMountByName(getString(L, 2));
if (!mount) {
lua_pushnil(L);
return 1;
}
mountId = mount->id;
}
pushBoolean(L, player->tameMount(mountId));
return 1;
}
int LuaScriptInterface::luaPlayerRemoveMount(lua_State* L) {
// player:removeMount(mountId or mountName)
Player* player = getUserdata<Player>(L, 1);
if (!player) {
lua_pushnil(L);
return 1;
}
uint8_t mountId;
if (isNumber(L, 2)) {
mountId = getNumber<uint8_t>(L, 2);
}
else {
Mount* mount = g_game.mounts.getMountByName(getString(L, 2));
if (!mount) {
lua_pushnil(L);
return 1;
}
mountId = mount->id;
}
pushBoolean(L, player->untameMount(mountId));
return 1;
}
int LuaScriptInterface::luaPlayerHasMount(lua_State* L) {
// player:hasMount(mountId or mountName)
const Player* player = getUserdata<const Player>(L, 1);
if (!player) {
lua_pushnil(L);
return 1;
}
Mount* mount = nullptr;
if (isNumber(L, 2)) {
mount = g_game.mounts.getMountByID(getNumber<uint8_t>(L, 2));
}
else {
mount = g_game.mounts.getMountByName(getString(L, 2));
}
if (mount) {
pushBoolean(L, player->hasMount(mount));
}
else {
lua_pushnil(L);
}
return 1;
}
int LuaScriptInterface::luaPlayerGetPremiumDays(lua_State* L)
{
// player:getPremiumDays()
@ -8823,10 +8914,14 @@ int LuaScriptInterface::luaPlayerGetClient(lua_State* L)
// player:getClient()
Player* player = getUserdata<Player>(L, 1);
if (player) {
lua_createtable(L, 0, 2);
lua_createtable(L, 0, 5);
setField(L, "version", player->getProtocolVersion());
setField(L, "os", player->getOperatingSystem());
} else {
setField(L, "otcv8", player->getOTCv8Version());
setField(L, "ping", player->getLocalPing());
setField(L, "fps", player->getFPS());
}
else {
lua_pushnil(L);
}
return 1;
@ -11294,11 +11389,15 @@ int LuaScriptInterface::luaConditionSetSpeedDelta(lua_State* L)
int LuaScriptInterface::luaConditionSetOutfit(lua_State* L)
{
// condition:setOutfit(outfit)
// condition:setOutfit(lookTypeEx, lookType, lookHead, lookBody, lookLegs, lookFeet[, lookAddons])
// condition:setOutfit(lookTypeEx, lookType, lookHead, lookBody, lookLegs, lookFeet[, lookAddons[, lookMount, lookWings, lookAura, lookShader]])
Outfit_t outfit;
if (isTable(L, 2)) {
outfit = getOutfit(L, 2);
} else {
outfit.lookShader = getNumber<uint16_t>(L, 12, outfit.lookShader);
outfit.lookAura = getNumber<uint16_t>(L, 11, outfit.lookAura);
outfit.lookWings = getNumber<uint16_t>(L, 10, outfit.lookWings);
outfit.lookMount = getNumber<uint16_t>(L, 9, outfit.lookMount);
outfit.lookAddons = getNumber<uint8_t>(L, 8, outfit.lookAddons);
outfit.lookFeet = getNumber<uint8_t>(L, 7);
outfit.lookLegs = getNumber<uint8_t>(L, 6);
@ -12277,3 +12376,20 @@ void LuaEnvironment::executeTimerEvent(uint32_t eventIndex)
luaL_unref(luaState, LUA_REGISTRYINDEX, parameter);
}
}
int LuaScriptInterface::luaCreatureSetProgressbar(lua_State* L)
{
// creature:sendProgressbar(duration, leftToRight)
Creature* creature = getUserdata<Creature>(L, 1);
uint32_t duration = getNumber<uint32_t>(L, 2);
bool ltr = getBoolean(L, 3);
if (creature) {
g_game.startProgressbar(creature, duration, ltr);
pushBoolean(L, true);
}
else {
lua_pushnil(L);
}
return 1;
}

View File

@ -786,6 +786,8 @@ class LuaScriptInterface
static int luaCreatureGetPathTo(lua_State* L);
static int luaCreatureSetProgressbar(lua_State* L);
// Player
static int luaPlayerCreate(lua_State* L);
@ -909,6 +911,10 @@ class LuaScriptInterface
static int luaPlayerHasOutfit(lua_State* L);
static int luaPlayerSendOutfitWindow(lua_State* L);
static int luaPlayerAddMount(lua_State* L);
static int luaPlayerRemoveMount(lua_State* L);
static int luaPlayerHasMount(lua_State* L);
static int luaPlayerGetPremiumDays(lua_State* L);
static int luaPlayerAddPremiumDays(lua_State* L);
static int luaPlayerRemovePremiumDays(lua_State* L);

View File

@ -775,6 +775,23 @@ bool Monsters::loadMonster(const std::string& file, const std::string& monsterNa
std::cout << "[Warning - Monsters::loadMonster] Missing look type/typeex. " << file << std::endl;
}
if ((attr = node.attribute("mount"))) {
mType->info.outfit.lookMount = pugi::cast<uint16_t>(attr.value());
}
if ((attr = node.attribute("aura"))) {
mType->info.outfit.lookAura = pugi::cast<uint16_t>(attr.value());
}
if ((attr = node.attribute("wings"))) {
mType->info.outfit.lookWings = pugi::cast<uint16_t>(attr.value());
}
if ((attr = node.attribute("shader"))) {
Shader* shader = g_game.shaders.getShaderByName(attr.as_string());
mType->info.outfit.lookShader = shader ? shader->id : 0;
}
if ((attr = node.attribute("corpse"))) {
mType->info.lookcorpse = pugi::cast<uint16_t>(attr.value());
}

82
src/mounts.cpp Normal file
View File

@ -0,0 +1,82 @@
/**
* The Forgotten Server - a free and open-source MMORPG server emulator
* Copyright (C) 2019 Mark Samman <mark.samman@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "otpch.h"
#include "mounts.h"
#include "pugicast.h"
#include "tools.h"
bool Mounts::reload()
{
mounts.clear();
return loadFromXml();
}
bool Mounts::loadFromXml()
{
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file("data/XML/mounts.xml");
if (!result) {
printXMLError("Error - Mounts::loadFromXml", "data/XML/mounts.xml", result);
return false;
}
for (auto mountNode : doc.child("mounts").children()) {
mounts.emplace_back(
static_cast<uint8_t>(pugi::cast<uint16_t>(mountNode.attribute("id").value())),
pugi::cast<uint16_t>(mountNode.attribute("clientid").value()),
mountNode.attribute("name").as_string(),
pugi::cast<int32_t>(mountNode.attribute("speed").value()),
mountNode.attribute("premium").as_bool()
);
}
mounts.shrink_to_fit();
return true;
}
Mount* Mounts::getMountByID(uint8_t id)
{
auto it = std::find_if(mounts.begin(), mounts.end(), [id](const Mount& mount) {
return mount.id == id;
});
return it != mounts.end() ? &*it : nullptr;
}
Mount* Mounts::getMountByName(const std::string& name) {
auto mountName = name.c_str();
for (auto& it : mounts) {
if (strcasecmp(mountName, it.name.c_str()) == 0) {
return &it;
}
}
return nullptr;
}
Mount* Mounts::getMountByClientID(uint16_t clientId)
{
auto it = std::find_if(mounts.begin(), mounts.end(), [clientId](const Mount& mount) {
return mount.clientId == clientId;
});
return it != mounts.end() ? &*it : nullptr;
}

52
src/mounts.h Normal file
View File

@ -0,0 +1,52 @@
/**
* The Forgotten Server - a free and open-source MMORPG server emulator
* Copyright (C) 2019 Mark Samman <mark.samman@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef FS_MOUNTS_H_73716D11906A4C5C9F4A7B68D34C9BA6
#define FS_MOUNTS_H_73716D11906A4C5C9F4A7B68D34C9BA6
struct Mount
{
Mount(uint8_t id, uint16_t clientId, std::string name, int32_t speed, bool premium) :
name(std::move(name)), speed(speed), clientId(clientId), id(id), premium(premium) {}
std::string name;
int32_t speed;
uint16_t clientId;
uint8_t id;
bool premium;
};
class Mounts
{
public:
bool reload();
bool loadFromXml();
Mount* getMountByID(uint8_t id);
Mount* getMountByName(const std::string& name);
Mount* getMountByClientID(uint16_t clientId);
const std::vector<Mount>& getMounts() const {
return mounts;
}
private:
std::vector<Mount> mounts;
};
#endif

View File

@ -51,7 +51,7 @@ Position NetworkMessage::getPosition()
void NetworkMessage::addString(const std::string& value)
{
size_t stringLen = value.length();
if (!canAdd(stringLen + 2) || stringLen > 8192) {
if (!canAdd(stringLen + 2)) {
return;
}
@ -69,7 +69,7 @@ void NetworkMessage::addDouble(double value, uint8_t precision/* = 2*/)
void NetworkMessage::addBytes(const char* bytes, size_t size)
{
if (!canAdd(size) || size > 8192) {
if (!canAdd(size)) {
return;
}

View File

@ -43,7 +43,14 @@ public:
add_header(info.length);
}
void addCryptoHeader() {
void addCryptoHeader(bool addChecksum, bool compression = false) {
if (compression) {
add_header<uint32_t>(0);
}
else if (addChecksum) {
//add_header(adlerChecksum(buffer + outputBufferStart, info.length));
}
writeMessageLength();
}

View File

@ -528,6 +528,9 @@ void Player::addStorageValue(const uint32_t key, const int32_t value, const bool
);
return;
}
else if (IS_IN_KEYRANGE(key, MOUNTS_RANGE) || IS_IN_KEYRANGE(key, WINGS_RANGE) || IS_IN_KEYRANGE(key, AURAS_RANGE)) {
// do nothing
}
else {
std::cout << "Warning: unknown reserved key: " << key << " player: " << getName() << std::endl;
return;
@ -684,6 +687,31 @@ void Player::sendPing()
}
}
void Player::autoOpenContainers()
{
for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; i++) {
Item* item = inventory[i];
if (!item) {
continue;
}
if (Container* container = item->getContainer()) {
if (container->getAutoOpen() >= 0) {
addContainer(container->getAutoOpen(), container);
onSendContainer(container);
}
for (ContainerIterator it = container->iterator(); it.hasNext(); it.advance()) {
if (Container* subContainer = (*it)->getContainer()) {
if (subContainer->getAutoOpen() >= 0) {
addContainer(subContainer->getAutoOpen(), subContainer);
onSendContainer(subContainer);
}
}
}
}
}
}
Item* Player::getWriteItem(uint32_t& windowTextId, uint16_t& maxWriteLen)
{
windowTextId = this->windowTextId;
@ -3034,6 +3062,11 @@ void Player::updateItemsLight(bool internal /*=false*/)
void Player::onAddCondition(ConditionType_t type)
{
Creature::onAddCondition(type);
if (type == CONDITION_OUTFIT) {
dismount();
}
sendIcons();
}
@ -3825,6 +3858,256 @@ void Player::sendClosePrivate(uint16_t channelId)
}
}
uint8_t Player::getCurrentMount() const
{
int32_t value;
if (getStorageValue(PSTRG_MOUNTS_CURRENTMOUNT, value)) {
return value;
}
return 0;
}
void Player::setCurrentMount(uint8_t mountId)
{
addStorageValue(PSTRG_MOUNTS_CURRENTMOUNT, mountId);
}
bool Player::toggleMount(bool mount)
{
if ((OTSYS_TIME() - lastToggleMount) < 3000 && !wasMounted) {
sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED);
return false;
}
if (mount) {
if (isMounted()) {
return false;
}
if (!group->access && tile->hasFlag(TILESTATE_PROTECTIONZONE)) {
sendCancelMessage(RETURNVALUE_ACTIONNOTPERMITTEDINPROTECTIONZONE);
return false;
}
const Outfit* playerOutfit = Outfits::getInstance().getOutfitByLookType(getSex(), defaultOutfit.lookType);
if (!playerOutfit) {
return false;
}
uint8_t currentMountId = getCurrentMount();
if (currentMountId == 0) {
sendOutfitWindow();
return false;
}
Mount* currentMount = g_game.mounts.getMountByID(currentMountId);
if (!currentMount) {
return false;
}
if (!hasMount(currentMount)) {
setCurrentMount(0);
sendOutfitWindow();
return false;
}
if (currentMount->premium && !isPremium()) {
sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT);
return false;
}
if (hasCondition(CONDITION_OUTFIT)) {
sendCancelMessage(RETURNVALUE_NOTPOSSIBLE);
return false;
}
defaultOutfit.lookMount = currentMount->clientId;
if (currentMount->speed != 0) {
g_game.changeSpeed(this, currentMount->speed);
}
}
else {
if (!isMounted()) {
return false;
}
dismount();
}
g_game.internalCreatureChangeOutfit(this, defaultOutfit);
lastToggleMount = OTSYS_TIME();
return true;
}
bool Player::tameMount(uint8_t mountId)
{
if (!g_game.mounts.getMountByID(mountId)) {
return false;
}
const uint8_t tmpMountId = mountId - 1;
const uint32_t key = PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31);
int32_t value;
if (getStorageValue(key, value)) {
value |= (1 << (tmpMountId % 31));
}
else {
value = (1 << (tmpMountId % 31));
}
addStorageValue(key, value);
return true;
}
bool Player::untameMount(uint8_t mountId)
{
if (!g_game.mounts.getMountByID(mountId)) {
return false;
}
const uint8_t tmpMountId = mountId - 1;
const uint32_t key = PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31);
int32_t value;
if (!getStorageValue(key, value)) {
return true;
}
value &= ~(1 << (tmpMountId % 31));
addStorageValue(key, value);
if (getCurrentMount() == mountId) {
if (isMounted()) {
dismount();
g_game.internalCreatureChangeOutfit(this, defaultOutfit);
}
setCurrentMount(0);
}
return true;
}
bool Player::hasMount(const Mount* mount) const
{
if (isAccessPlayer()) {
return true;
}
if (mount->premium && !isPremium()) {
return false;
}
const uint8_t tmpMountId = mount->id - 1;
int32_t value;
if (!getStorageValue(PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31), value)) {
return false;
}
return ((1 << (tmpMountId % 31)) & value) != 0;
}
void Player::dismount()
{
Mount* mount = g_game.mounts.getMountByID(getCurrentMount());
if (mount && mount->speed > 0) {
g_game.changeSpeed(this, -mount->speed);
}
defaultOutfit.lookMount = 0;
}
bool Player::hasWing(const Wing* wing) const
{
if (isAccessPlayer()) {
return true;
}
if (wing->premium && !isPremium()) {
return false;
}
const uint8_t tmpWingId = wing->id - 1;
int32_t value;
if (!getStorageValue(PSTRG_WINGS_RANGE_START + (tmpWingId / 31), value)) {
return false;
}
return ((1 << (tmpWingId % 31)) & value) != 0;
}
uint8_t Player::getCurrentWing() const
{
int32_t value;
if (getStorageValue(PSTRG_WINGS_CURRENTWINGS, value)) {
return value;
}
return 0;
}
void Player::setCurrentWing(uint8_t wingId)
{
addStorageValue(PSTRG_WINGS_CURRENTWINGS, wingId);
}
bool Player::hasAura(const Aura* aura) const
{
if (isAccessPlayer()) {
return true;
}
if (aura->premium && !isPremium()) {
return false;
}
const uint8_t tmpAuraId = aura->id - 1;
int32_t value;
if (!getStorageValue(PSTRG_AURAS_RANGE_START + (tmpAuraId / 31), value)) {
return false;
}
return ((1 << (tmpAuraId % 31)) & value) != 0;
}
uint8_t Player::getCurrentAura() const
{
int32_t value;
if (getStorageValue(PSTRG_AURAS_CURRENTAURA, value)) {
return value;
}
return 0;
}
void Player::setCurrentAura(uint8_t auraId)
{
addStorageValue(PSTRG_AURAS_CURRENTAURA, auraId);
}
bool Player::hasShader(const Shader* shader) const
{
if (isAccessPlayer()) {
return true;
}
if (shader->premium && !isPremium()) {
return false;
}
const uint8_t tmpShaderId = shader->id - 1;
int32_t value;
if (!getStorageValue(PSTRG_SHADERS_RANGE_START + (tmpShaderId / 31), value)) {
return false;
}
return ((1 << (tmpShaderId % 31)) & value) != 0;
}
bool Player::addOfflineTrainingTries(skills_t skill, uint64_t tries)
{
if (tries == 0 || skill == SKILL_LEVEL) {

View File

@ -33,6 +33,10 @@
#include "guild.h"
#include "groups.h"
#include "town.h"
#include "mounts.h"
#include "auras.h"
#include "wings.h"
#include "shaders.h"
class BehaviourDatabase;
class House;
@ -131,6 +135,42 @@ class Player final : public Creature, public Cylinder
}
std::string getDescription(int32_t lookDistance) const final;
uint8_t getCurrentMount() const;
void setCurrentMount(uint8_t mountId);
bool isMounted() const
{
return defaultOutfit.lookMount != 0;
}
bool hasMount() const
{
return defaultOutfit.lookMount != 0;
}
bool hasAura() const
{
return defaultOutfit.lookAura != 0;
}
bool hasWings() const
{
return defaultOutfit.lookWings != 0;
}
bool hasShader() const
{
return defaultOutfit.lookShader != 0;
}
bool toggleMount(bool mount);
bool tameMount(uint8_t mountId);
bool untameMount(uint8_t mountId);
bool hasMount(const Mount* mount) const;
void dismount();
bool hasWing(const Wing* wing) const;
uint8_t getCurrentAura() const;
void setCurrentAura(uint8_t auraId);
bool hasAura(const Aura* aura) const;
uint8_t getCurrentWing() const;
void setCurrentWing(uint8_t wingId);
bool hasShader(const Shader* shader) const;
void setGUID(uint32_t guid) {
this->guid = guid;
}
@ -761,6 +801,7 @@ class Player final : public Creature, public Cylinder
client->sendInventoryItem(slot, item);
}
}
void autoOpenContainers();
//event methods
void onUpdateTileItem(const Tile* tile, const Position& pos, const Item* oldItem,
@ -997,10 +1038,39 @@ class Player final : public Creature, public Cylinder
}
}
void sendProgressbar(uint32_t id, uint32_t duration, bool ltr = true) {
if (client) {
client->sendProgressbar(id, duration, ltr);
}
}
void receivePing() {
lastPong = OTSYS_TIME();
}
void setFPS(uint16_t value)
{
fps = value;
}
void setLocalPing(uint16_t value)
{
localPing = value;
}
uint16_t getFPS() const
{
return fps;
}
uint16_t getLocalPing() const
{
return localPing;
}
uint16_t getOTCv8Version() const
{
if (client)
return client->otclientV8;
return 0;
}
void onThink(uint32_t interval) final;
void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) final;
@ -1025,6 +1095,10 @@ class Player final : public Creature, public Cylinder
void learnInstantSpell(const std::string& spellName);
void forgetInstantSpell(const std::string& spellName);
bool hasLearnedInstantSpell(const std::string& spellName) const;
const std::map<uint8_t, OpenContainer>& getOpenContainers() const
{
return openContainers;
}
protected:
std::forward_list<Condition*> getMuteConditions() const;
@ -1115,6 +1189,7 @@ class Player final : public Creature, public Cylinder
int64_t lastPong;
int64_t nextAction = 0;
int64_t earliestAttackTime = 0;
int64_t lastToggleMount = 0;
BedItem* bedItem = nullptr;
Guild* guild = nullptr;
@ -1162,6 +1237,8 @@ class Player final : public Creature, public Cylinder
uint16_t staminaMinutes = 3360;
uint16_t maxWriteLen = 0;
uint16_t localPing = 0;
uint16_t fps = 0;
int16_t lastDepotId = -1;
uint8_t soul = 0;
@ -1179,6 +1256,7 @@ class Player final : public Creature, public Cylinder
bool secureMode = false;
bool inMarket = false;
bool wasMounted = false;
bool ghostMode = false;
bool pzLocked = false;
bool isConnecting = false;

View File

@ -25,14 +25,27 @@
extern RSA g_RSA;
Protocol::~Protocol()
{
if (compression) {
deflateEnd(&zstream);
}
}
void Protocol::onSendMessage(const OutputMessage_ptr& msg) const
{
if (!rawMessages) {
bool compressed = false;
if (compression && msg->getLength() > 64) {
compress(*msg);
compressed = true;
}
msg->writeMessageLength();
if (encryptionEnabled) {
XTEA_encrypt(*msg);
msg->addCryptoHeader();
msg->addCryptoHeader(checksumEnabled, compressed);
}
}
}
@ -153,3 +166,34 @@ uint32_t Protocol::getIP() const
return 0;
}
void Protocol::enableCompression()
{
if (compression)
return;
if (deflateInit2(&zstream, 6, Z_DEFLATED, -15, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
std::cerr << "ZLIB initialization error: " << (zstream.msg ? zstream.msg : "unknown") << std::endl;
}
compression = true;
}
void Protocol::compress(OutputMessage& msg) const
{
static thread_local std::vector<uint8_t> buffer(NETWORKMESSAGE_MAXSIZE);
zstream.next_in = msg.getOutputBuffer();
zstream.avail_in = msg.getLength();
zstream.next_out = buffer.data();
zstream.avail_out = buffer.size();
if (deflate(&zstream, Z_SYNC_FLUSH) != Z_OK) {
std::cerr << "ZLIB deflate error: " << (zstream.msg ? zstream.msg : "unknown") << std::endl;
return;
}
int finalSize = buffer.size() - zstream.avail_out - 4;
if (finalSize < 0) {
std::cerr << "Packet compression error: " << (zstream.msg ? zstream.msg : "unknown") << std::endl;
return;
}
msg.reset();
msg.addBytes((const char*)buffer.data(), finalSize);
}

View File

@ -21,12 +21,13 @@
#define FS_PROTOCOL_H_D71405071ACF4137A4B1203899DE80E1
#include "connection.h"
#include <zlib.h>
class Protocol : public std::enable_shared_from_this<Protocol>
{
public:
explicit Protocol(Connection_ptr connection) : connection(connection), key(), encryptionEnabled(false), rawMessages(false) {}
virtual ~Protocol() = default;
virtual ~Protocol();
// non-copyable
Protocol(const Protocol&) = delete;
@ -74,6 +75,9 @@ protected:
void setXTEAKey(const uint32_t* key) {
memcpy(this->key, key, sizeof(*key) * 4);
}
void disableChecksum() {
checksumEnabled = false;
}
void XTEA_encrypt(OutputMessage& msg) const;
bool XTEA_decrypt(NetworkMessage& msg) const;
@ -82,16 +86,20 @@ protected:
void setRawMessages(bool value) {
rawMessages = value;
}
void enableCompression();
virtual void release() {}
friend class Connection;
OutputMessage_ptr outputBuffer;
private:
void compress(OutputMessage& msg) const;
const ConnectionWeak_ptr connection;
uint32_t key[4];
bool encryptionEnabled;
bool rawMessages;
bool encryptionEnabled = false;
bool checksumEnabled = true;
bool rawMessages = false;
bool compression = false;
mutable z_stream zstream = { 0 };
};
#endif

View File

@ -56,6 +56,17 @@ void ProtocolGame::release()
void ProtocolGame::login(const std::string& name, uint32_t accountId, OperatingSystem_t operatingSystem, bool isFake)
{
// OTCv8 features and extended opcodes
if (otclientV8 || operatingSystem >= CLIENTOS_OTCLIENT_LINUX) {
if (otclientV8)
sendFeatures();
NetworkMessage opcodeMessage;
opcodeMessage.addByte(0x32);
opcodeMessage.addByte(0x00);
opcodeMessage.add<uint16_t>(0x00);
writeToOutputBuffer(opcodeMessage);
}
//dispatcher thread
Player* foundPlayer = g_game.getPlayerByName(name);
if (!foundPlayer || g_config.getBoolean(ConfigManager::ALLOW_CLONES)) {
@ -145,6 +156,8 @@ void ProtocolGame::login(const std::string& name, uint32_t accountId, OperatingS
}
}
player->autoOpenContainers();
if (operatingSystem >= CLIENTOS_OTCLIENT_LINUX) {
player->registerCreatureEvent("ExtendedOpcode");
}
@ -197,6 +210,7 @@ void ProtocolGame::connect(uint32_t playerId, OperatingSystem_t operatingSystem)
player->client = getThis();
sendAddCreature(player, player->getPosition(), 0, false);
player->autoOpenContainers();
player->lastIP = player->getIP();
player->lastLoginSaved = std::max<time_t>(time(nullptr), player->lastLoginSaved + 1);
acceptPackets = true;
@ -263,20 +277,18 @@ void ProtocolGame::onRecvFirstMessage(NetworkMessage& msg)
enableXTEAEncryption();
setXTEAKey(key);
if (operatingSystem >= CLIENTOS_OTCLIENT_LINUX) {
NetworkMessage opcodeMessage;
opcodeMessage.addByte(0x32);
opcodeMessage.addByte(0x00);
opcodeMessage.add<uint16_t>(0x00);
writeToOutputBuffer(opcodeMessage);
}
msg.skipBytes(1); // gamemaster flag
uint32_t accountNumber = msg.get<uint32_t>();
std::string characterName = msg.getString();
std::string password = msg.getString();
// OTCv8 version detection
uint16_t otcV8StringLength = msg.get<uint16_t>();
if (otcV8StringLength == 5 && msg.getString(5) == "OTCv8") {
otclientV8 = msg.get<uint16_t>(); // 253, 260, 261, ...
}
/*if (version < CLIENT_VERSION_MIN || version > CLIENT_VERSION_MAX) {
//sendUpdateRequest();
disconnectClient("Use Tibia 7.72 to login!");
@ -338,8 +350,31 @@ void ProtocolGame::onRecvFirstMessage(NetworkMessage& msg)
void ProtocolGame::onConnect()
{
// Checksum missimplementation
/* auto output = OutputMessagePool::getOutputMessage();
static std::random_device rd;
static std::ranlux24 generator(rd());
static std::uniform_int_distribution<uint16_t> randNumber(0x00, 0xFF);
// Skip checksum
output->skipBytes(sizeof(uint32_t));
// Packet length & type
output->add<uint16_t>(0x0006);
output->addByte(0x1F);
// Add timestamp & random number
challengeTimestamp = static_cast<uint32_t>(time(nullptr));
output->add<uint32_t>(challengeTimestamp);
challengeRandom = randNumber(generator);
output->addByte(challengeRandom);
// Go back and write checksum
output->skipBytes(-12);
output->add<uint32_t>(adlerChecksum(output->getOutputBuffer() + sizeof(uint32_t), 8));
send(output); */
}
void ProtocolGame::disconnectClient(const std::string& message) const
@ -390,6 +425,7 @@ void ProtocolGame::parsePacket(NetworkMessage& msg)
case 0x1D: addGameTask(&Game::playerReceivePingBack, player->getID()); break;
case 0x1E: addGameTask(&Game::playerReceivePing, player->getID()); break;
case 0x32: parseExtendedOpcode(msg); break; //otclient extended opcode
case 0x40: parseNewPing(msg); break;
case 0x64: parseAutoWalk(msg); break;
case 0x65: addGameTask(&Game::playerMove, player->getID(), DIRECTION_NORTH); break;
case 0x66: addGameTask(&Game::playerMove, player->getID(), DIRECTION_EAST); break;
@ -445,6 +481,7 @@ void ProtocolGame::parsePacket(NetworkMessage& msg)
case 0xCC: parseSeekInContainer(msg); break;
case 0xD2: addGameTask(&Game::playerRequestOutfit, player->getID()); break;
case 0xD3: parseSetOutfit(msg); break;
case 0xD4: parseToggleMount(msg); break;
case 0xDC: parseAddVip(msg); break;
case 0xDD: parseRemoveVip(msg); break;
case 0xE6: parseBugReport(msg); break;
@ -735,9 +772,27 @@ void ProtocolGame::parseSetOutfit(NetworkMessage& msg)
newOutfit.lookLegs = msg.getByte();
newOutfit.lookFeet = msg.getByte();
newOutfit.lookAddons = msg.getByte();
newOutfit.lookMount = msg.get<uint16_t>();
newOutfit.lookWings = otclientV8 ? msg.get<uint16_t>() : 0;
newOutfit.lookAura = otclientV8 ? msg.get<uint16_t>() : 0;
std::string shaderName = otclientV8 ? msg.getString() : "";
Shader* shader = g_game.shaders.getShaderByName(shaderName);
newOutfit.lookShader = shader ? shader->id : 0;
addGameTask(&Game::playerChangeOutfit, player->getID(), newOutfit);
}
void ProtocolGame::parseToggleMount(NetworkMessage& msg)
{
int mount = msg.get<int8_t>();
int wings = -1, aura = -1, shader = -1;
if (otclientV8 >= 254) {
wings = msg.get<int8_t>();
aura = msg.get<int8_t>();
shader = msg.get<int8_t>();
}
addGameTask(&Game::playerToggleOutfitExtension, player->getID(), mount, wings, aura, shader);
}
void ProtocolGame::parseUseItem(NetworkMessage& msg)
{
Position pos = msg.getPosition();
@ -2127,9 +2182,13 @@ void ProtocolGame::sendOutfitWindow()
msg.addByte(0xC8);
Outfit_t currentOutfit = player->getDefaultOutfit();
Mount* currentMount = g_game.mounts.getMountByID(player->getCurrentMount());
if (currentMount) {
currentOutfit.lookMount = currentMount->clientId;
}
AddOutfit(msg, currentOutfit);
const ClientVersion_t clientVersion = g_game.getClientVersion();
std::vector<ProtocolOutfit> protocolOutfits;
if (player->isAccessPlayer()) {
static const std::string gamemasterOutfitName = "Gamemaster";
@ -2145,22 +2204,72 @@ void ProtocolGame::sendOutfitWindow()
}
protocolOutfits.emplace_back(outfit.name, outfit.lookType, addons);
if (CLIENT_VERSION_780 <= clientVersion && clientVersion <= CLIENT_VERSION_792) {
if (protocolOutfits.size() == 20) { // Game client doesn't allow more than 15 outfits in 780-792
if (protocolOutfits.size() == 100) { // Game client doesn't allow more than 100 outfits
break;
}
}
}
msg.addByte(protocolOutfits.size());
for (const ProtocolOutfit& outfit : protocolOutfits) {
msg.add<uint16_t>(outfit.lookType);
if (clientVersion > CLIENT_VERSION_781) {
msg.addString(outfit.name);
}
msg.addByte(outfit.addons);
}
std::vector<const Mount*> mounts;
for (const Mount& mount : g_game.mounts.getMounts()) {
if (player->hasMount(&mount)) {
mounts.push_back(&mount);
}
}
msg.addByte(mounts.size());
for (const Mount* mount : mounts) {
msg.add<uint16_t>(mount->clientId);
msg.addString(mount->name);
}
if (otclientV8) {
std::vector<const Wing*> wings;
for (const Wing& wing : g_game.wings.getWings()) {
if (player->hasWing(&wing)) {
wings.push_back(&wing);
}
}
msg.addByte(wings.size());
for (const Wing* wing : wings) {
msg.add<uint16_t>(wing->clientId);
msg.addString(wing->name);
}
std::vector<const Aura*> auras;
for (const Aura& aura : g_game.auras.getAuras()) {
if (player->hasAura(&aura)) {
auras.push_back(&aura);
}
}
msg.addByte(auras.size());
for (const Aura* aura : auras) {
msg.add<uint16_t>(aura->clientId);
msg.addString(aura->name);
}
std::vector<const Shader*> shaders;
for (const Shader& shader : g_game.shaders.getShaders()) {
if (player->hasShader(&shader)) {
shaders.push_back(&shader);
}
}
msg.addByte(shaders.size());
for (const Shader* shader : shaders) {
msg.add<uint16_t>(shader->id);
msg.addString(shader->name);
}
}
writeToOutputBuffer(msg);
}
@ -2284,9 +2393,18 @@ void ProtocolGame::AddOutfit(NetworkMessage& msg, const Outfit_t& outfit)
msg.addByte(outfit.lookLegs);
msg.addByte(outfit.lookFeet);
msg.addByte(outfit.lookAddons);
} else {
}
else {
msg.addItemId(outfit.lookTypeEx);
}
msg.add<uint16_t>(outfit.lookMount);
if (otclientV8) {
msg.add<uint16_t>(outfit.lookWings);
msg.add<uint16_t>(outfit.lookAura);
Shader* shader = g_game.shaders.getShaderByID(outfit.lookShader);
msg.addString(shader ? shader->name : "");
}
}
void ProtocolGame::AddWorldLight(NetworkMessage& msg, const LightInfo& lightInfo)
@ -2414,3 +2532,75 @@ void ProtocolGame::parseExtendedOpcode(NetworkMessage& msg)
// process additional opcodes via lua script event
addGameTask(&Game::parsePlayerExtendedOpcode, player->getID(), opcode, buffer);
}
// OTCv8
void ProtocolGame::sendFeatures()
{
if (!otclientV8)
return;
std::map<GameFeature, bool> features;
// place for non-standard OTCv8 features
//features[GameExtendedOpcode] = true;
//features[GameChangeMapAwareRange] = true;
//features[GameNewWalking] = true;
//features[GameEnvironmentEffect] = false; // disable it, useless 2 byte with every tile
//features[GameExtendedClientPing] = true;
//features[GameItemTooltip] = true; // fully available from version 2.6
//features[GameWingsAndAura] = true;
//features[GameOutfitShaders] = true;
// packet compression
// we don't send feature, because feature assumes all packets are compressed
// if adler32 is enabled then compression can be detected automaticly, just adlre32 must be 0
if (g_config.getBoolean(ConfigManager::PACKET_COMPRESSION)) {
enableCompression();
}
if (features.empty())
return;
auto msg = getOutputBuffer(1024);
msg->addByte(0x43);
msg->add<uint16_t>(features.size());
for (auto& feature : features) {
msg->addByte((uint8_t)feature.first);
msg->addByte(feature.second ? 1 : 0);
}
send(std::move(getCurrentBuffer())); // send this packet immediately
}
void ProtocolGame::parseNewPing(NetworkMessage& msg)
{
uint32_t pingId = msg.get<uint32_t>();
uint16_t localPing = msg.get<uint16_t>();
uint16_t fps = msg.get<uint16_t>();
addGameTask(&Game::playerReceiveNewPing, player->getID(), localPing, fps);
g_dispatcher.addTask(createTask(std::bind(&ProtocolGame::sendNewPing, getThis(), pingId)));
}
void ProtocolGame::sendNewPing(uint32_t pingId)
{
if (!otclientV8)
return;
NetworkMessage msg;
msg.addByte(0x40);
msg.add<uint32_t>(pingId);
writeToOutputBuffer(msg);
}
void ProtocolGame::sendProgressbar(uint32_t id, uint32_t duration, bool ltr)
{
if (!otclientV8 || otclientV8 < 260)
return;
NetworkMessage msg;
msg.addByte(0x3b);
msg.add<uint32_t>(id);
msg.add<uint32_t>(duration);
msg.addByte(ltr);
writeToOutputBuffer(msg);
}

View File

@ -52,6 +52,7 @@ class ProtocolGame final : public Protocol
// static protocol information
enum { server_sends_first = true };
enum { protocol_identifier = 0 }; // Not required as we send first
enum { use_checksum = true };
static const char* protocol_name() {
return "gameworld protocol";
@ -90,6 +91,7 @@ class ProtocolGame final : public Protocol
//Parse methods
void parseAutoWalk(NetworkMessage& msg);
void parseSetOutfit(NetworkMessage& msg);
void parseToggleMount(NetworkMessage& msg);
void parseSay(NetworkMessage& msg);
void parseLookAt(NetworkMessage& msg);
void parseLookInBattleList(NetworkMessage& msg);
@ -265,6 +267,12 @@ class ProtocolGame final : public Protocol
//otclient
void parseExtendedOpcode(NetworkMessage& msg);
//OTCv8
void sendFeatures();
void sendProgressbar(uint32_t id, uint32_t duration, bool ltr = true);
void parseNewPing(NetworkMessage& msg);
void sendNewPing(uint32_t pingId);
friend class Player;
// Helpers so we don't need to bind every time
@ -289,6 +297,7 @@ class ProtocolGame final : public Protocol
bool debugAssertSent = false;
bool acceptPackets = false;
uint16_t otclientV8 = 0;
};
#endif

View File

@ -193,6 +193,13 @@ void ProtocolLogin::onRecvFirstMessage(NetworkMessage& msg)
return;
}
// OTCv8 version detection
uint16_t otclientV8 = 0;
uint16_t otcV8StringLength = msg.get<uint16_t>();
if (otcV8StringLength == 5 && msg.getString(5) == "OTCv8") {
otclientV8 = msg.get<uint16_t>(); // 253, 260, 261, ...
}
auto thisPtr = std::static_pointer_cast<ProtocolLogin>(shared_from_this());
g_dispatcher.addTask(createTask(std::bind(&ProtocolLogin::getCharacterList, thisPtr, accountNumber, password, version)));
}

View File

@ -32,6 +32,7 @@ class ProtocolLogin : public Protocol
// static protocol information
enum {server_sends_first = false};
enum {protocol_identifier = 0x01};
enum { use_checksum = true };
static const char* protocol_name() {
return "login protocol";
}

View File

@ -29,6 +29,7 @@ class ProtocolStatus final : public Protocol
// static protocol information
enum {server_sends_first = false};
enum {protocol_identifier = 0xFF};
enum { use_checksum = false };
static const char* protocol_name() {
return "status protocol";
}

52
src/shaders.cpp Normal file
View File

@ -0,0 +1,52 @@
#include "otpch.h"
#include "shaders.h"
#include "pugicast.h"
#include "tools.h"
bool Shaders::reload()
{
shaders.clear();
return loadFromXml();
}
bool Shaders::loadFromXml()
{
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file("data/XML/shaders.xml");
if (!result) {
printXMLError("Error - Shaders::loadFromXml", "data/XML/shaders.xml", result);
return false;
}
for (auto shaderNode : doc.child("shaders").children()) {
shaders.emplace_back(
static_cast<uint8_t>(pugi::cast<uint16_t>(shaderNode.attribute("id").value())),
shaderNode.attribute("name").as_string(),
shaderNode.attribute("premium").as_bool()
);
}
shaders.shrink_to_fit();
return true;
}
Shader* Shaders::getShaderByID(uint8_t id)
{
auto it = std::find_if(shaders.begin(), shaders.end(), [id](const Shader& shader) {
return shader.id == id;
});
return it != shaders.end() ? &*it : nullptr;
}
Shader* Shaders::getShaderByName(const std::string& name) {
auto shaderName = name.c_str();
for (auto& it : shaders) {
if (strcasecmp(shaderName, it.name.c_str()) == 0) {
return &it;
}
}
return nullptr;
}

30
src/shaders.h Normal file
View File

@ -0,0 +1,30 @@
#ifndef FS_SHADERS_H
#define FS_SHADERS_H
struct Shader
{
Shader(uint8_t id, std::string name, bool premium) :
name(std::move(name)), id(id), premium(premium) {}
uint8_t id;
std::string name;
bool premium;
};
class Shaders
{
public:
bool reload();
bool loadFromXml();
Shader* getShaderByID(uint8_t id);
Shader* getShaderByName(const std::string& name);
const std::vector<Shader>& getShaders() const {
return shaders;
}
private:
std::vector<Shader> shaders;
};
#endif

View File

@ -890,6 +890,31 @@ std::string getSkillName(uint8_t skillid)
}
}
uint32_t adlerChecksum(const uint8_t* data, size_t length)
{
if (length > NETWORKMESSAGE_MAXSIZE) {
return 0;
}
const uint16_t adler = 65521;
uint32_t a = 1, b = 0;
while (length > 0) {
size_t tmp = length > 5552 ? 5552 : length;
length -= tmp;
do {
a += *data++;
b += a;
} while (--tmp);
a %= adler;
b %= adler;
}
return (b << 16) | a;
}
std::string ucfirst(std::string str)
{

View File

@ -85,6 +85,8 @@ std::string getCombatName(CombatType_t combatType);
std::string getSkillName(uint8_t skillid);
uint32_t adlerChecksum(const uint8_t* data, size_t length);
std::string ucfirst(std::string str);
std::string ucwords(std::string str);
bool booleanString(const std::string& str);

63
src/wings.cpp Normal file
View File

@ -0,0 +1,63 @@
#include "otpch.h"
#include "wings.h"
#include "pugicast.h"
#include "tools.h"
bool Wings::reload()
{
wings.clear();
return loadFromXml();
}
bool Wings::loadFromXml()
{
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file("data/XML/wings.xml");
if (!result) {
printXMLError("Error - Wings::loadFromXml", "data/XML/wings.xml", result);
return false;
}
for (auto wingNode : doc.child("wings").children()) {
wings.emplace_back(
static_cast<uint8_t>(pugi::cast<uint16_t>(wingNode.attribute("id").value())),
pugi::cast<uint16_t>(wingNode.attribute("clientid").value()),
wingNode.attribute("name").as_string(),
pugi::cast<int32_t>(wingNode.attribute("speed").value()),
wingNode.attribute("premium").as_bool()
);
}
wings.shrink_to_fit();
return true;
}
Wing* Wings::getWingByID(uint8_t id)
{
auto it = std::find_if(wings.begin(), wings.end(), [id](const Wing& wing) {
return wing.id == id;
});
return it != wings.end() ? &*it : nullptr;
}
Wing* Wings::getWingByName(const std::string& name) {
auto wingName = name.c_str();
for (auto& it : wings) {
if (strcasecmp(wingName, it.name.c_str()) == 0) {
return &it;
}
}
return nullptr;
}
Wing* Wings::getWingByClientID(uint16_t clientId)
{
auto it = std::find_if(wings.begin(), wings.end(), [clientId](const Wing& wing) {
return wing.clientId == clientId;
});
return it != wings.end() ? &*it : nullptr;
}

33
src/wings.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef FS_WINGS_H
#define FS_WINGS_H
struct Wing
{
Wing(uint8_t id, uint16_t clientId, std::string name, int32_t speed, bool premium) :
name(std::move(name)), speed(speed), clientId(clientId), id(id), premium(premium) {}
std::string name;
int32_t speed;
uint16_t clientId;
uint8_t id;
bool premium;
};
class Wings
{
public:
bool reload();
bool loadFromXml();
Wing* getWingByID(uint8_t id);
Wing* getWingByName(const std::string& name);
Wing* getWingByClientID(uint16_t clientId);
const std::vector<Wing>& getWings() const {
return wings;
}
private:
std::vector<Wing> wings;
};
#endif

View File

@ -143,6 +143,7 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\src\actions.cpp" />
<ClCompile Include="..\src\auras.cpp" />
<ClCompile Include="..\src\ban.cpp" />
<ClCompile Include="..\src\baseevents.cpp" />
<ClCompile Include="..\src\bed.cpp" />
@ -179,6 +180,7 @@
<ClCompile Include="..\src\map.cpp" />
<ClCompile Include="..\src\monster.cpp" />
<ClCompile Include="..\src\monsters.cpp" />
<ClCompile Include="..\src\mounts.cpp" />
<ClCompile Include="..\src\movement.cpp" />
<ClCompile Include="..\src\networkmessage.cpp" />
<ClCompile Include="..\src\npc.cpp" />
@ -204,6 +206,7 @@
<ClCompile Include="..\src\script.cpp" />
<ClCompile Include="..\src\scriptmanager.cpp" />
<ClCompile Include="..\src\server.cpp" />
<ClCompile Include="..\src\shaders.cpp" />
<ClCompile Include="..\src\spawn.cpp" />
<ClCompile Include="..\src\spells.cpp" />
<ClCompile Include="..\src\protocolstatus.cpp" />
@ -216,6 +219,7 @@
<ClCompile Include="..\src\vocation.cpp" />
<ClCompile Include="..\src\waitlist.cpp" />
<ClCompile Include="..\src\wildcardtree.cpp" />
<ClCompile Include="..\src\wings.cpp" />
<ClCompile Include="..\src\xtea.cpp" />
<ClCompile Include="..\src\quests.cpp" />
<ClCompile Include="..\src\iomarket.cpp" />
@ -223,6 +227,7 @@
<ItemGroup>
<ClInclude Include="..\src\account.h" />
<ClInclude Include="..\src\actions.h" />
<ClInclude Include="..\src\auras.h" />
<ClInclude Include="..\src\ban.h" />
<ClInclude Include="..\src\baseevents.h" />
<ClInclude Include="..\src\bed.h" />
@ -263,6 +268,7 @@
<ClInclude Include="..\src\map.h" />
<ClInclude Include="..\src\monster.h" />
<ClInclude Include="..\src\monsters.h" />
<ClInclude Include="..\src\mounts.h" />
<ClInclude Include="..\src\movement.h" />
<ClInclude Include="..\src\networkmessage.h" />
<ClInclude Include="..\src\npc.h" />
@ -282,6 +288,7 @@
<ClInclude Include="..\src\script.h" />
<ClInclude Include="..\src\scriptmanager.h" />
<ClInclude Include="..\src\server.h" />
<ClInclude Include="..\src\shaders.h" />
<ClInclude Include="..\src\spawn.h" />
<ClInclude Include="..\src\spells.h" />
<ClInclude Include="..\src\protocolstatus.h" />
@ -296,6 +303,7 @@
<ClInclude Include="..\src\vocation.h" />
<ClInclude Include="..\src\waitlist.h" />
<ClInclude Include="..\src\wildcardtree.h" />
<ClInclude Include="..\src\wings.h" />
<ClInclude Include="..\src\xtea.h" />
<ClInclude Include="..\src\quests.h" />
<ClInclude Include="..\src\iomarket.h" />