feat: Cyclopedia Map - Discovery System ("Measuring Tibia") + Area Donations / Improved Respawn Rate (client 15.25)
Full server-side implementation of the Cyclopedia Map exploration features introduced by CipSoft: the Discovery System (per-subarea viewpoint exploration with permanent unlocks and rewards) and Area Donations unlocking a temporary Improved Respawn Rate per main area. All wire formats were reverse-engineered from the 15.25 client binary; all geometry comes 1:1 from the client's own map assets.
Ships as two patches with disjoint file sets (apply in any order, both required for the full feature):
| Patch |
Scope |
0001-feat-discovery-system-cyclopedia-map-15.25.patch |
Map protocol layer (S2C 0xDD, C2S 0x92/0xDB), discovery logic, generated geometry, god tools |
0001-feat-improved-respawn-area-donations-15.25.patch |
Respawn zone registry + spawn engine, server-save boost cycle, spawn timing fixes, shutdown save fix |
Part 1 - Discovery System
Mechanics
- Automatic exploration (matches the current live behavior, no "Start Discovering" button): entering a registered subarea activates a session and rolls 7 hidden viewpoints inside the subarea's real footprint (walkable tiles only, never inside the player's current view, minimum spacing).
- Walking within view range (+-7/+-5, same floor) of a viewpoint discovers it (effect + message + map checkmark). 7/7 completes the subarea permanently (player KV).
- Viewpoints persist across sessions; completed subareas greet with "fully explored".
- Rewards: first fully explored main area grants the Discoverer outfit + achievement "Widely Travelled"; 50% of main areas grants addon 1 + title "Dedicated Entrepreneur"; 100% grants addon 2 + achievement "Measuring the World" + title "Globetrotter". Titles run through
PlayerTitle::checkMap reading discovery/map-percent KV.
Geometry - generated from the client's own assets
The client renders discovery regions from per-subarea bitmap masks, so the server uses the exact same shapes:
map.dat (protobuf tibia.protobuf.map.Map) provides all areas/subareas (ids, names, kinds, label pins) and per-subarea mask records (subarea-XXXX-<sha>.bmp.lzma, CIP LZMA container, 32bpp BMP, alpha = painted, 1 px = 1 tile).
- Generator output
data/scripts/discovery_geometry.lua:
- 207 surface subareas, pixel-exact (1.89M tiles, 41,680 rects, verified mutually disjoint), across 22+ main areas,
- 69 underground subareas approximated from NPC-landmark clusters (median center, outlier rejection, padding, floors from landmark z +-1),
- rects are packed as strings and parsed to flat arrays at load (Lua has a 65,536 constants-per-chunk limit),
- remaining deep dungeons have no client-side data and are intentionally not discoverable (consistent with the original client, which cannot render discovery without a mask). They do not count toward any completion percentage.
- Runtime lookup: bbox prefilter + floor set + flat rect scan; viewpoint rolls use a cumulative-area binary search. Detection floors are separate from viewpoint-roll floors (rooftops count as inside, viewpoints only roll on reachable levels).
Protocol (RE'd from the 15.25 client)
S2C 0xDD frame: [0xDD][u8 subtype][body]:
| Sub |
Name |
Body |
| 1 |
DiscoveryData |
u16 nMain {u16 areaId, u8 status, u8 progress} + u16 nDiscovered {u16 subId} + u16 nDiscoverable {u16 subId} |
| 5 |
SetDiscoveryArea |
u16 subId, bool active, u8 poiTarget, u8 count, {pos(5B), u8 state} x count |
| 9 |
Donations |
u64 goal, u8 count, {u16 areaId, bool improvedRespawnActive, u64 donated} x count |
| 10 |
SetCurrentArea |
u16 subId (0 = none) |
| 11 |
SetExploringArea |
u16 subId (0 = none); the client keeps currentPlayerSubAreaID and currentExploringSubAreaID separately and the viewpoint overlay needs both |
C2S 0x92 (protobuf varints: request / viewport / legacy start) is answered with the cached DiscoveryData.
God tool
/disco - refresh map data; status (per-main summary), where (position -> matching footprint, flags approximate boxes), done [subId] (restore/mark complete), reset, tp, poi (dump rolled viewpoints to the server log), plus the donation commands listed below.
Part 2 - Area Donations / Improved Respawn Rate
Mechanics
- Players donate gold to a main area through the map UI (or
/disco donate <areaId> <gold>); money is taken from inventory + bank, the resource balance is re-sent so the gold shown in the dialog updates instantly.
- Pool reaches the goal (default 10,000,000 gold, tunable) -> boost becomes SCHEDULED (broadcast message).
- At the daily server save the scheduled boost activates: goal amount is consumed (excess carries over), the boost runs for 24h (until the next save) and then expires by timestamp. A 60s watcher re-applies zones on any active-set change, so expiry also works across long uptimes.
- While active, every monster spawn inside the area's subarea footprints (all floors) respawns twice as fast, and the area label on the map shows the improved-respawn indicator (the
bool in sub 9).
State lives in global KV (discovery-donations scope), so it survives restarts. Test helpers: /disco donations (pools + SCHEDULED / ACTIVE until states), /disco ss (simulate the server-save boundary), /disco resetdonations.
C2S 0xDB - the 15.25 map channel (raw, not protobuf)
[u8 action][raw LE fields]:
| Action |
Length |
Body |
Handling |
| 1 |
9 |
u16 areaId, u32 goldAmount, u8 pad |
Donate |
| 2 |
5 |
u32 areaId (currently viewed) |
Must stay unanswered - replying makes the client re-request in a loop until the packet-rate limiter disconnects it |
Spawn engine correctness fixes (required for exact halving, improve vanilla too)
- Stamp
lastSpawn at the actual death/removal moment. Upstream only stamped it when the periodic check's cleanup() first noticed the corpse, so the respawn wait silently started up to a full check tick late; with AoE double-kills the second corpse regularly lost a whole interval (measured: 90s spawn appearing after 120s+).
- Aim the check timer at the earliest pending respawn (
getNextCheckDelay) instead of the fixed gcd cadence, so halved (and vanilla) waits land exactly and are never quantized up.
- Guard the 4.2s non-blockable staging window against duplicate staged spawn chains (effect spam) now that checks can land inside it.
- Verified with server-side timeline logging: boosted 90s spawns consistently return in 45.0s + 4.2s staging, measured from the exact death stamp, including simultaneous AoE kills.
Shutdown save fix (SIGBREAK race)
Upstream queued the shutdown handler on the dispatcher and immediately tore the thread pool down, so setGameState(SHUTDOWN) (player kicks + saveAll) never actually ran on Ctrl+Break stops - shutdown logs had no "Saving server..." line and global KV (donation pools) silently vanished on restart. The signal thread now waits (up to 90s) for the handler to finish before the pool goes down.
Tunables
| Constant |
Default |
Where |
DONATION_GOAL |
10,000,000 gold |
data/scripts/discovery_system.lua (published to KV for the C++ handler) |
BOOST_DURATION |
24h |
discovery_system.lua |
| respawn factor |
2x (interval / 2) |
SpawnMonster::checkSpawnMonster / getNextCheckDelay |
POI_TARGET |
7 viewpoints |
discovery_system.lua |
GEOMETRY_VERSION |
bump to wipe stale rolled viewpoints on login (completed subareas are kept) |
discovery_system.lua |
feat: Cyclopedia Map - Discovery System ("Measuring Tibia") + Area Donations / Improved Respawn Rate (client 15.25)
Full server-side implementation of the Cyclopedia Map exploration features introduced by CipSoft: the Discovery System (per-subarea viewpoint exploration with permanent unlocks and rewards) and Area Donations unlocking a temporary Improved Respawn Rate per main area. All wire formats were reverse-engineered from the 15.25 client binary; all geometry comes 1:1 from the client's own map assets.
Ships as two patches with disjoint file sets (apply in any order, both required for the full feature):
0001-feat-discovery-system-cyclopedia-map-15.25.patch0001-feat-improved-respawn-area-donations-15.25.patchPart 1 - Discovery System
Mechanics
PlayerTitle::checkMapreadingdiscovery/map-percentKV.Geometry - generated from the client's own assets
The client renders discovery regions from per-subarea bitmap masks, so the server uses the exact same shapes:
map.dat(protobuftibia.protobuf.map.Map) provides all areas/subareas (ids, names, kinds, label pins) and per-subarea mask records (subarea-XXXX-<sha>.bmp.lzma, CIP LZMA container, 32bpp BMP, alpha = painted, 1 px = 1 tile).data/scripts/discovery_geometry.lua:Protocol (RE'd from the 15.25 client)
S2C
0xDDframe:[0xDD][u8 subtype][body]:u16 nMain {u16 areaId, u8 status, u8 progress}+u16 nDiscovered {u16 subId}+u16 nDiscoverable {u16 subId}u16 subId, bool active, u8 poiTarget, u8 count, {pos(5B), u8 state} x countu64 goal, u8 count, {u16 areaId, bool improvedRespawnActive, u64 donated} x countu16 subId(0 = none)u16 subId(0 = none); the client keepscurrentPlayerSubAreaIDandcurrentExploringSubAreaIDseparately and the viewpoint overlay needs bothC2S
0x92(protobuf varints: request / viewport / legacy start) is answered with the cached DiscoveryData.God tool
/disco- refresh map data;status(per-main summary),where(position -> matching footprint, flags approximate boxes),done [subId](restore/mark complete),reset,tp,poi(dump rolled viewpoints to the server log), plus the donation commands listed below.Part 2 - Area Donations / Improved Respawn Rate
Mechanics
/disco donate <areaId> <gold>); money is taken from inventory + bank, the resource balance is re-sent so the gold shown in the dialog updates instantly.boolin sub 9).State lives in global KV (
discovery-donationsscope), so it survives restarts. Test helpers:/disco donations(pools + SCHEDULED / ACTIVE until states),/disco ss(simulate the server-save boundary),/disco resetdonations.C2S 0xDB - the 15.25 map channel (raw, not protobuf)
[u8 action][raw LE fields]:u16 areaId, u32 goldAmount, u8 padu32 areaId(currently viewed)Spawn engine correctness fixes (required for exact halving, improve vanilla too)
lastSpawnat the actual death/removal moment. Upstream only stamped it when the periodic check'scleanup()first noticed the corpse, so the respawn wait silently started up to a full check tick late; with AoE double-kills the second corpse regularly lost a whole interval (measured: 90s spawn appearing after 120s+).getNextCheckDelay) instead of the fixed gcd cadence, so halved (and vanilla) waits land exactly and are never quantized up.Shutdown save fix (SIGBREAK race)
Upstream queued the shutdown handler on the dispatcher and immediately tore the thread pool down, so
setGameState(SHUTDOWN)(player kicks +saveAll) never actually ran on Ctrl+Break stops - shutdown logs had no "Saving server..." line and global KV (donation pools) silently vanished on restart. The signal thread now waits (up to 90s) for the handler to finish before the pool goes down.Tunables
DONATION_GOALdata/scripts/discovery_system.lua(published to KV for the C++ handler)BOOST_DURATIONdiscovery_system.luaSpawnMonster::checkSpawnMonster/getNextCheckDelayPOI_TARGETdiscovery_system.luaGEOMETRY_VERSIONdiscovery_system.lua