Modding Weapon Glow & Gem Sockets

Panda

Chaos Pirates
Developer
Server Owner
Registered
LV
0
 
Joined
May 28, 2026
Messages
31
Reaction score
21
Points
13
Location
Chaos Tower
Website
chaospirates.com
Modding Weapon Glows & Gem Sockets in Tales of Pirates (DX9)

A complete, code-verified guide to the two things people always ask about: why a weapon glows (and how to add / change / escalate it), and how gem sockets work. It also covers the parts the classic guides leave out — the level-escalation layer, and an engine init-order bug that makes perfectly-correct glow data render nothing.

Part 1 - Weapon glows / effects

A weapon's glow is built from three independent layers. Older guides cover the first two; the third (escalation) is under-documented, and a separate engine bug can stop even perfect data from rendering. All three are below.

The resolve chain (driven by the socketed gems):
Code:
socketed gems' COLORS
  -> Item_Stoneeffect(c1,c2,c3)      (scripts.lua)  -> index 1..14
     -> ItemRefineInfo.txt           (per weapon: a glow id per color slot + size floats)
        -> ItemRefineEffectInfo.txt  (glow id -> sceneffect id(s) + light id, per char & dummy)
           -> sceneffectinfo.txt     (sceneffect id -> .par particle file)
              -> Client/effect/*.par

Myth-buster: some guides say "gems only add stats; the glow is forge-only." That is wrong. The socketed gems drive the glow: their COLORS pick which glow shows, their combined LEVEL picks the intensity tier. The "+N" on the weapon name is the sum of socketed gem levels.

Layer 1 - COLOR (which glow) = the gems' colors
  • StoneInfo.txt col5 = the gem's color: 1=Red, 2=Blue, 3=Green, 4=Yellow. Row legend: ID | Gem Name | Gem ItemID | ForgeIntoItemType(csv) | GemColorGlow | hint [| hexcolor].
  • The client feeds the 3 socketed gems' colors into Lua Item_Stoneeffect(c1,c2,c3) -> an index 1-14 -> that picks a column in the weapon's ItemRefineInfo row (col0=red, col1=blue, col3=yellow, the rest = color mixes). Mixed colors -> a combo slot, so one weapon can show multiple colors at once.
  • The weapon model also gates which glows can render - some effects only show on certain models.

Layer 2 - LOOK / SIZE = ItemRefineInfo + ItemRefineEffectInfo
Code:
ItemRefineInfo row:  ItemID | Name | Eff1..Eff14 (glow id per color slot) | sizeLance sizeCarsise sizePhyllis sizeAmi
   e.g.  5283  Barborosa's Knife   3 3 0 12 12 0 3 0 3 0 0 3 0 0   0.61 0.61 0.61 0.61
  • The 14 columns are glow ids keyed by color slot; set them all equal to force ONE glow regardless of gem color.
  • The 4 trailing floats are static per-character glow SIZE (0.1 tiny ... 2.6 giant).
  • ItemRefineInfo only ASSIGNS a glow id; ItemRefineEffectInfo STORES the actual glow. One ItemRefineEffectInfo row can hold several simultaneous effect slots (centre orb + spinning ring + blade trail + tip-balls), each with its sceneffect id, a light id (nLightID = the blade-texture glow; 0 = none), and a dummy/bone for attachment.

Layer 3 - LEVEL ESCALATION (the part the classic guides omit)
  • The client renders each effect as sEffectID * 10 + tier. So base effect 315 renders as 3150 / 3151 / 3152 / 3153 for tiers 0/1/2/3 - these must be four distinct sceneffectinfo entries, each pointing at its own .par.
  • tier = (combinedGemLevel - 1) / N, capped at 3 (4 tiers). Official Tales of Pirates steps at +4 / +7 / +10 = (nLevel-1)/3. Most shared private-server source ships (nLevel-1)/4 = +5 / +9 / +13; change that one divisor in SItemForge::Refresh (UIItemCommand.cpp) to match retail.

The trap that bites everyone (reserved ID range): sceneffect IDs 1000-3000 are reserved by the Magic / skill system. Register a weapon glow there and it collides with a spell (or renders nothing). Use the 300-400 band (the stock weapon glows) or 4000+ for brand-new effects.

"My data is correct but the glow shows nothing / never gets bigger" - the engine init-order bug
This one is in no older guide. If your color/size data AND your per-tier .par files are all correct, but the weapon still shows no glow or the glow never escalates with level, check the client error log for:
Code:
ERROR  msgSceneItem SetForgeEffect effect fail,ID 3151
That means CEffectObj::Create(sEffectID*10+tier) failed - the particle isn't in the effect registry even though the .par exists on disk. Root cause in the shared DX9 client: the engine scans effect\*.par into the resource manager inside LoadTotalPartCtrl(), but that runs before the effect path (_pszEFFectPath) is set, so the scan hits an empty path and registers nothing. Every particle effect (forge glows - and often skills, melee, fairies) then fails to resolve.
Fix: re-run the .par scan once a real in-world character exists (the path is set by then) - i.e. re-invoke the InitRes3() / LoadTotalPartCtrl() scan on first character-create / equip. After that, Create() resolves and the tiers render. If you cannot rebuild the engine DLL, trigger the one-time re-scan from the game layer on the first in-world frame.

Filebase caveat: different bases (mothannakh, alex, corsairs, top-recode, ...) ship these pieces in different states - some have the data but not the render path, some the reverse. Before expecting tiered glows, verify the whole chain: (a) the sceneffectinfo per-tier variants (base*10 + 0..3); (b) the tier formula in SItemForge::Refresh; (c) Item_Stoneeffect in scripts.lua; (d) populated ItemRefineInfo columns + ItemRefineEffectInfo rows for the weapons you care about.

Two more "no escalation" gotchas:
  • A gem missing from the client StoneInfo doesn't count. The client only sums a slot's level if the gem resolves in StoneInfo - a gem missing/misaligned there contributes neither its COLOR (wrong/empty glow) nor its LEVEL (tier never climbs).
  • Live refresh: the glow refresh early-returns when the forge value is unchanged - re-equip or relog to force a re-evaluate after forging.

How to (glows):
  • Give a glow to a weapon that has none: point that weapon's ItemRefineInfo color columns at an existing glow id (copy a glowing weapon's row).
  • Change a glow: swap the glow id in its ItemRefineInfo row, or repoint that glow's sceneffect id in ItemRefineEffectInfo.
  • Resize: edit the 4 size floats at the end of the ItemRefineInfo row (0.1 ... 2.6).
  • Force one glow for any gem combo: set all 14 color columns equal.

Part 2 - Gem sockets (compatibility, new gems, stats)

Two layers run this: the server decides whether a gem may go into a piece and computes the stat; the client shows the socket UI and which gems are valid.

The server side - GemVar (the source of truth) - in resource/script/calculate/variable.lua:
Code:
GemVar[1] = {ID = 878, Level = 6, Type = 1, Effect = 4, Attribute = ITEMATTR_VAL_MNATK, Equip = {1,0}}
GemVar[2] = {ID = 879, Level = 6, Type = 1, Effect = 6, Attribute = ITEMATTR_VAL_MNATK, Equip = {2,3,4,7,9,0}}
  • ID = the gem's ItemInfo id
  • Level = max gem level
  • Effect = stat per level -> final bonus = Effect x gemLevel
  • Attribute = which stat (ITEMATTR_VAL_STR/AGI/CON/DEF/HIT/...)
  • Equip = {...,0} = the list of equipment TYPES this gem may socket into (0-terminated). This is the real compatibility gate.

The check, in forge.lua:
Code:
function CheckStoneType(Item, Stone1, Stone2)
    ...
    for a = 1, #GemVar do
        if GemVar[a].ID == GetItemID(Stone1) then
            for b = 0, #GemVar[a].Equip do
                if GemVar[a].Equip[b] == GetItemType(Item) then return 1 end  -- allowed
                if GemVar[a].Equip[b] == 0 then return 0 end                  -- end of list -> denied
            end
        end
    end
    return 0
end
So a gem fits a piece if and only if the piece's ItemInfo type is in that gem's Equip list.

Common equipment type numbers:
1=1H weapon, 2=2H weapon, 3=bow, 4=gun, 7=dagger, 9=staff/wand, 11=chest, 22=head, 23=accessory, 24=boots, 27=gloves.

The client side - StoneInfo.txt (server resource/ + client scripts/table/):
Code:
index | name        | itemID | equipTypes  | colorGlow | hintID            | colorHex
1       Fiery Gem      0878     1             1          ItemHint_LieYanS    F6D243
2       Furious Gem    0879     2,3,4,7,9     4          ItemHint_ZhiYanS    F6D243
equipTypes here is the client UI mirror of GemVar.Equip (col5 = the COLOR used by Part 1's glow). Keep these in sync with the server or the UI and the server will disagree.

Sockets on the equipment: a piece's socket count lives in its ItemInfo.txt row (the max-holes column). Gems themselves are ItemInfo type 49.

How to (gems):
  • Add a brand-new gem:
    • ItemInfo.txt - add the gem item (type 49) with a name/icon.
    • StoneInfo.txt - add a row: itemID, the equipTypes it fits, its color, hint, colour (client).
    • variable.lua - add GemVar[n] = {ID=<itemId>, Level=..., Effect=..., Attribute=ITEMATTR_VAL_..., Equip={types...,0}} (server stat + gate).
  • Let a gem socket into gear it currently can't (e.g. a weapon-only gem into armour): add the new type numbers to both GemVar[n].Equip (server) and the gem's equipTypes in StoneInfo.txt (client). Example - allow Furious Gem (0879) into chest/head/gloves: Equip = {2,3,4,7,9,11,22,27,0} and equipTypes = 2,3,4,7,9,11,22,27.
  • Give a piece more sockets: raise its socket-count column in ItemInfo.txt.

Applying your changes (gotchas)
  • Server reads the .txt tables on boot; the client reads compiled .bin. After editing client tables (StoneInfo, ItemInfo, the refine tables) recompile them so the client matches, and restart the GameServer for server-side .txt/Lua (variable.lua, forge.lua).
  • Edit BOTH copies of any shared table (server resource/ and client scripts/table/) - they are separate files.
  • GemVar.Equip is the server's authority - if the UI lets you try but the socket "fails", it is almost always a GemVar.Equip mismatch.
  • Weapon glows are gem-driven - color (gem colors) + intensity (combined gem level). If a glow won't escalate and your data is right, it is the engine init-order bug above - check the client log for "SetForgeEffect effect fail".
  • Don't use sceneffect IDs 1000-3000 for glows (Magic system).
 
  • Like
Reactions: zLuke
Anyone using mothanna's, yatops, spidpex, another version of corsairs, or Alex's files (maybe he fixed it?) should take a peak if they are having these issues.
 
Thanks for the detailed guide, btw I am not sure if you tried this but I was curious on how I could make new shape glow/effect glow.

From what I tested the original shape glows r 1-4, idk how to edit 5 = black/6 = pink etc.
As for effect glow it is pretty simple just recolor texture since all eff/par used r same but if shape glow won't change it will look garbage, obviously u can disable shape glow but then still your weapon would look like shit.
 
From what I tested the original shape glows r 1-4, idk how to edit 5 = black/6 = pink etc.
To add more types of glow colors, you must first add them to the Client\scripts\txt\item.lit file.

Copy the json file and python script to the Client\scripts\txt folder.
Add your glow to the json file as it is done for the purple color and compile the item.lit file with the python script.

You also need to create and add the purple.tga texture to Client\texture\item

JSON:
{
  "header": {
    "version": 1,
    "type": 1,
    "mask": [ 0, 0, 0, 0 ]
  },
  "items": [
    {
      "id": 0,
      "lit": []
    },
    {
      "id": 1,
      "descriptor": "red",
      "lit": [
        { "id": 0, "file": "red.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 1, "file": "red.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 2, "file": "red.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 3, "file": "red.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 }
      ]
    },
    {
      "id": 2,
      "descriptor": "blue",
      "lit": [
        { "id": 0, "file": "blue.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 1, "file": "blue.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 2, "file": "blue.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 3, "file": "blue.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 }
      ]
    },
    {
      "id": 3,
      "descriptor": "yellow",
      "lit": [
        { "id": 0, "file": "yellow.TGA", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 1, "file": "yellow.TGA", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 2, "file": "yellow.TGA", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 3, "file": "yellow.TGA", "anim_type": 5, "transp_type": 1, "opacity": 1.0 }
      ]
    },
    {
      "id": 4,
      "descriptor": "green",
      "lit": [
        { "id": 0, "file": "green.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 1, "file": "green.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 2, "file": "green.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 3, "file": "green.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 }
      ]
    },
    {
        "id": 5,
        "descriptor": "purple",
        "lit": [
        { "id": 0, "file": "purple.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 1, "file": "purple.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 2, "file": "purple.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 },
        { "id": 3, "file": "purple.tga", "anim_type": 5, "transp_type": 1, "opacity": 1.0 }
        ]
    }
  ]
}

Python:
#!/usr/bin/env python3

from __future__ import annotations

import json
import struct
import sys
from pathlib import Path
from typing import Any, BinaryIO


SCRIPT_DIR = Path(__file__).resolve().parent
INPUT_JSON = SCRIPT_DIR / "item.lit.clean.json"
OUTPUT_LIT = SCRIPT_DIR / "item.lit"

LW_MAX_FILE = 128
LW_MAX_NAME = 64
STRING_ENCODING = "cp1251"
FILE_HEADER_FMT = "<6I"  # version, type, mask[4]
LIT_STRUCT_FMT = f"<I{LW_MAX_FILE}sIIf"


def _encode_fixed(
    text: str,
    size: int,
    raw_hex: str | None = None,
) -> bytes:
    if raw_hex:
        raw = bytes.fromhex(raw_hex)
        if len(raw) != size:
            raise ValueError(f"raw hex length mismatch: expected {size}, got {len(raw)}")
        return raw

    b = text.encode(STRING_ENCODING, errors="replace")
    if len(b) >= size:
        b = b[: size - 1]
    return b + (b"\x00" * (size - len(b)))


def _write_u32(fp: BinaryIO, value: int) -> None:
    fp.write(struct.pack("<I", value & 0xFFFFFFFF))


def compile_item_lit(input_json: Path, output_bin: Path) -> None:
    with input_json.open("r", encoding="utf-8") as fp:
        data: dict[str, Any] = json.load(fp)

    header = data.get("header", {})
    version = int(header.get("version", 1))
    type_ = int(header.get("type", 1))
    mask = header.get("mask", [0, 0, 0, 0])
    if not isinstance(mask, list) or len(mask) != 4:
        raise ValueError("header.mask must be an array of 4 integers")
    m0, m1, m2, m3 = (int(mask[0]), int(mask[1]), int(mask[2]), int(mask[3]))

    items = data.get("items", [])
    if not isinstance(items, list):
        raise ValueError("items must be an array")

    with output_bin.open("wb") as fp:
        fp.write(struct.pack(FILE_HEADER_FMT, version, type_, m0, m1, m2, m3))
        _write_u32(fp, len(items))

        for item in items:
            if not isinstance(item, dict):
                raise ValueError("Each item must be an object")

            item_id = int(item.get("id", 0))
            descriptor = str(item.get("descriptor", ""))
            descriptor_raw_hex = item.get("descriptor_raw_hex")
            item_file = str(item.get("file", ""))
            file_raw_hex = item.get("file_raw_hex")
            lit_arr = item.get("lit", [])
            if not isinstance(lit_arr, list):
                raise ValueError("item.lit must be an array")

            _write_u32(fp, item_id)
            fp.write(
                _encode_fixed(
                    descriptor,
                    LW_MAX_NAME,
                    descriptor_raw_hex if isinstance(descriptor_raw_hex, str) else None,
                )
            )
            fp.write(
                _encode_fixed(
                    item_file,
                    LW_MAX_FILE,
                    file_raw_hex if isinstance(file_raw_hex, str) else None,
                )
            )
            _write_u32(fp, len(lit_arr))

            for lit in lit_arr:
                if not isinstance(lit, dict):
                    raise ValueError("Each lit entry must be an object")

                lit_id = int(lit.get("id", 0))
                lit_file = str(lit.get("file", ""))
                lit_file_raw_hex = lit.get("file_raw_hex")
                anim_type = int(lit.get("anim_type", 0))
                transp_type = int(lit.get("transp_type", 0))

                if "opacity_u32" in lit:
                    opacity_u32 = int(lit["opacity_u32"]) & 0xFFFFFFFF
                    opacity = struct.unpack("<f", struct.pack("<I", opacity_u32))[0]
                else:
                    opacity = float(lit.get("opacity", 0.0))

                packed = struct.pack(
                    LIT_STRUCT_FMT,
                    lit_id & 0xFFFFFFFF,
                    _encode_fixed(
                        lit_file,
                        LW_MAX_FILE,
                        lit_file_raw_hex if isinstance(lit_file_raw_hex, str) else None,
                    ),
                    anim_type & 0xFFFFFFFF,
                    transp_type & 0xFFFFFFFF,
                    opacity,
                )
                fp.write(packed)


def main() -> int:
    if not INPUT_JSON.exists():
        print(f"Input JSON not found: {INPUT_JSON}", file=sys.stderr)
        return 1

    compile_item_lit(INPUT_JSON, OUTPUT_LIT)
    print(f"Input:  {INPUT_JSON}")
    print(f"Output: {OUTPUT_LIT}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
 

Attachments

  • Like
Reactions: Panda