Persist temporary summon expiry and define durable follower categories - #147
Conversation
|
Warning Review limit reachedNext included review available in 8 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (11)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
moshehbenavraham
left a comment
There was a problem hiding this comment.
Verdict: issues found
Adversarial review of head b8481b80e17f3c12009f0bbf1b5c599f77321930 (all 10 files plus save_char_pets / load_char_pets / prepare_saved_pet_row / pet_retrieve_stored / ePURGEMOB / charm-duration / copyover login). CodeRabbit's rate-limit stub is not this review. #120 / pet_save_objs.pet_idnum is out of this diff.
The version-4 T <kind> <epoch> parser, spent-decoy serialization (T 1 <now> rather than durable), and active roster login path are largely what #119 asked for: the scheduler handle is never persisted, malformed blobs still fail parse and retain the row, and an expired active pet is extracted roomless without setting restore_failed so a later save_char_pets snapshot can drop it. Copyover really does save_player_pets() then enter_player_game() -> load_char_pets().
The hole is the shared decoder's expiry path vs stored rows.
Blocker
- Expired stored deadline pets are never dropped (
src/players.c:7248, callerpet_retrieve_stored~7818 is not in the diff).- Claimed: restore rejects an expired record, discards the roomless copy, and the next snapshot drops the row.
- Actual: true only for
PET_STATE_ACTIVEafter a successful load plus a later snapshot.pet_retrieve_storedtreatsprepare_saved_pet_rowreturning NULL as "The keeper cannot rouse that follower." and ROLLBACKs, sopet_statestaysPET_STATE_STORED.save_char_petsonly replaces active rows. - Why it matters:
PET_KEEPER_CAPACITYis 10.stable storeaccepts any loyal follower, including an illusory decoy.pet_store_surviving_followers()auto-stores every charmed NPC on death, including decoys. After the 120sSPELL_MISLEADePURGEMOBelapses, reclaim fails forever and the slot stays occupied. Help says the pet is gone when you return; the stable list still shows it.
Non-blockers
-
restore_pet_lifetimedoes not verify the freshePURGEMOBattached (src/players.c:6363).attach_mud_eventis void and frees the event on admission failure; the function stillreturn trueand the pet is published. That is session-length immortality on the reconstruct path (a later decoy save would write spentT 1 <now>). Claimed: live records get a fresh native event. -
New tests never drive the production restore/snapshot/keeper path (
unittests/CuTest/test_pet_lifetime.c). They exerciseserialize_pet_runtime_state/restore_pet_runtime_state_for_testonly (deadline round-trip, repeated serialize, malformedTlines, v3 stray line, spent decoy, PETS strings). They do not callsave_char_pets,load_char_pets,prepare_saved_pet_row, orpet_retrieve_stored. Issue #119's disconnect/reboot/copyover coverage is call-graph commentary. Finding 1 is exactly the untested path. -
Save eligibility is still "every charmed NPC" (
pet_lifetime_kind/save_char_pets). #119 asked to define durable categories instead of treating every charmed NPC as implicitly persistent. Magical summons still set the charm bit withoutePURGEMOBor a charm affect withduration >= 0(onlySPELL_MISLEADgets the 120s event), so they classify asPET_LIFETIME_DURABLEand still reload forever. Labels changed; what gets snapshotted did not.
Checked and not wrong
- Durable serialize is
T 0 0(init_pet_runtime_statememset), not stack garbage. - Parser
%n+pet_state_line_is_completerejects trailing junk; v4 requiresT; v3 strayTfails;T 0 5/T 1 0/T 2 5fail validation. - Absolute epoch + pulse ceiling: repeated saves stay within ~1s; not an immortality leak.
- NPC
ePURGEMOBis not in the player event copyover table; pets rebuild from DB only (no double event). stop_follower's 12s purge is after the pet leaves the follower list, so it is not snapshotted as a deadline companion.- Help
.hlpandhelp_pet_entries.sqlmatch aside from SQLpet''sescaping.
| return NULL; | ||
| } | ||
| /* The next snapshot drops the expired row; nothing else about the roster failed. */ | ||
| if (!restore_pet_lifetime(mob, has_runtime_state ? &runtime_state : NULL, time(NULL))) |
There was a problem hiding this comment.
[blocker] Expired restore is a non-failure extract of a roomless copy. That only lets the next active snapshot drop the row.
pet_retrieve_stored (not in this diff, ~7818) shares this decoder. On NULL it sets "The keeper cannot rouse that follower." and ROLLBACK, so PET_STATE_STORED is unchanged. save_char_pets only DELETEs/replaces PET_STATE_ACTIVE rows.
Claimed: reject expired, discard roomless copy, next snapshot drops the row.
Actual: stored deadline pets (manual stable store, or pet_store_surviving_followers on death, both of which accept decoys) stay in the stable forever after the epoch, occupying one of 10 slots. Help says the pet is gone when you return.
Fix on this path: if lifetime restore fails because the deadline elapsed (or a decoy had no deadline), delete or mark-unretrievable the stored row and its pet_save_objs instead of rolling back to the expired snapshot. Do not set restore_failed for that case on login (active drop-on-snapshot is correct).
| return false; | ||
| if (remaining > LONG_MAX / PASSES_PER_SEC) | ||
| remaining = LONG_MAX / PASSES_PER_SEC; | ||
| attach_mud_event(new_mud_event(ePURGEMOB, pet, NULL), (long)remaining * PASSES_PER_SEC); |
There was a problem hiding this comment.
[non-blocker] attach_mud_event is void; on admission failure it frees the event and returns. This function still return true and the caller publishes the pet.
Claimed: a live record gets a fresh native ePURGEMOB for the remaining time.
Actual: the call is issued; success is not observed. A deadline follower restored without an event is the original #119 bug for that session (a decoy would be saved spent on the next snapshot, so it is not forever).
After attach, require mud_event_is_live(char_has_mud_event(pet, ePURGEMOB)) and return false (discard) if it is not live.
| CuAssertTrue(tc, remaining >= 87L * PASSES_PER_SEC && remaining <= 92L * PASSES_PER_SEC); | ||
| } | ||
|
|
||
| void Test_pet_lifetime_rejects_expired_and_malformed_records(CuTest *tc) |
There was a problem hiding this comment.
[non-blocker] This suite drives serialize_pet_runtime_state / restore_pet_runtime_state_for_test only. Expired is "helper returned false and no event," not "prepare_saved_pet_row extracted a roomless read_mobile copy and the DB row was dropped."
Missing vs the PR/#119 claims:
save_char_pets/load_char_pets(disconnect, reboot, copyover all go through those)- keeper retrieve of an expired
T 1 <past>stored row (pet_retrieve_storedrollback vs active snapshot drop) restore_failedleft false on expiry so login can still markPET_ROSTER_LOADED
The serializer cases here (malformed T lines, v3 stray T, spent decoy T 1 <now>) are real. They would not have caught the stored-row leak.
| runtime-state record (version 4, `T <kind> <epoch>`) stores the absolute | ||
| real-time deadline rather than the scheduler handle. The deadline keeps | ||
| elapsing offline: restore rejects an expired record, discards the roomless | ||
| copy, and lets the next snapshot drop the row; a live record gets a fresh |
There was a problem hiding this comment.
[non-blocker / docs] "lets the next snapshot drop the row" is only true for PET_STATE_ACTIVE after load_char_pets succeeds. Stored rows have no such snapshot: save_char_pets replaces active rows only, and keeper retrieve rolls back on expiry.
Same overclaim is in the PR body and the prepare_saved_pet_row comment at src/players.c:7247. If stored deadline pets are meant to expire, document that reclaim deletes the stored row; if they are meant to pause, pet_lifetime_kind/serialize must not keep a wall-clock epoch while PET_STATE_STORED.
b8481b8 to
cf1440c
Compare
|
Addressed all four review findings in the follow-up commit (branch squashed to one commit on top of current master; no rebase was needed). 1. Expired stored deadline pets were never dropped (blocker). 2. 3. Tests never drove the production restore/snapshot/keeper path. Added 4. Save eligibility was still every charmed NPC. Added Also fixed an Local CI replication (GitHub jobs are not treated as gates):
|
Fixes #119. Follower persistence is now an explicit policy in pet_lifetime_kind(): durable followers persist until dismissed, killed, or stored; timed control keeps its saved charm duration and pauses offline; deadline followers (a live ePURGEMOB event, and illusory decoys, which never persist without one) save an absolute real-time deadline that keeps elapsing offline, across reboots and copyovers; ordinary spell summons outside the kept families are session-bound and never saved. Runtime-state record version 4 adds a validated "T <kind> <epoch>" line; the scheduler handle is never persisted. Restore rejects expired, spent-decoy, and session records, and a restored deadline fails closed if its event cannot be scheduled. The keeper boards only durable and timed-control followers, and reclaiming a stored row whose lifetime has ended deletes the row inside the reclaim transaction instead of holding the slot forever. "pets" shows each pet's policy and remaining real time. Tests cover the record format and, against MariaDB, the production save_char_pets, load_char_pets, pet_store_pet, and pet_retrieve_stored paths. Help (help.hlp and the SQL component) and the save-systems document are updated. Claude-Session: https://claude.ai/code/session_01Adu1bF5vNR7Drp3a9oKR97
cf1440c to
4add1ca
Compare
|
Merged to master as 303ba07 (squash of one commit, rebased onto #146 first with no conflicts). What merged
Rebased verification: Follow-up
|
Fixes #119
Problem
save_char_pets()/load_char_pets()preserved substantial runtime state but not the remainingePURGEMOBlifetime. A temporary follower such as an illusory decoy could reload without its expiration event and live forever. Every charmed NPC was also implicitly persistent, so ordinary spell summons reloaded across logouts.Policy
Follower persistence is now explicit (
pet_lifetime_kind()insrc/utils.c):ePURGEMOBevent, plus illusory decoys, which never persist without one): an absolute real-time deadline that keeps elapsing offline, across reboots and copyovers.The pet keeper boards only durable and timed-control followers. Reclaiming a stored row whose lifetime has ended (for example a decoy stabled before this policy existed) deletes the row and its objects inside the reclaim transaction and tells the player the keeper released it, so the stable slot is not held forever.
Changes
T <kind> <epoch>line bound to the existing pet row identity. The scheduler handle is never persisted. The existing versioned parser and the atomic snapshot contract are unchanged.ePURGEMOBfor the remaining time, and the restore fails closed if that event cannot be scheduled.save_char_pets()skips session followers;pet_store_pet()and the keeper'sstable storerefuse session and deadline followers with a player-facing message.petsshows each pet's persistence policy and remaining real time, with a footer explaining offline behavior.lib/text/help/help.hlpandsql/components/help_pet_entries.sql(PETS and STABLE).docs/systems/SAVE_SYSTEMS_BREAKDOWN.mdupdated.Tests
unittests/CuTest/test_pet_lifetime.ccovers the record format: deadline round trip with a fresh event, repeated saves agreeing on the absolute deadline, expired restoration rejected with no event, malformed records, missing or stray lifetime lines by version, legacy v3 records restoring as durable, decoys saved as spent without a live event, session summons classified and rejected, keeper acceptance per kind, and status output for every policy.Test_pet_lifetime_survives_snapshot_restore_and_keeper_releaseinunittests/CuTest/test_database_persistence.cdrives the production path against MariaDB (LUMINARI_TEST_MYSQL_ENABLE=1) with real prototypes and a room:save_char_pets()skips the session summon and stores the absolute deadline;load_char_pets()republishes the deadline follower with a fresh event and the control follower with its affect; an expired active row is refused on reload while the roster still counts as fully restored and the nextsave_char_pets()drops it;pet_store_pet()refuses deadline and session followers;pet_retrieve_stored()deletes a spent legacy stored decoy and reports the release, and still reclaims a live durable stored follower into the room. Disconnect, reboot, and copyover all reach persistence through these same functions.https://claude.ai/code/session_01Adu1bF5vNR7Drp3a9oKR97