Skip to content

fix: Hirelings load - #933

Merged
jprzimba merged 1 commit into
mainfrom
hirelings
Sep 10, 2026
Merged

jprzimba merged 1 commit into
mainfrom
hirelings

Conversation

@jprzimba

@jprzimba jprzimba commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes #929

@jprzimba
jprzimba merged commit 01f51c8 into main Sep 10, 2026
16 checks passed
@jprzimba
jprzimba deleted the hirelings branch September 10, 2026 20:30
jprzimba added a commit that referenced this pull request Sep 18, 2026
…rket::processWebOrders) (#909)

* feat(core): add database player concurrency lock to prevent web/game race conditions

* fix(core): improve lock expiration with db timestamp and fix config sorting

* feat(schema): add migration 64 for player concurrency lock columns and index

* fix(core): use int64_t for lock timestamps and bound future timestamp window

* style(migrations): format migration 64 with StyLua

* chore(schema): update db_version to 64 in schema.sql

* feat(market): add asynchronous web market order queue processor (IOMarket::processWebOrders)

* fix(market): use async db task, getPlayerByGUID, and native inbox delivery with tier preservation

* fix(market): fix tier use-after-free, add world_id scoping, refund rollback, and migrations 64/65

* style: format Lua and C++ files according to StyLua and Clang-Format

* chore(schema): update db_version to 65 in schema.sql

* fix(market): include missing headers in iomarket.cpp for non-unity debug builds

* fix: soul cores (#910)

* fix: soul cores

Added missing soul cores and fixed the article a/an depending on the consonant or vocal porplery. Fixed description in some.

* Update items.xml

* fix: register the !time talkaction (#913)

#904 rewrote the command body and renamed the callback target, but left
the declaration as `local statusTime`:

    local statusTime = TalkAction("!time")
    function timeTalkAction.onSay(player, words, param)

`timeTalkAction` is never declared, so indexing it throws and the error
escapes the main chunk. statusTime:groupType() and statusTime:register()
on the last two lines never run, so !time is not registered at all --
the command silently does nothing in game and the only symptom is a Lua
error at startup.

Point the callback back at the declared local. The rewritten body is
unchanged and correct: getTibiaTimerDayOrNight() and
getFormattedWorldTime() both take no arguments and read world state
themselves.

Tests: loaded the script under LuaJIT with a stubbed TalkAction --
before, 0 talkactions registered; after, 1 (!time). Booted the server
and confirmed the startup log goes from the Lua error to 0 errors.

* fix(market): include missing account enum headers in iomarket.cpp for debug builds

* fix: vcpkg path setup for MSBuild (#916)

Add conditional imports for the vcpkg MSBuild props and targets files, and make the installed package directory absolute to the project root. Update protobuf tool and source paths to use the correct Windows executable name and relative project paths so local builds and CI resolve vcpkg and protoc reliably.

* fix: gold exploit on bow and inkwell NPC prices (#914)

* fix: gold exploit on bow and inkwell NPC prices

Two NPCs bought an item back for more than another NPC sold it for, so
players could buy and sell in a loop for unlimited gold. The engine
mints the payout in Npc::onPlayerSellItem and NPCs have no gold pool,
so nothing bounded it.

Perod bought bows for 400, the same price every NPC sells them for,
while Avriel and Aurelia sell at 350 -- 50 gold per bow. TibiaWiki
lists Perod under the bow's sellto as "Perod: 100", matching the ~20
other NPCs, so his sell price is now 100.

The Librarian bought inkwells for 15 while every other NPC sells them
for 10 -- 5 gold each. TibiaWiki lists it under the inkwell's sellto
with no override, which means the item's npcvalue of 9, so its sell
price is now 9. Its buy price of 20 matches the wiki override and is
unchanged.

Both entries date back to the initial source import and neither is
gated by storage.

Tests: booted the server and confirmed the price check reports both
items before the change and nothing after. Cross-checked every other
NPC price for these two items against the wiki; the rest already match.

* fix: evaluate the NPC shop price check after all NPCs load

The check compared the most recently seen buy price against the most
recently seen sell price, overwriting both as it walked the shop lists.
Whether it fired at all depended on NPC load order, and the NPC names it
printed were whichever happened to be seen last rather than the ones a
player would actually use.

It missed the inkwell exploit completely and reported the bow one
against Avriel when Aurelia offers the same price. Replaying the old
algorithm in alphabetical order reports nothing at all.

Record the cheapest buy and the most valuable sell per item instead,
then evaluate once from the Server Initialization startup event, after
every NPC is registered. Those two extremes are what a player would
actually use, so the result no longer depends on load order. The message
now names the real pair and the profit per trade.

Tests: with the bow and inkwell prices reverted, the check reports both
exploits with correct NPCs and amounts; with them corrected it reports
nothing.

* update: the new frontier quest (#900)

* update: the new frontier quest

The Mission 05 is just way too fucked out. A total waste of time doing this for nothing, not rpg or fun at all. So I've decided to fix this mission in the following way:

Despite your interaction, you need the item to preasude a positive reply with the npc, else you just get rejected. Any keyword will work if you have the item. This will add some quality of life to open tibia despite is not real tibia content, but who cares when this mission in a pain in the but not for the dificulty but the messed twisted ways.

I hope it's merged, if not use it to improve your gameplay if ur server is rpg.

* Lua code format - (Stylua)

* lua format

* fix: storage call

check storages.lua, the call is incorrect

* update: dialogs

add dialogs to npcs

* update telas

Updated npc with shadows of yalahar quest

* Lua code format - (Stylua)

* Update telas.lua

* Lua code format - (Stylua)

* Update telas.lua

* Update telas.lua

* Update telas.lua

* fix nunchaku id

sorry, copy-paste problem form old pr

---------

Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>

* fix: DBResult::getNumber returns 0 for time_t on macOS (#911)

getNumber dispatched on exact types (std::is_same_v<T, int64_t>). That
holds on LP64 Linux, where int64_t, long and time_t are all the same
type, but not elsewhere: on macOS int64_t is long long while time_t is
long, so getNumber<time_t> matched no branch, logged "Invalid signed
type T" and returned a default-constructed 0.

Every time_t column read silently came back as zero -- lastlogin,
lastlogout, skulltime, ban timestamps and premium expiry -- with no
crash and no failed query. A fresh boot logged the error 6475 times.

Dispatch on width instead of exact type so any integral type maps to a
conversion function that can represent it. The std::is_integral_v guard
keeps floating-point types on the existing error path. On LP64 this is
behaviour-preserving: the same conversions are selected as before.

Tests: no automated coverage for DBResult; verified by running the
server against MariaDB 12.3 and confirming the 6475 errors drop to 0
and player timestamps load correctly.

* feat: macOS build support (#912)

* fix: DBResult::getNumber returns 0 for time_t on macOS

getNumber dispatched on exact types (std::is_same_v<T, int64_t>). That
holds on LP64 Linux, where int64_t, long and time_t are all the same
type, but not elsewhere: on macOS int64_t is long long while time_t is
long, so getNumber<time_t> matched no branch, logged "Invalid signed
type T" and returned a default-constructed 0.

Every time_t column read silently came back as zero -- lastlogin,
lastlogout, skulltime, ban timestamps and premium expiry -- with no
crash and no failed query. A fresh boot logged the error 6475 times.

Dispatch on width instead of exact type so any integral type maps to a
conversion function that can represent it. The std::is_integral_v guard
keeps floating-point types on the existing error path. On LP64 this is
behaviour-preserving: the same conversions are selected as before.

Tests: no automated coverage for DBResult; verified by running the
server against MariaDB 12.3 and confirming the 6475 errors drop to 0
and player timestamps load correctly.

* feat: macOS build support

The project only targeted Linux and Windows. Building on macOS failed
before reaching the compiler, and one source construct was rejected by
libc++. None of these changes affect the Linux or Windows builds.

CMakeLists.txt: -march=x86-64 -mtune=generic -mno-avx -mno-sse4 was
applied to every non-MSVC compiler, which is fatal on Apple Silicon.
Guarded behind an x86 CMAKE_SYSTEM_PROCESSOR match, so x86 hosts keep
the commodity-CPU flags and arm64 skips them.

vcpkg.json: gmp was declared "platform": "linux" while BaseConfig.cmake
calls find_package(GMP REQUIRED) unconditionally, so configure failed on
macOS with no GMP installed. Widened to "!windows", which still leaves
Windows on mpir.

src/io/ioprey.hpp: the three unique_ptr null sentinels were declared
above the classes they reference. libstdc++ accepts this, but libc++
instantiates ~unique_ptr at that point and rejects the incomplete type.
Moved them below the class definitions; no behaviour change.

CMakePresets.json: added macos-release and macos-debug, mirroring the
Linux presets and conditioned on Darwin.

The SSE2/AVX2 intrinsics in astarnodes.cpp needed no change -- they are
already guarded and have scalar fallbacks that arm64 selects.

Building also requires autoconf and automake on the host, which vcpkg's
gmp port needs; that is an environment prerequisite, not a repo change.

Tests: no automated coverage for these paths. Verified by building
RelWithDebInfo on macOS 26.6 arm64 with Apple clang 21 and running the
result against MariaDB 12.3 -- server reaches "Crystal server online!",
loads the 35143x34812 map with 86924 monsters and 1042 NPCs, and answers
the status protocol.

* feat: multi world system (#451)

* feat: multi world system

SOME CONFIGS WILL BE REMOVED FROM config.lua AND WILL BE USED FROM DATABASE, TABLE worlds!

Implementation of the multiword system.

* Lua code format - (Stylua)

* Code format - (Clang-format)

* Create 61.lua

* fix: market issue

* Code format - (Clang-format)

* small fixes

* fix: some schema issues

* Fix account test: player key case mismatch

---------

Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>

* update: Document multiworld setup

Add a new multiworld guide covering server/site ownership boundaries, setup order, world seeding rules, and troubleshooting. Link the guide from the README so the docs are discoverable.

* ci: add macOS build workflow (#924)

* fix(tests): make the account reload test independent of hash map order

InMemoryAccountRepository::loadByID scans the account map and returns the
first entry whose id matches. "Account::reload reloads account info"
seeded two entries that both carry id 1, under different descriptors:

    accountRepository.addAccount("crystal@test.com",  { 1, ..., GOD });
    ...
    accountRepository.addAccount("crystal2@test.com", { 1, ..., GAMEMASTER });

Which of the two loadByID returns is then decided by iteration order, and
the backing store is a phmap::flat_hash_map, so that order is unspecified.
The assertion happens to hold on the Linux x86-64 CI runners and fails on
macOS arm64, where the second entry is not visited first:

    Running test "Account::reload reloads account info"... FAILED
    account_test.cpp:104 - test condition: [false]

Reuse the same descriptor for the second write so the map holds exactly one
entry for id 1 and reload has a single candidate to find. That is also the
scenario the test means to cover — the account's stored data changing
between load and reload, not a second account appearing.

Tests: unit suite on macOS 26.6.1 (arm64, Apple clang 21) goes from 1
failed to all passing — 104 tests, 269 asserts across the 7 suites.

* ci: add macOS build workflow

macOS builds are supported since #912, but nothing on CI exercises them,
so the platform can only regress silently — the toolchain differs from
both existing targets. It is libc++ rather than libstdc++, and Apple clang
rather than gcc or MSVC, which is what surfaced the incomplete-type and
type-dispatch problems fixed in #912 and #911.

Add "Build - macOS", modelled on the Ubuntu workflow: same triggers and
src/** path filter, same vcpkg-baseline-from-manifest step, ccache, and
lukka/run-cmake driving the macos-release and macos-debug presets that
already exist in CMakePresets.json. The matrix covers macos-14 and
macos-15 so two Xcode toolchains are exercised, both arm64-osx.

Two macOS-specific details:

- vcpkg builds gmp from source here and its build system needs autotools,
  which the runner images do not guarantee, so autoconf, automake and
  libtool are installed idempotently first.
- The companion dummy workflow runs on ubuntu-latest rather than macOS.
  Branch protection matches the check name, which comes from the job's
  matrix, so the names still line up and four macOS runners are not spent
  on an echo.

Unit tests run the same way as on Ubuntu, from the tests/unit build
directory, which the security suite depends on for its working directory.

Tests: actionlint reports no findings on either file. Locally on macOS
26.6.1 (arm64, Apple clang 21) the macos-release preset configures and
builds clean with BUILD_TESTS=ON, and ctest passes 104 tests / 269 asserts
across all 7 suites with the preceding commit applied.

* fix(tests): give InMemoryAccountRepository the id uniqueness the DB has

The previous commit stopped one test from depending on hash map iteration
order, but left the mock able to reach the state that made it possible.

addAccount keys on the descriptor, so writing the same id under two
descriptors leaves two entries carrying it. loadByID then returns whichever
the iteration visits first, and the backing store is a
phmap::flat_hash_map, so that choice is unspecified. It is stable for a
given build — it does not flake — but it differs across platforms, which is
why the reload test passed on Linux x86-64 and failed on macOS arm64.

AccountRepositoryDB cannot produce that state: `accounts`.`id` is the
primary key and loadByID reads a row back with WHERE `id` = ?, so at most
one account ever carries an id. Enforce the same invariant in the mock by
dropping any entry already holding the id being written. loadByID is then
left with a single candidate and its result no longer depends on iteration
order on any platform.

Add a test covering the case the old code got wrong — rewriting an id under
a different descriptor — so the invariant cannot be dropped silently.

Tests: the new test fails with the guard reverted (account_test.cpp:121)
and passes with it, confirming it pins the behaviour rather than passing
either way. Full unit suite on macOS 26.6.1 (arm64, Apple clang 21): 105
tests, 272 asserts, all passing.

* chore: ignore macOS .DS_Store files (#923)

Finder writes .DS_Store into any directory it browses, so working in the
repo on macOS leaves them showing up as untracked noise in git status.

Add a macOS section ignoring .DS_Store at any depth.

* fix(game): compare zone type instead of negating it when leaving a party (#922)

playerLeaveParty() tested `!player->getZoneType() == ZONE_PROTECTION`,
which applies the negation first and then compares the resulting bool
against the enumerator. Clang warns on this (-Wparentheses).

The check happens to produce the intended result today only because
ZONE_PROTECTION is the zeroth enumerator of ZoneType_t: `!zone` is true
exactly when zone is 0, and comparing that bool to 0 yields
`zone != ZONE_PROTECTION`. Reordering the enum or giving ZONE_PROTECTION
a non-zero value would silently invert the condition, letting players
leave a party while in a fight outside a protection zone.

Write the comparison directly. Behavior is unchanged, and the check no
longer depends on the enumerator's numeric value.

Tests: no behavioral change to test; verified the build is clean and the
-Wparentheses warning is gone.

* fix(database): persist db_version globally instead of per-world (#921)

The multiworld change scoped every server_config write to the current
world, including db_version. Migrations run in initializeDatabase(),
which CrystalServer::run() calls before worlds().load(), so at that point
getCurrentWorld() is still the default-constructed World with id 0. The
resulting UPDATE ... WHERE world_id = 0 AND config = 'db_version' matched
no rows on any database whose row has world_id = 1.

The version therefore never advanced. Because updateDatabase() logs
"Database has been updated to version N" unconditionally before the
write, this failed silently: an upgraded server reported success and then
re-ran every migration above the stale version on each subsequent boot.

The guard was also inverted. strcasecmp returns 0 on a match, so
`if (strcasecmp(config, "db_version"))` was false precisely for
db_version, sending the schema-global value down the world-scoped path
while genuinely per-world values took the global one.

Scope db_version writes globally and everything else per-world, mirroring
getDatabaseConfig() directly above. Test on `config == "db_version"` so
the read and write paths cannot disagree, and omit world_id from the
db_version INSERT so the column default applies rather than inserting 0,
which would violate the foreign key to worlds(id) added in migration 64.

Tests: verified against a database migrated from 63 to 64, where the bug
reproduced. With a temporary no-op migration 65 installed, db_version
advanced to 65 on world_id 1 and the following boot ran no migrations;
before the fix the same path left it at 63.

* fix: Rookgaard status healing npcs (#920)

Handles poison and fire properly
I think there's no energy fields in Rookgaard, well that's it. - Made it less redundant.

* fix: pugi::cast<float> does not compile with libc++ (#926)

* fix: pugi::cast<float> does not compile with libc++

libc++ ships only the integral overloads of std::from_chars and explicitly
deletes the rest, so every macOS CI job fails to compile pugicast.hpp as soon
as vocation.cpp instantiates pugi::cast<float>. Neither Xcode 15.4 nor 16.4
has the floating-point overloads; libstdc++ and MSVC do, which is why only
macOS sees it.

Floating point now goes through strtof/strtod/strtold behind a small
detail::fromChars helper, on every platform rather than only on macOS, so a
config value parses the same everywhere. strtod is the more permissive of the
two: it skips leading whitespace, accepts a leading '+' and parses hexadecimal
floats, so the helper turns those away up front and accept/reject behaviour
stays identical to what from_chars gave before. Locale is not a factor here,
since nothing in the server installs one and this stays the C locale's '.'.

Tests: pugicast_test.cpp covers the integer and floating point casts and pins
the from_chars parity, including the three shapes strtod would otherwise let
through. The utils suite goes from 46 asserts to 72, all passing.

* ci: drop macos-14 from the macOS build matrix

macos-14 defaults to Xcode 15.4, whose clang predates P0960, the
parenthesized initialisation of aggregates. Three call sites rely on it —
make_shared of WebhookTask, and emplace_back of SlotInfo and PromotionScroll —
so that runner cannot build them, while macos-15's Xcode 16.4 compiles all
three.

Nothing in the tree needs a compiler that old, so the matrix drops to macos-15
rather than adding constructors to work around it. The dummy workflow drops
the same entries so the check names stay in step.

* fix: load map items with decay (#915)

* feat: The Isle of Evil Quest (#919)

* isle-of-evil-quest

* Update king_tibianus.lua

* Update king_tibianus.lua

* feat: char bazaar auction setup from the game client (#918)

* Monstros na cidade de Targuna

* Monstros na cidade de Targuna II

* Feature

* Freshwater Turtle

* Change GITHUB_TOKEN to PAT_TOKEN in workflow

* Update lua-format.yml

* Fix

* Usage bug

* Update adrian.lua

* Chests in targuna

* Fix debug

* feat: char bazaar auction setup from the game client

The client already ships the whole "Character Auction Settings" dialog. This
module answers the data requests it sends when the dialog opens - the sell
conditions, the item picker, the sales arguments - and, on Confirm, creates the
auction.

Everything after an auction exists - bids, cancelling, paying, transferring the
character - stays on the website, which is where those tables live. The module
writes into them and degrades cleanly when they are absent: the dialog still
opens and Confirm refuses with a reason instead of failing silently.

The request opcode 0x76 is not parsed by ProtocolGame, so it reaches the module
system as-is and no engine change is needed. The reply opcode is 0x80,
GameserverMessageCharacterTradeConfiguration; the layouts of the three blocks it
carries are documented in the module where they were measured.

Adds `players`.`charbazaar`, the flag that claims a character for an auction so
the game server and the website cannot both sell it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Lua code format - (Stylua)

* up

* Update schema.sql

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>

* fix: Revert lua format (#928)

* update: doors table (#930)

Just added missing door, there's probably a lot more, I will check every door once I got the time.

* fix: Hirelings load (#933)

Fixes #929

* update: dire penguin raids (#931)

* update: dire penguin raids

Just regular format as the rest of the raids, insthead of all of them at once.

* fix typo

sorry, renaming typo

* feat: declare map ids and container contents in the tables (#932)

* feat(startup): declare map ids and container contents in the tables

Action ids, unique ids and what sits inside a container lived in two places at once: the
world.otbm and data-global/startup/tables/. Whatever the map carried was invisible to
anyone reading the Lua, which is why two entries could quietly fight over the same door
for years, and why "which numbers are in use, and where" could not be answered without
opening the map editor. This moves all of it into the tables and teaches the server to
report whatever is still left behind.

What moved into the tables

- Six new container_*.lua files declare what is inside chests, bookcases and bags:
  container_quest, container_books_1 through _4 and container_misc. The contents stay
  physical, so whoever takes an item keeps it and it comes back on restart, exactly as
  it did when the OTBM carried it. A container the map does not have is created; one
  that already holds something is left alone and reported.
- A reward chest declares no contents at all, only its unique id. Its prize is the
  `reward` in chest.lua: quest_reward_common.lua registers uid 5000-9000 and
  10000-12000, catches the click and returns true on every path, so the chest never
  opens as a container and a second, physical copy of the prize would reach nobody.
  The action id 2000 chest (quest_system1.lua) is the opposite and keeps its contents,
  because there the contents are the prize.
- Thirteen of those reward chests hand over a book, and the text used to exist only as
  container contents. It now sits beside the reward, in the AttributeTable of
  quest_reward_common.lua; seven of them used to arrive blank. Each entry names the
  itemId that carries the text, so a chest that gives a book together with a sword no
  longer stamps the text on the sword as well.
- The thematic tables gain the ids the map used to hold, each next to the entries it
  belongs with, sorted by key.
- teleport.lua, tile.lua, item.lua, lever.lua, chest.lua, corpse.lua, door_key.lua,
  door_level.lua, door_quest.lua, item_unmovable.lua and tile_pick.lua all grew for the
  same reason.

How it works from now on

The rule is short: never write an action id, a unique id or container contents into the
map editor. Declare them in data-global/startup/tables/ instead.

`tables/load.lua` reads every table file and the Map Attributes Loader applies them in a
fixed order: the containers are filled first, then the sign and book texts, then
`CreateMapItem`, then every thematic table, and `Game.reportShadowedScripts()` last, once
the ids are stamped. Order matters, and anything later overwrites anything earlier, so
two entries claiming the same item is a bug even when it appears to work -- the order of
`pairs()` is not defined by Lua.

Two loader changes make that practical:

- `loadLuaMapAction` accepts a list of blocks under one key, so an action id shared by
  several different items no longer needs a table of its own:

      [100] = {
          { itemId = 3003, itemPos = { { x = 31948, y = 31925, z = 8 } } }, -- rope
          { itemId = 7741, itemPos = { { x = 32011, y = 31709, z = 7 } } }, -- ice cube
      },

- `loadLuaMapAction` and `loadLuaMapUnique` now find an item inside a container on the
  tile, not only loose on it. Several keys live inside a chest, and `getItemById()`
  never reached those.

A number a script reads is data, not a key: it is never renumbered to fit a range. The
reserved ranges in the README describe intent, and hundreds of the numbers already in
use fall outside all of them, so a range is not proof a number is free -- grep is.

What the server now reports

- The OTBM parser counts every action id, unique id and filled container still stored in
  the map file, and the load names them with their positions, capped at ten examples so
  a map mid-migration cannot bury the boot log. Only the main map is reported: overlay,
  quest and custom maps load long after the tables were applied.
- The startup registry warns when two table entries claim the same item, which used to
  be silent and simply left the loser's value never happening.
- `Game.reportShadowedScripts()` reports scripts registered by position that can never
  run. `Actions::getAction` resolves unique id, then action id, then item id, and only
  then the position, so a position script is dead whenever an item on that tile carries
  an id another script also registered. The base already warns about two scripts
  claiming the same id, or the same position; this is the case that crosses the two and
  stayed quiet.

A clean boot prints none of these.

Fixes found on the way

- A dead ChestUnique entry pointing at z = 110, a floor that does not exist.
- Keys declared on a chest they no longer sit inside.

Five ids are deliberately **not** declared. The move had to renumber them, because the
number the map carries is already a key somewhere else or falls inside a range another
handler swallows -- so it did nothing in the map either. Declaring them under a new
number would stamp a value the map never had, which is the one thing this change must
not do. Every one of those items is `unmove` in appearances.dat and is not a container,
so the id was not holding it in place or gating its loot; leaving them out keeps the
item exactly as it behaves today.

Docs

data-global/startup/README.md is now a guide to the folder: the rule, how the loaders
run and in which order, which table to use for what, how to pick a number, and what a
clean boot looks like, and where a reward chest's prize belongs. It also lists the
eleven ranges a script registers in a loop -- the table had four -- and spells out why a
number that appears nowhere is still not free to reuse: a handler can read that an id
exists without ever naming it (`walkback.lua` tests `item.uid > 65535`, which means
"this item has no unique id"), a loop can claim a whole range, and the engine itself
reads the attribute -- `Item::canBeMoved` refuses any unique id and action id 100, and
either one keeps a corpse from being looted.

Nothing else in the datapack changes. Where an id, a duplicated declaration or a chest
already behaves a certain way here, it keeps behaving that way: this moves the storage
of the data, not the data.

The map file still holds its copy of the ids until the world.otbm is updated. Until
then nothing breaks: the server names each one on boot and the tables stamp the same
values over them.

* Lua code format - (Stylua)

---------

Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>

* fix: Use FAMILIARSNAME constant for familiar check

Replace the local 'summons' table with the global FAMILIARSNAME constant in Player:onLookInBattleList to centralize familiar name checks and avoid duplicate lists. Functionality unchanged; familiar display still shows master and disappearance time. Also includes an unrelated binary update to data-global/world/world.otbm.

* fix: resolve startup entry and lever conflicts reported by the map id loader (#937)

The map id loader added in #932 surfaced three pre-existing conflicts.

Kilmaresh catacomb doors: door_quest.lua declared the same six positions twice,
so Storage.Kilmaresh.CatacombDoors overwrote Storage.Kilmaresh.Sixth.Favor.
Nothing in the questline ever sets CatacombDoors (only freequests grants it),
which left those doors permanently sealed. Drop the duplicate block and narrow
the remaining one to itemId 9558, so the action id no longer lands on every
item sharing the tile.

Kilmaresh energy fields: the [40004] positions did not match the shipped map.
Point them at the four energy fields that actually exist there.

Secret Library levers: the four bosses were implemented twice, once by
actions_bossesLever.lua registered on action id 4950 and once by the BossLever
scripts registered by position. Action ids are checked before positions, so the
BossLever scripts never ran. Move the fight contents into the BossLever configs
and drop the old script.

Notes on that port:

- wild knowledge now declares mazzinorDeath. That event creates the vortex which
  is the only way to hurt Mazzinor, since mazzinorHealth heals back every hit,
  and BossLever does not register events on the monsters it spawns.
- Ghulosh resets its stage counter from boss.createFunction rather than
  onUseExtra: the counter is global, and onUseExtra also runs for lever pulls
  that fail their conditions, which would reset a fight already in progress.
- the library entry tiles now read the BossLever cooldown instead of the timer
  storages the removed script used to set.
- summons spawn once per lever pull instead of in 25 second waves.

* fix: pits of inferno bosses (#939)

* fix: pits of inferno bosses

Just added loot properly.

* Lua code format - (Stylua)

---------

Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>

* feat(look): name the script that answers for an item (#938)

A script can claim an item through its unique id, its action id, its item id or
the position the item sits on, and only the first of those to match ever runs.
A position registration leaves no trace on the item, so from inside the game
there was no way to tell that a tile had a script behind it, let alone which
file to open when one misbehaved.

Item:getScriptBindings() now reports every action and move event hooked to an
item: the file each one lives in and how it got hooked. The look description
prints them under the position line and marks a registration as shadowed when
an earlier one answers first, which is the conflict reportShadowedPositionScripts()
warns about at startup, now visible per tile instead of only in a boot log.

Only Lua scripts show up. Doors gated by a storage are handled in C++, so they
report nothing. The paths are trimmed against dataPackDirectory and
coreDirectory rather than an assumed folder name.

* fix: Initialize player cooldown timers on login (#935)

This updates the player login script to initialize stamina, XP stamina, and concoction cooldown timers using the current Unix time instead of a hardcoded 1. This ensures cooldown tracking starts from the actual login moment and prevents immediate reuse or incorrect timing behavior.

* feat(core): add database player concurrency lock to prevent web/game race conditions (#908)

* feat(core): add database player concurrency lock to prevent web/game race conditions

* fix(core): improve lock expiration with db timestamp and fix config sorting

* feat(schema): add migration 64 for player concurrency lock columns and index

* fix(core): use int64_t for lock timestamps and bound future timestamp window

* style(migrations): format migration 64 with StyLua

* chore(schema): update db_version to 64 in schema.sql

* ci: trigger workflow re-run

* Create 66.lua

---------

Co-authored-by: João Paulo <jprzimba@gmail.com>

* fix: Stop reseeding RNG in DropLoot (#934)

Remove math.randomseed(os.time()) from Blessings.DropLoot in data/libs/systems/blessing.lua. Reseeding the RNG on every drop call can produce predictable/poor randomness and disturb the global RNG state; relying on a single process-wide seed gives more consistent, higher-quality random behavior.

---------

Co-authored-by: Paco <frjafopa@gmail.com>
Co-authored-by: Alan <126849283+aacruzgon@users.noreply.github.com>
Co-authored-by: João Paulo <jprzimba@gmail.com>
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Guilherme <guilherme.vrsantana@gmail.com>
Co-authored-by: HT Cesta <58153179+htc16@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: HirelingsInit() never loads hirelings from database (missing db.storeQuery call)

1 participant