• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!
  • 2026 staff recruitment is open! Check it out and consider applying!

TFS 1.X+ otclient communication to server

Mjmackan

Mapper ~ Writer
Joined
Jul 18, 2009
Messages
1,474
Solutions
17
Reaction score
194
Location
Sweden
Hey!

I'm fooling around with otclient and tried make a simple, basic talent tree but i cant get the serverside to answer, it feels like im missing something in sources or such. Is there any tutorial out there or specific source code addon i need for this to work?
And yes I have updated my server with .json file, but it seems that my server wont answer to networkMessages, if thats all i have no clue.

Using tfs 1.4.2

LUA:
local OPCODE = 105

print("[Talents System] Aggressive Registration Version Loaded")

local function sendRefresh(player)
    local msg = NetworkMessage()
    msg:addByte(OPCODE)
    msg:addString(json.encode({
        action = "refresh",
        data = {
            points = 75,
            tokens = 15,
            talentInfo = {
                {name = "Life Boost", icon = "/images/game/talents/life", desc = "Increases maximum health", value = "30/100"},
                {name = "Mana Boost", icon = "/images/game/talents/mana", desc = "Increases maximum mana", value = "20/100"},
                {name = "Damage Boost", icon = "/images/game/talents/damage", desc = "Increases damage", value = "15/50"},
                {name = "Speed Boost", icon = "/images/game/talents/speed", desc = "Increases speed", value = "25/40"}
            }
        }
    }))
    player:send(msg)
end

-- Try every possible way to register
local function registerOpcode()
    -- Method 1
    if type(Player) == "table" then
        Player.onExtendedOpcode = function(player, opcode, buffer)
            if opcode == OPCODE then
                print("[Talents] Received via Player.onExtendedOpcode")
                sendRefresh(player)
            end
        end
    end

    -- Method 2
    _G.onExtendedOpcode = function(player, opcode, buffer)
        if opcode == OPCODE then
            print("[Talents] Received via _G.onExtendedOpcode")
            sendRefresh(player)
        end
    end

    print("[Talents System] All registration methods attempted")
end

registerOpcode()

1778798643828.webp
 
Last edited:
Solution
Mjmackan said:
That seems to work, but I still dont understand what im doing wrong then..

yeah i think the missing part is that your client window is still being filled locally. it opens the UI and creates the talent rows, but it never actually asks the server for the data.

try this instead:

right now the client opens the window and fills the list by itself. what you want instead is for the client to ask the server for the talent data when the window opens, then let the server send the data back and build the list from that response.

client side otclient mod:

LUA:
-- =============================================
-- TALENTS CLIENT
-- =============================================

local OPCODE = 105
local talentWindow
local talentsButton...
Code:
player:send(msg)
must be
Code:
player:sendExtendedOpcode(opcode, buffer)

Also make sure you register on login or else it won't do anything, you want something like player:registerEvent("TalentOpcode") in your creaturescript/login.lua however, if you are using revscript, you can register it all in the same script, of course


LUA:
local OPCODE = 105

print("[Talents] Loaded Successfully")

local function sendTalentData(player)

    local data = {
        action = "refresh",
        data = {
            points = 75,
            tokens = 15,
            talentInfo = {
                {
                    name = "Life Boost",
                    icon = "/images/game/talents/life",
                    desc = "Increases maximum health",
                    value = "30/100"
                },
                {
                    name = "Mana Boost",
                    icon = "/images/game/talents/mana",
                    desc = "Increases maximum mana",
                    value = "20/100"
                },
                {
                    name = "Damage Boost",
                    icon = "/images/game/talents/damage",
                    desc = "Increases damage",
                    value = "15/50"
                },
                {
                    name = "Speed Boost",
                    icon = "/images/game/talents/speed",
                    desc = "Increases speed",
                    value = "25/40"
                }
            }
        }
    }

    player:sendExtendedOpcode(OPCODE, json.encode(data))

    print("[Talents] Data sent")
end

local talentEvent = CreatureEvent("TalentOpcode")

function talentEvent.onExtendedOpcode(player, opcode, buffer)

    print("[Talents] Opcode received:", opcode)

    if opcode ~= OPCODE then
        return
    end

    print("[Talents] Buffer:", buffer)

    local success, decoded = pcall(function()
        return json.decode(buffer)
    end)

    if not success then
        print("[Talents] JSON decode failed")
        return
    end

    if decoded.action == "request" then
        sendTalentData(player)
    end
end

talentEvent:register()

local loginEvent = CreatureEvent("TalentLogin")

function loginEvent.onLogin(player)

    player:registerEvent("TalentOpcode")

    print("[Talents] Event registered for", player:getName())

    return true
end

loginEvent:register()
 
Code:
player:send(msg)
must be
Code:
player:sendExtendedOpcode(opcode, buffer)

Also make sure you register on login or else it won't do anything, you want something like player:registerEvent("TalentOpcode") in your creaturescript/login.lua however, if you are using revscript, you can register it all in the same script, of course


LUA:
local OPCODE = 105

print("[Talents] Loaded Successfully")

local function sendTalentData(player)

    local data = {
        action = "refresh",
        data = {
            points = 75,
            tokens = 15,
            talentInfo = {
                {
                    name = "Life Boost",
                    icon = "/images/game/talents/life",
                    desc = "Increases maximum health",
                    value = "30/100"
                },
                {
                    name = "Mana Boost",
                    icon = "/images/game/talents/mana",
                    desc = "Increases maximum mana",
                    value = "20/100"
                },
                {
                    name = "Damage Boost",
                    icon = "/images/game/talents/damage",
                    desc = "Increases damage",
                    value = "15/50"
                },
                {
                    name = "Speed Boost",
                    icon = "/images/game/talents/speed",
                    desc = "Increases speed",
                    value = "25/40"
                }
            }
        }
    }

    player:sendExtendedOpcode(OPCODE, json.encode(data))

    print("[Talents] Data sent")
end

local talentEvent = CreatureEvent("TalentOpcode")

function talentEvent.onExtendedOpcode(player, opcode, buffer)

    print("[Talents] Opcode received:", opcode)

    if opcode ~= OPCODE then
        return
    end

    print("[Talents] Buffer:", buffer)

    local success, decoded = pcall(function()
        return json.decode(buffer)
    end)

    if not success then
        print("[Talents] JSON decode failed")
        return
    end

    if decoded.action == "request" then
        sendTalentData(player)
    end
end

talentEvent:register()

local loginEvent = CreatureEvent("TalentLogin")

function loginEvent.onLogin(player)

    player:registerEvent("TalentOpcode")

    print("[Talents] Event registered for", player:getName())

    return true
end

loginEvent:register()
I registered it all in revscript and tried with player:sendExtendedOpcode(opcode, buffer) but i still wont get any response when i use my otclient mod.
 
I registered it all in revscript and tried with player:sendExtendedOpcode(opcode, buffer) but i still wont get any response when i use my otclient mod.
put this right before you send the opcode from the otclient mod:

LUA:
print("sending talent opcode")

local protocol = g_game.getProtocolGame()
if protocol then
protocol:sendExtendedOpcode(105, json.encode({ action = "request" }))
else
print("no ProtocolGame yet")
end

then put a print as the first line inside your server onExtendedOpcode

if you see the client print but nothing on the server, then the packet is not reaching tfs at all. that usually means the client is not sending it properly, extended opcode is not enabled, or you are trying to send it before ProtocolGame is ready

also check that extended opcode is enabled on the client:

LUA:
g_game.enableFeature(GameExtendedOpcode)

and make sure you register/send it after login, not while the module is just loading.

so basically:

client prints, server does not = otclient side issue
server prints, client gets no reply = server response issue
 
put this right before you send the opcode from the otclient mod:

LUA:
print("sending talent opcode")

local protocol = g_game.getProtocolGame()
if protocol then
protocol:sendExtendedOpcode(105, json.encode({ action = "request" }))
else
print("no ProtocolGame yet")
end

then put a print as the first line inside your server onExtendedOpcode

if you see the client print but nothing on the server, then the packet is not reaching tfs at all. that usually means the client is not sending it properly, extended opcode is not enabled, or you are trying to send it before ProtocolGame is ready

also check that extended opcode is enabled on the client:

LUA:
g_game.enableFeature(GameExtendedOpcode)

and make sure you register/send it after login, not while the module is just loading.

so basically:

client prints, server does not = otclient side issue
server prints, client gets no reply = server response issue
That seems to work, but I still dont understand what im doing wrong then..

otclient mod:
LUA:
-- =============================================
-- TALENTS CLIENT - ALL 17 TALENTS
-- =============================================

local talentWindow

function init()
    talentWindow = g_ui.displayUI("talents")
    talentWindow:hide()

    modules.client_topmenu.addRightGameToggleButton("TalentsButton", tr("Talents"), "/images/topbuttons/questlog", talentWindowtoggle)
    print("[Talents] Full 17 Talents Version Loaded!")
end

function talentWindowtoggle()
    if talentWindow:isVisible() then
        talentWindow:hide()
        return
    end

    talentWindow.talentBalance:setText("Talent Points: 92")
    talentWindow.tokensBalance:setText("Talent Tokens: 22")

    local list = talentWindow.talentList
    list:destroyChildren()

    local talents = {
        {name = "Life Boost",          icon = "/game_talents/images/health.png",              desc = "Increases maximum health",          value = 35, max = 100},
        {name = "Mana Boost",          icon = "/game_talents/images/mana.png",                desc = "Increases maximum mana",            value = 28, max = 100},
        {name = "Damage Increase",     icon = "/game_talents/images/damageincrease.png",      desc = "Increases all damage dealt",        value = 18, max = 50},
        {name = "Defense Boost",       icon = "/game_talents/images/reduction.png",           desc = "Reduces damage taken",              value = 15, max = 50},
        {name = "Crit Chance",         icon = "/game_talents/images/critical.png",            desc = "Increases critical hit chance",     value = 12, max = 30},
        {name = "Attack Speed",        icon = "/game_talents/images/atkspeed.png",            desc = "Increases attack speed",            value = 22, max = 40},
        {name = "Spell Power",         icon = "/game_talents/images/modsspellpower.png",      desc = "Increases spell power",             value = 14, max = 50},
        {name = "Mana Cost Reduction", icon = "/game_talents/images/manacostreduction.png",   desc = "Reduces mana cost of spells",        value = 18, max = 40},
        {name = "Healing Increase",    icon = "/game_talents/images/healingincrease.png",     desc = "Increases healing received",        value = 20, max = 50},
        {name = "Penetration",         icon = "/game_talents/images/penetration.png",         desc = "Ignores enemy defense",             value = 8,  max = 30},
        {name = "Luck",                icon = "/game_talents/images/luck.png",                desc = "Increases luck & drop rate",        value = 5,  max = 25},
        {name = "Spell Duration",      icon = "/game_talents/images/modsspelltime.png",       desc = "Increases spell duration",          value = 10, max = 40},
        {name = "Mana Buster",         icon = "/game_talents/images/mana_buster.png",         desc = "Reduces enemy mana",                value = 7,  max = 30},
        {name = "Sacred Defense",      icon = "/game_talents/images/sred.png",                desc = "Holy & Sacred resistance",          value = 12, max = 40}
    }

    for _, t in ipairs(talents) do
        local widget = g_ui.createWidget("talentTableInfo", list)
        
        widget.talentTableIcon:setImageSource(t.icon)
        widget.talentTableIcon:setTooltip(t.name .. "\n" .. t.desc)
        widget.talentTableName:setText(t.name)
        widget.value:setText(t.value .. " / " .. t.max)

        widget.add.onClick = function()
            if t.value < t.max then
                t.value = t.value + 1
                widget.value:setText(t.value .. " / " .. t.max)
            end
        end

        widget.remove.onClick = function()
            if t.value > 0 then
                t.value = t.value - 1
                widget.value:setText(t.value .. " / " .. t.max)
            end
        end
    end

    talentWindow:show()
    talentWindow:focus()
end

print("[Talents] Full version with 14 core talents loaded!")

TFS server revscript:
LUA:
-- =============================================
-- TALENTS SYSTEM - IMPROVED SERVER SIDE
-- =============================================

local OPCODE = 105

print("[Talents System] Loading...")

local function sendTalentData(player)
    local data = {
        action = "refresh",
        data = {
            points = 75,
            tokens = 15,
            talentInfo = {
                {name = "Life Boost",          icon = "/game_talents/images/health.png",              desc = "Increases maximum health",          value = "35/100"},
                {name = "Mana Boost",          icon = "/game_talents/images/mana.png",                desc = "Increases maximum mana",            value = "28/100"},
                {name = "Damage Increase",     icon = "/game_talents/images/damageincrease.png",      desc = "Increases damage",                   value = "18/50"},
                {name = "Defense Boost",       icon = "/game_talents/images/reduction.png",           desc = "Reduces damage taken",              value = "15/50"},
                {name = "Crit Chance",         icon = "/game_talents/images/critical.png",            desc = "Increases critical chance",         value = "12/30"},
                {name = "Attack Speed",        icon = "/game_talents/images/atkspeed.png",            desc = "Increases attack speed",            value = "22/40"}
            }
        }
    }

    player:sendExtendedOpcode(OPCODE, json.encode(data))
    print("[Talents] Data sent to " .. player:getName())
end

-- Main handler
local function onTalentOpcode(player, opcode, buffer)
    if opcode ~= OPCODE then return end

    print("[Talents] ✅ PACKET RECEIVED from " .. player:getName())

    local success, decoded = pcall(json.decode, buffer)
    if not success then
        print("[Talents] JSON decode failed")
        return
    end

    if decoded.action == "refresh" then
        sendTalentData(player)
    end
end

-- Register using CreatureEvent (most reliable on your server)
local talentEvent = CreatureEvent("TalentOpcode")

function talentEvent.onExtendedOpcode(player, opcode, buffer)
    onTalentOpcode(player, opcode, buffer)
end

talentEvent:register()

-- Auto register on login
local loginEvent = CreatureEvent("TalentLogin")

function loginEvent.onLogin(player)
    player:registerEvent("TalentOpcode")
    print("[Talents] Registered for " .. player:getName())
    return true
end

loginEvent:register()

print("[Talents System] Fully Loaded!")
 
Mjmackan said:
That seems to work, but I still dont understand what im doing wrong then..

yeah i think the missing part is that your client window is still being filled locally. it opens the UI and creates the talent rows, but it never actually asks the server for the data.

try this instead:

right now the client opens the window and fills the list by itself. what you want instead is for the client to ask the server for the talent data when the window opens, then let the server send the data back and build the list from that response.

client side otclient mod:

LUA:
-- =============================================
-- TALENTS CLIENT
-- =============================================

local OPCODE = 105
local talentWindow
local talentsButton

local function renderTalents(payload)
    if not talentWindow then
        return
    end

    local data = payload.data
    if not data then
        print("[Talents] no data in server payload")
        return
    end

    talentWindow.talentBalance:setText("Talent Points: " .. tostring(data.points or 0))
    talentWindow.tokensBalance:setText("Talent Tokens: " .. tostring(data.tokens or 0))

    local list = talentWindow.talentList
    list:destroyChildren()

    for _, t in ipairs(data.talentInfo or {}) do
        local widget = g_ui.createWidget("talentTableInfo", list)

        widget.talentTableIcon:setImageSource(t.icon)
        widget.talentTableIcon:setTooltip(t.name .. "\n" .. t.desc)
        widget.talentTableName:setText(t.name)
        widget.value:setText(tostring(t.value) .. " / " .. tostring(t.max))

        widget.add.onClick = function()
            local protocol = g_game.getProtocolGame()
            if protocol then
                protocol:sendExtendedOpcode(OPCODE, json.encode({
                    action = "add",
                    talent = t.name
                }))
            end
        end

        widget.remove.onClick = function()
            local protocol = g_game.getProtocolGame()
            if protocol then
                protocol:sendExtendedOpcode(OPCODE, json.encode({
                    action = "remove",
                    talent = t.name
                }))
            end
        end
    end
end

local function onTalentOpcode(protocol, opcode, buffer)
    print("[Talents] server response:", buffer)

    local status, payload = pcall(function()
        return json.decode(buffer)
    end)

    if not status or not payload then
        print("[Talents] failed to decode server json")
        return
    end

    if payload.action == "refresh" then
        renderTalents(payload)
    end
end

local function requestTalents()
    local protocol = g_game.getProtocolGame()
    if not protocol then
        print("[Talents] no ProtocolGame yet")
        return
    end

    print("[Talents] requesting data from server")

    protocol:sendExtendedOpcode(OPCODE, json.encode({
        action = "request"
    }))
end

function talentWindowtoggle()
    if talentWindow:isVisible() then
        talentWindow:hide()
        return
    end

    talentWindow:show()
    talentWindow:focus()

    requestTalents()
end

function init()
    talentWindow = g_ui.displayUI("talents")
    talentWindow:hide()

    talentsButton = modules.client_topmenu.addRightGameToggleButton(
        "TalentsButton",
        tr("Talents"),
        "/images/topbuttons/questlog",
        talentWindowtoggle
    )

    ProtocolGame.registerExtendedOpcode(OPCODE, onTalentOpcode)

    print("[Talents] client module loaded")
end

function terminate()
    ProtocolGame.unregisterExtendedOpcode(OPCODE)

    if talentsButton then
        talentsButton:destroy()
        talentsButton = nil
    end

    if talentWindow then
        talentWindow:destroy()
        talentWindow = nil
    end

    print("[Talents] client module unloaded")
end

server side revscript:

LUA:
-- =============================================
-- TALENTS SERVER SIDE
-- TFS 1.4.2 revscript
-- =============================================

local OPCODE = 105

print("[Talents System] Loading...")

local talents = {
    {name = "Life Boost",          icon = "/game_talents/images/health.png",             desc = "Increases maximum health",       value = 35, max = 100},
    {name = "Mana Boost",          icon = "/game_talents/images/mana.png",               desc = "Increases maximum mana",         value = 28, max = 100},
    {name = "Damage Increase",     icon = "/game_talents/images/damageincrease.png",     desc = "Increases all damage dealt",     value = 18, max = 50},
    {name = "Defense Boost",       icon = "/game_talents/images/reduction.png",          desc = "Reduces damage taken",           value = 15, max = 50},
    {name = "Crit Chance",         icon = "/game_talents/images/critical.png",           desc = "Increases critical hit chance",  value = 12, max = 30},
    {name = "Attack Speed",        icon = "/game_talents/images/atkspeed.png",           desc = "Increases attack speed",         value = 22, max = 40},
    {name = "Spell Power",         icon = "/game_talents/images/modsspellpower.png",     desc = "Increases spell power",          value = 14, max = 50},
    {name = "Mana Cost Reduction", icon = "/game_talents/images/manacostreduction.png",  desc = "Reduces mana cost of spells",    value = 18, max = 40},
    {name = "Healing Increase",    icon = "/game_talents/images/healingincrease.png",    desc = "Increases healing received",     value = 20, max = 50},
    {name = "Penetration",         icon = "/game_talents/images/penetration.png",        desc = "Ignores enemy defense",          value = 8,  max = 30},
    {name = "Luck",                icon = "/game_talents/images/luck.png",               desc = "Increases luck and drop rate",   value = 5,  max = 25},
    {name = "Spell Duration",      icon = "/game_talents/images/modsspelltime.png",      desc = "Increases spell duration",       value = 10, max = 40},
    {name = "Mana Buster",         icon = "/game_talents/images/mana_buster.png",        desc = "Reduces enemy mana",             value = 7,  max = 30},
    {name = "Sacred Defense",      icon = "/game_talents/images/sred.png",               desc = "Holy and sacred resistance",     value = 12, max = 40}
}

local function sendTalentData(player)
    local data = {
        action = "refresh",
        data = {
            points = 75,
            tokens = 15,
            talentInfo = talents
        }
    }

    player:sendExtendedOpcode(OPCODE, json.encode(data))
    print("[Talents] Data sent to " .. player:getName())
end

local function onTalentOpcode(player, opcode, buffer)
    if opcode ~= OPCODE then
        return
    end

    print("[Talents] Packet received from " .. player:getName())
    print("[Talents] Buffer: " .. buffer)

    local success, decoded = pcall(function()
        return json.decode(buffer)
    end)

    if not success or not decoded then
        print("[Talents] JSON decode failed")
        return
    end

    if decoded.action == "request" then
        sendTalentData(player)
        return
    end

    if decoded.action == "add" then
        print("[Talents] add requested: " .. tostring(decoded.talent))
        sendTalentData(player)
        return
    end

    if decoded.action == "remove" then
        print("[Talents] remove requested: " .. tostring(decoded.talent))
        sendTalentData(player)
        return
    end

    print("[Talents] Unknown action: " .. tostring(decoded.action))
end

local talentEvent = CreatureEvent("TalentOpcode")

function talentEvent.onExtendedOpcode(player, opcode, buffer)
    onTalentOpcode(player, opcode, buffer)
end

talentEvent:register()

local loginEvent = CreatureEvent("TalentLogin")

function loginEvent.onLogin(player)
    player:registerEvent("TalentOpcode")
    print("[Talents] Registered opcode event for " .. player:getName())
    return true
end

loginEvent:register()

print("[Talents System] Fully Loaded!")

main thing i changed:

your server was waiting for decoded.action == "refresh", but the client should not send refresh. the client should send request, and the server should answer with refresh.

also your old client was never calling sendExtendedOpcode, it was just loading hardcoded talent data locally.

the add/remove part here only sends the action back to the server and refreshes the same static values. if you want the buttons to actually save changes, you’ll need to store/update the talent values server-side.
 
Last edited:
Solution
yeah i think the missing part is that your client window is still being filled locally. it opens the UI and creates the talent rows, but it never actually asks the server for the data.

try this instead:

right now the client opens the window and fills the list by itself. what you want instead is for the client to ask the server for the talent data when the window opens, then let the server send the data back and build the list from that response.

client side otclient mod:

LUA:
-- =============================================
-- TALENTS CLIENT
-- =============================================

local OPCODE = 105
local talentWindow
local talentsButton

local function renderTalents(payload)
    if not talentWindow then
        return
    end

    local data = payload.data
    if not data then
        print("[Talents] no data in server payload")
        return
    end

    talentWindow.talentBalance:setText("Talent Points: " .. tostring(data.points or 0))
    talentWindow.tokensBalance:setText("Talent Tokens: " .. tostring(data.tokens or 0))

    local list = talentWindow.talentList
    list:destroyChildren()

    for _, t in ipairs(data.talentInfo or {}) do
        local widget = g_ui.createWidget("talentTableInfo", list)

        widget.talentTableIcon:setImageSource(t.icon)
        widget.talentTableIcon:setTooltip(t.name .. "\n" .. t.desc)
        widget.talentTableName:setText(t.name)
        widget.value:setText(tostring(t.value) .. " / " .. tostring(t.max))

        widget.add.onClick = function()
            local protocol = g_game.getProtocolGame()
            if protocol then
                protocol:sendExtendedOpcode(OPCODE, json.encode({
                    action = "add",
                    talent = t.name
                }))
            end
        end

        widget.remove.onClick = function()
            local protocol = g_game.getProtocolGame()
            if protocol then
                protocol:sendExtendedOpcode(OPCODE, json.encode({
                    action = "remove",
                    talent = t.name
                }))
            end
        end
    end
end

local function onTalentOpcode(protocol, opcode, buffer)
    print("[Talents] server response:", buffer)

    local status, payload = pcall(function()
        return json.decode(buffer)
    end)

    if not status or not payload then
        print("[Talents] failed to decode server json")
        return
    end

    if payload.action == "refresh" then
        renderTalents(payload)
    end
end

local function requestTalents()
    local protocol = g_game.getProtocolGame()
    if not protocol then
        print("[Talents] no ProtocolGame yet")
        return
    end

    print("[Talents] requesting data from server")

    protocol:sendExtendedOpcode(OPCODE, json.encode({
        action = "request"
    }))
end

function talentWindowtoggle()
    if talentWindow:isVisible() then
        talentWindow:hide()
        return
    end

    talentWindow:show()
    talentWindow:focus()

    requestTalents()
end

function init()
    talentWindow = g_ui.displayUI("talents")
    talentWindow:hide()

    talentsButton = modules.client_topmenu.addRightGameToggleButton(
        "TalentsButton",
        tr("Talents"),
        "/images/topbuttons/questlog",
        talentWindowtoggle
    )

    ProtocolGame.registerExtendedOpcode(OPCODE, onTalentOpcode)

    print("[Talents] client module loaded")
end

function terminate()
    ProtocolGame.unregisterExtendedOpcode(OPCODE)

    if talentsButton then
        talentsButton:destroy()
        talentsButton = nil
    end

    if talentWindow then
        talentWindow:destroy()
        talentWindow = nil
    end

    print("[Talents] client module unloaded")
end

server side revscript:

LUA:
-- =============================================
-- TALENTS SERVER SIDE
-- TFS 1.4.2 revscript
-- =============================================

local OPCODE = 105

print("[Talents System] Loading...")

local talents = {
    {name = "Life Boost",          icon = "/game_talents/images/health.png",             desc = "Increases maximum health",       value = 35, max = 100},
    {name = "Mana Boost",          icon = "/game_talents/images/mana.png",               desc = "Increases maximum mana",         value = 28, max = 100},
    {name = "Damage Increase",     icon = "/game_talents/images/damageincrease.png",     desc = "Increases all damage dealt",     value = 18, max = 50},
    {name = "Defense Boost",       icon = "/game_talents/images/reduction.png",          desc = "Reduces damage taken",           value = 15, max = 50},
    {name = "Crit Chance",         icon = "/game_talents/images/critical.png",           desc = "Increases critical hit chance",  value = 12, max = 30},
    {name = "Attack Speed",        icon = "/game_talents/images/atkspeed.png",           desc = "Increases attack speed",         value = 22, max = 40},
    {name = "Spell Power",         icon = "/game_talents/images/modsspellpower.png",     desc = "Increases spell power",          value = 14, max = 50},
    {name = "Mana Cost Reduction", icon = "/game_talents/images/manacostreduction.png",  desc = "Reduces mana cost of spells",    value = 18, max = 40},
    {name = "Healing Increase",    icon = "/game_talents/images/healingincrease.png",    desc = "Increases healing received",     value = 20, max = 50},
    {name = "Penetration",         icon = "/game_talents/images/penetration.png",        desc = "Ignores enemy defense",          value = 8,  max = 30},
    {name = "Luck",                icon = "/game_talents/images/luck.png",               desc = "Increases luck and drop rate",   value = 5,  max = 25},
    {name = "Spell Duration",      icon = "/game_talents/images/modsspelltime.png",      desc = "Increases spell duration",       value = 10, max = 40},
    {name = "Mana Buster",         icon = "/game_talents/images/mana_buster.png",        desc = "Reduces enemy mana",             value = 7,  max = 30},
    {name = "Sacred Defense",      icon = "/game_talents/images/sred.png",               desc = "Holy and sacred resistance",     value = 12, max = 40}
}

local function sendTalentData(player)
    local data = {
        action = "refresh",
        data = {
            points = 75,
            tokens = 15,
            talentInfo = talents
        }
    }

    player:sendExtendedOpcode(OPCODE, json.encode(data))
    print("[Talents] Data sent to " .. player:getName())
end

local function onTalentOpcode(player, opcode, buffer)
    if opcode ~= OPCODE then
        return
    end

    print("[Talents] Packet received from " .. player:getName())
    print("[Talents] Buffer: " .. buffer)

    local success, decoded = pcall(function()
        return json.decode(buffer)
    end)

    if not success or not decoded then
        print("[Talents] JSON decode failed")
        return
    end

    if decoded.action == "request" then
        sendTalentData(player)
        return
    end

    if decoded.action == "add" then
        print("[Talents] add requested: " .. tostring(decoded.talent))
        sendTalentData(player)
        return
    end

    if decoded.action == "remove" then
        print("[Talents] remove requested: " .. tostring(decoded.talent))
        sendTalentData(player)
        return
    end

    print("[Talents] Unknown action: " .. tostring(decoded.action))
end

local talentEvent = CreatureEvent("TalentOpcode")

function talentEvent.onExtendedOpcode(player, opcode, buffer)
    onTalentOpcode(player, opcode, buffer)
end

talentEvent:register()

local loginEvent = CreatureEvent("TalentLogin")

function loginEvent.onLogin(player)
    player:registerEvent("TalentOpcode")
    print("[Talents] Registered opcode event for " .. player:getName())
    return true
end

loginEvent:register()

print("[Talents System] Fully Loaded!")

main thing i changed:

your server was waiting for decoded.action == "refresh", but the client should not send refresh. the client should send request, and the server should answer with refresh.

also your old client was never calling sendExtendedOpcode, it was just loading hardcoded talent data locally.

the add/remove part here only sends the action back to the server and refreshes the same static values. if you want the buttons to actually save changes, you’ll need to store/update the talent values server-side.
Now i understand what you meant before. You're a got damn wizard harry, hehe, thank you so much explaining. :D
 
yeah i think the missing part is that your client window is still being filled locally. it opens the UI and creates the talent rows, but it never actually asks the server for the data.

try this instead:

right now the client opens the window and fills the list by itself. what you want instead is for the client to ask the server for the talent data when the window opens, then let the server send the data back and build the list from that response.

client side otclient mod:

LUA:
-- =============================================
-- TALENTS CLIENT
-- =============================================

local OPCODE = 105
local talentWindow
local talentsButton

local function renderTalents(payload)
    if not talentWindow then
        return
    end

    local data = payload.data
    if not data then
        print("[Talents] no data in server payload")
        return
    end

    talentWindow.talentBalance:setText("Talent Points: " .. tostring(data.points or 0))
    talentWindow.tokensBalance:setText("Talent Tokens: " .. tostring(data.tokens or 0))

    local list = talentWindow.talentList
    list:destroyChildren()

    for _, t in ipairs(data.talentInfo or {}) do
        local widget = g_ui.createWidget("talentTableInfo", list)

        widget.talentTableIcon:setImageSource(t.icon)
        widget.talentTableIcon:setTooltip(t.name .. "\n" .. t.desc)
        widget.talentTableName:setText(t.name)
        widget.value:setText(tostring(t.value) .. " / " .. tostring(t.max))

        widget.add.onClick = function()
            local protocol = g_game.getProtocolGame()
            if protocol then
                protocol:sendExtendedOpcode(OPCODE, json.encode({
                    action = "add",
                    talent = t.name
                }))
            end
        end

        widget.remove.onClick = function()
            local protocol = g_game.getProtocolGame()
            if protocol then
                protocol:sendExtendedOpcode(OPCODE, json.encode({
                    action = "remove",
                    talent = t.name
                }))
            end
        end
    end
end

local function onTalentOpcode(protocol, opcode, buffer)
    print("[Talents] server response:", buffer)

    local status, payload = pcall(function()
        return json.decode(buffer)
    end)

    if not status or not payload then
        print("[Talents] failed to decode server json")
        return
    end

    if payload.action == "refresh" then
        renderTalents(payload)
    end
end

local function requestTalents()
    local protocol = g_game.getProtocolGame()
    if not protocol then
        print("[Talents] no ProtocolGame yet")
        return
    end

    print("[Talents] requesting data from server")

    protocol:sendExtendedOpcode(OPCODE, json.encode({
        action = "request"
    }))
end

function talentWindowtoggle()
    if talentWindow:isVisible() then
        talentWindow:hide()
        return
    end

    talentWindow:show()
    talentWindow:focus()

    requestTalents()
end

function init()
    talentWindow = g_ui.displayUI("talents")
    talentWindow:hide()

    talentsButton = modules.client_topmenu.addRightGameToggleButton(
        "TalentsButton",
        tr("Talents"),
        "/images/topbuttons/questlog",
        talentWindowtoggle
    )

    ProtocolGame.registerExtendedOpcode(OPCODE, onTalentOpcode)

    print("[Talents] client module loaded")
end

function terminate()
    ProtocolGame.unregisterExtendedOpcode(OPCODE)

    if talentsButton then
        talentsButton:destroy()
        talentsButton = nil
    end

    if talentWindow then
        talentWindow:destroy()
        talentWindow = nil
    end

    print("[Talents] client module unloaded")
end

server side revscript:

LUA:
-- =============================================
-- TALENTS SERVER SIDE
-- TFS 1.4.2 revscript
-- =============================================

local OPCODE = 105

print("[Talents System] Loading...")

local talents = {
    {name = "Life Boost",          icon = "/game_talents/images/health.png",             desc = "Increases maximum health",       value = 35, max = 100},
    {name = "Mana Boost",          icon = "/game_talents/images/mana.png",               desc = "Increases maximum mana",         value = 28, max = 100},
    {name = "Damage Increase",     icon = "/game_talents/images/damageincrease.png",     desc = "Increases all damage dealt",     value = 18, max = 50},
    {name = "Defense Boost",       icon = "/game_talents/images/reduction.png",          desc = "Reduces damage taken",           value = 15, max = 50},
    {name = "Crit Chance",         icon = "/game_talents/images/critical.png",           desc = "Increases critical hit chance",  value = 12, max = 30},
    {name = "Attack Speed",        icon = "/game_talents/images/atkspeed.png",           desc = "Increases attack speed",         value = 22, max = 40},
    {name = "Spell Power",         icon = "/game_talents/images/modsspellpower.png",     desc = "Increases spell power",          value = 14, max = 50},
    {name = "Mana Cost Reduction", icon = "/game_talents/images/manacostreduction.png",  desc = "Reduces mana cost of spells",    value = 18, max = 40},
    {name = "Healing Increase",    icon = "/game_talents/images/healingincrease.png",    desc = "Increases healing received",     value = 20, max = 50},
    {name = "Penetration",         icon = "/game_talents/images/penetration.png",        desc = "Ignores enemy defense",          value = 8,  max = 30},
    {name = "Luck",                icon = "/game_talents/images/luck.png",               desc = "Increases luck and drop rate",   value = 5,  max = 25},
    {name = "Spell Duration",      icon = "/game_talents/images/modsspelltime.png",      desc = "Increases spell duration",       value = 10, max = 40},
    {name = "Mana Buster",         icon = "/game_talents/images/mana_buster.png",        desc = "Reduces enemy mana",             value = 7,  max = 30},
    {name = "Sacred Defense",      icon = "/game_talents/images/sred.png",               desc = "Holy and sacred resistance",     value = 12, max = 40}
}

local function sendTalentData(player)
    local data = {
        action = "refresh",
        data = {
            points = 75,
            tokens = 15,
            talentInfo = talents
        }
    }

    player:sendExtendedOpcode(OPCODE, json.encode(data))
    print("[Talents] Data sent to " .. player:getName())
end

local function onTalentOpcode(player, opcode, buffer)
    if opcode ~= OPCODE then
        return
    end

    print("[Talents] Packet received from " .. player:getName())
    print("[Talents] Buffer: " .. buffer)

    local success, decoded = pcall(function()
        return json.decode(buffer)
    end)

    if not success or not decoded then
        print("[Talents] JSON decode failed")
        return
    end

    if decoded.action == "request" then
        sendTalentData(player)
        return
    end

    if decoded.action == "add" then
        print("[Talents] add requested: " .. tostring(decoded.talent))
        sendTalentData(player)
        return
    end

    if decoded.action == "remove" then
        print("[Talents] remove requested: " .. tostring(decoded.talent))
        sendTalentData(player)
        return
    end

    print("[Talents] Unknown action: " .. tostring(decoded.action))
end

local talentEvent = CreatureEvent("TalentOpcode")

function talentEvent.onExtendedOpcode(player, opcode, buffer)
    onTalentOpcode(player, opcode, buffer)
end

talentEvent:register()

local loginEvent = CreatureEvent("TalentLogin")

function loginEvent.onLogin(player)
    player:registerEvent("TalentOpcode")
    print("[Talents] Registered opcode event for " .. player:getName())
    return true
end

loginEvent:register()

print("[Talents System] Fully Loaded!")

main thing i changed:

your server was waiting for decoded.action == "refresh", but the client should not send refresh. the client should send request, and the server should answer with refresh.

also your old client was never calling sendExtendedOpcode, it was just loading hardcoded talent data locally.

the add/remove part here only sends the action back to the server and refreshes the same static values. if you want the buttons to actually save changes, you’ll need to store/update the talent values server-side.
If i want to display another window like a reset window for the talentpoints, so a confirmation window, do i still send a request to the server and server then asks to open the window? Or thats unnecessary?
 
If i want to display another window like a reset window for the talentpoints, so a confirmation window, do i still send a request to the server and server then asks to open the window? Or thats unnecessary?
nah, i wouldn’t make the server tell the client to open the confirmation window.

the confirmation window is just ui, so keep that client-side. only send something to the server after the player clicks yes/confirm.

so the flow would be:

player clicks reset -> client opens confirm window -> player clicks yes -> client sends reset opcode -> server checks/does the reset -> server sends back refresh

something like this on the client:

LUA:
local function sendResetTalents()
    local protocol = g_game.getProtocolGame()
    if not protocol then
        print("[Talents] no ProtocolGame")
        return
    end

    protocol:sendExtendedOpcode(OPCODE, json.encode({
        action = "reset"
    }))
end

function openResetTalentWindow()
    displayGeneralBox(
        "Reset Talents",
        "Are you sure you want to reset your talents?",
        {
            {
                text = "Yes",
                callback = function()
                    sendResetTalents()
                end
            },
            {
                text = "No",
                callback = function()
                end
            }
        }
    )
end

then server side you just handle it like another action:

LUA:
if decoded.action == "reset" then
    print("[Talents] reset requested by " .. player:getName())

    -- reset the player's talent values here
    -- refund points / update storage or database here

    sendTalentData(player)
    return
end

the important part is: don’t trust the client to actually reset anything. the client only asks. the server should do the real reset, validate it, save it, then send the updated talent data back.
 
LUA:
if decoded.action == "reset" then
    print("[Talents] reset requested by " .. player:getName())

    -- reset the player's talent values here
    -- refund points / update storage or database here

    sendTalentData(player)
    return
end

and here shouldbe also a server side check if player meet the conditions about reset, never from buffer

example:
- client sending 20 red dragon scale (this value can be manipulated from client side even if your client is "protected")
in func where server receives the information u gotta check if its true that player have these items
 
nah, i wouldn’t make the server tell the client to open the confirmation window.

the confirmation window is just ui, so keep that client-side. only send something to the server after the player clicks yes/confirm.

so the flow would be:

player clicks reset -> client opens confirm window -> player clicks yes -> client sends reset opcode -> server checks/does the reset -> server sends back refresh

something like this on the client:

LUA:
local function sendResetTalents()
    local protocol = g_game.getProtocolGame()
    if not protocol then
        print("[Talents] no ProtocolGame")
        return
    end

    protocol:sendExtendedOpcode(OPCODE, json.encode({
        action = "reset"
    }))
end

function openResetTalentWindow()
    displayGeneralBox(
        "Reset Talents",
        "Are you sure you want to reset your talents?",
        {
            {
                text = "Yes",
                callback = function()
                    sendResetTalents()
                end
            },
            {
                text = "No",
                callback = function()
                end
            }
        }
    )
end

then server side you just handle it like another action:

LUA:
if decoded.action == "reset" then
    print("[Talents] reset requested by " .. player:getName())

    -- reset the player's talent values here
    -- refund points / update storage or database here

    sendTalentData(player)
    return
end

the important part is: don’t trust the client to actually reset anything. the client only asks. the server should do the real reset, validate it, save it, then send the updated talent data back.

LUA:
if decoded.action == "reset" then
    print("[Talents] reset requested by " .. player:getName())

    -- reset the player's talent values here
    -- refund points / update storage or database here

    sendTalentData(player)
    return
end

and here shouldbe also a server side check if player meet the conditions about reset, never from buffer

example:
- client sending 20 red dragon scale (this value can be manipulated from client side even if your client is "protected")
in func where server receives the information u gotta check if its true that player have these items
Thank you both, think im on the right direction now, this is fun af to play around with but yeah i noticed fast you could manipulate a lot if you're not carefull to validate it correctly.
 

Similar threads

Back
Top