Builder Receipt: Resurrecting Legend of the Green Dragon

I went back to an old PHP browser game because I wanted to know whether something from an earlier era of the web could be preserved without pretending the old implementation was still safe enough to run as-is. The question became simple to ask and hard to answer: can I keep Legend of the Green Dragon recognizable as itself while replacing the assumptions that no longer belong on a modern public server?

What Legend of the Green Dragon Is

Legend of the Green Dragon, usually shortened to LoGD, is a browser-based role-playing game that grew out of the early web era. The original game describes itself as being based on Seth Able’s Legend of the Red Dragon. The LoGD code was written in PHP with a MySQL backend, with the original core credited to Eric Stevens and JT Traub, and later work credited to the Dragonprime Development Team.

I am working from the preserved 1.1.2 Dragonprime Edition. I am not building a modern game loosely inspired by it, replacing the original with a different framework, redesigning the gameplay, or stripping away the strange little behaviors that made the game what it was.

The basic loop is simple enough to understand even if you never played one of these games. You create a character. You get a limited number of forest fights during a game day. You fight monsters, gain experience, earn gold and sometimes gems, buy better weapons and armor, train, gain levels, and eventually become strong enough to challenge the Green Dragon. Death is not the end of the character. The game has a graveyard and resurrection loop. Reaching and defeating the Dragon is a major progression milestone, and the larger game is built around repeated cycles of growth.

Around that core loop is everything that made old community browser games feel larger than the code that powered them: races, specialties, temporary buffs, mounts, clans, player-versus-player systems, mail, commentary, random events, side encounters, games of chance, shops, trainers, administration tools, and a module system that let server operators change the character of their world. That module system is one of the reasons LoGD mattered. A server could begin with the same core and become its own place. It is also one of the reasons modernizing it is difficult.

This is not a small single-purpose script where I can replace a database library and declare victory. It is a stateful multiplayer application whose rules are spread across old PHP pages, global state, serialized data, GET and POST behavior, database writes, modules, hooks, session state, combat state, and years of assumptions about how the web worked.

A page loading in PHP 8 does not prove the game works. A monster dying does not prove combat is trustworthy. A module appearing in a menu does not prove the module is safe. That distinction became the center of the project.

Why I Came Back to It

I preserved Legend of the Green Dragon 1.1.2 Dragonprime Edition in 2019. At the time, preservation itself mattered to me. Old web software disappears easily. Projects vanish, download links die, forums disappear, dependencies stop installing, and eventually something that once had an active community becomes a ZIP file somebody hopes still exists on an old drive. I did not want that to happen here.

But preserving source code and running preserved source code are two different things. The historical code has value because it is the historical code. That does not mean I should take a PHP application written for a very different security model, point a modern web server at it, and call the result a resurrection.

So I separated the goals. The historical repositories stay historical. The modernization happens somewhere else. That decision became the foundation of Legend of the Green Dragon: Resurrection.

The project has two responsibilities at the same time. One is preservation. The other is modernization. Neither one is allowed to destroy the other. The phrase I keep coming back to is:

Preserve the game. Modernize the implementation.

That sounds simple. It has not been simple.

Preserving the Historical Baseline First

Before I started serious modernization, I wanted a source baseline I could point to and trust. The preserved 1.1.2 Dragonprime core is represented by the historical tag historical-source-1.1.2. The preserved baseline contains 417 tracked historical files across 11 preservation commits. The source commit behind that tag is bdc29df9bc344774b41e0ad7cae7f6ed2f7512e2. Those details matter because I do not want modernization to quietly rewrite history.

The historical lotgd and lotgd-modules repositories are preservation artifacts. I have treated them as immutable during this work. The Resurrection repository is where the new work belongs. That gave me a clean rule whenever I reached an uncomfortable decision.

If something needs to change for a modern runtime, security boundary, database model, or testable application contract, change it in Resurrection. Do not retroactively pretend the original was something it was not. The historical source is evidence. It is not a disposable starting point.

I also kept the licensing and provenance constraints in the project instead of treating them like cleanup work that could be dealt with later. Resurrection is not an excuse to erase the people who built the original game.

The First Problem Was Not PHP Compatibility

The obvious way to describe this project would be “modernizing an old PHP game.” That is true, but it misses the hard part.

Old PHP compatibility problems are visible. Removed functions fail. Old database APIs break. Type assumptions become errors. Installation routines stop working. A modern runtime gives you a stack trace and tells you something is wrong. The more dangerous problems are the ones that still run.

A request parameter that can influence authoritative game state may keep working perfectly. So can a GET request that mutates a character, a serialized blob that accepts a shape the game never intended, a stale form, or a replayed action. An old module may install, activate, appear in the right place, and still cross an authority boundary that nobody tested twenty years ago.

That changed the goal from compatibility to trust. I do want LoGD to run on current PHP and modern databases. But I want it to run because the important state transitions have been examined, constrained, tested, and proven, not because I managed to suppress enough warnings to reach the village page.

That same distinction between something that appears to work and something I can actually trust also drove Builder Receipt: Making Cypher More Reliable.

A Separate Branch and a Deliberately Unfinished Pull Request

The public Resurrection foundation lives on main. The modernization work lives on a separate branch, modernization/core-modernization, and all of the modern-core work is being carried through PR #1, “Modernize LoGD core runtime and security foundation.”

That pull request is intentionally still open and intentionally still a draft. More important than the commit count or hashes is the status:

OPEN.

DRAFT.

NOT READY.

NOT MERGED.

There has been no production deployment, no public runtime, no release package, no release tag, no VPS deployment, no PR #2, and no Phase 4 work. That is not project paralysis. It is a rule. The branch does not get merged because I am tired of looking at it. The branch gets merged when the evidence supports merging it.

Building the Modern Runtime Foundation

The early work had to make the application runnable enough to test honestly. That included moving database access toward PDO, normalizing modern character handling with utf8mb4, establishing InnoDB where the supported modern database targets require transactional behavior, tightening installation behavior, modernizing password handling, rotating sessions appropriately, repairing logout behavior, and putting CSRF protection around state-changing requests that needed it.

The project also needed a way to deal with old serialized state without spreading raw unserialize() calls throughout the codebase. That led to a centralized scalar-state approach and then to stricter business-level validators as individual areas became better understood. The important idea was not “serialization is bad.” The important idea was that serialized data crossing an authority boundary must be treated as input, not truth.

The installer also became part of the trust boundary. A fresh modern installation had to create the expected schema consistently on both supported database targets. By the Phase 2 checkpoint, the project was proving fresh 38-table installs across PHP 8.4 with MariaDB 11.4 and PHP 8.5 with MySQL 8.4. That same checkpoint was already proving authentication, session behavior, logout, and bundled installer behavior.

The test floor at that point was still small by where the project ended up going: 39 PHPUnit tests with 299 assertions, 12 Python/HTTP tests, and 277 PHP files passing lint. That was progress. It was nowhere near enough.

Modernizing Dynamic Behavior Without Deleting It

Some of the old code was not merely incompatible. It relied on techniques that are difficult to justify in a modern public application. The answer could not always be “delete the feature.”

One example was expression evaluation. Legacy game code sometimes needs to evaluate small formulas or rule expressions. The dangerous shortcut is to let those strings become arbitrary PHP execution. I did not want Resurrection carrying a general code-execution mechanism just because the historical implementation had used one. The modernization work replaced those cases with a finite expression evaluator constrained to the vocabulary the game actually needs.

That distinction is important. The gameplay gets to keep a formula. The formula does not get to become PHP. Five legacy evaluation sites were removed from the old execution model and routed through constrained behavior instead.

The Gypsy Bandit behavior was another example. Old dynamic enemy logic had to be preserved as game behavior without leaving an open-ended execution path behind it. That work became a specific, testable AI behavior instead of a reason to keep unsafe dynamic execution around.

Administrative state needed the same treatment. The settings editor moved toward typed handling rather than accepting arbitrary values as equally trustworthy. Mount preference state received its own object-aware handling instead of being treated as a shapeless serialized blob.

One-use intents became a reusable pattern for actions where a valid request should only be able to mutate state once. The daily-hook scheduler became explicit enough to reason about and test instead of remaining just another old request-time side effect. Commentary moderation, module activation, and module dependencies were pulled into the same modernization mindset: preserve the supported function, make the authority explicit, and test the failure behavior instead of relying on old request assumptions.

Some historical surfaces were not worth pretending to support during this phase. LoGDnet, console behavior, payment paths, and the source viewer were disabled rather than left exposed in a half-modernized state. That choice shows another rule I have used throughout Resurrection. A feature can be historically real and still be unavailable in the current modernization branch. Preservation does not require exposure. I would rather return a deliberate disabled response than leave a legacy surface active without a security model I can defend.

The Moment Compatibility Turned Into Certification

Phase 3 forced a more uncomfortable question. The Resurrection repository ships bundled modules. What does it mean to say one of those modules is supported?

At first, all 24 bundled modules could pass lifecycle and metadata checks. They could be discovered. Their installer behavior could be examined. The module system itself was becoming trustworthy. But I did not want “the module system can load this file” to turn into “this module is certified.”

So the first honest Phase 3 certification result was brutal:

0 fully passing.

0 passing with a documented limitation.

24 blocked.

All 24 lifecycle checks passed, but all 24 modules remained blocked from full certification. That distinction is one of the most important decisions in the project.

A module can have a valid lifecycle and still not have a trustworthy business boundary. A module that changes gold needs its gold behavior tested. A module that changes combat needs its combat behavior tested. A race that changes onboarding needs the onboarding authority tested. A specialty that consumes skill uses and mutates combat state needs to prove those things through the real application path. A module is not certified because its PHP file parses.

I ran into the same problem from another direction while testing Bonumark Stream across hosting environments: working in one place was not enough to call the product portable. Builder Receipt: Bonumark Hosting Portability documents that work.

What PASS Had to Mean

As the certification work developed, PASS became much stricter. A useful PASS had to prove the supported path, not just one happy-path function. Depending on the module, that meant testing things such as:

  • Authentication
  • Active-module requirements
  • Real HTTP entry
  • POST versus GET behavior
  • CSRF rejection
  • One-use intent handling
  • Server-owned initial state
  • Persistence
  • Stale request rejection
  • Replay rejection
  • Duplicate submission rejection
  • Reselection rejection
  • Rollback behavior
  • Missing module or missing handler behavior
  • Wrong-specialty behavior
  • Invalid level or use counts
  • Terminal-state behavior
  • The actual game-state mutation the module claims to perform

That sounds excessive until an old game crosses from nostalgia into a public server. Then it becomes the minimum useful question: What exactly is the server agreeing to believe?

From 0 of 24 to 21 of 24

The certification count started moving only after the tests started reaching the actual business behavior. The early pass promoted ten bundled modules: Cedrik’s Potions, Dag Durnick, Dark Horse, Drinks, Find Gem, Find Gold, Foil Wench, and the bundled Dice, Five Six, and Stones games.

The next certification work brought in Crazy Audrey, Fairy, Glowing Stream, Outhouse, and Seth’s Song. That moved the project to 15 PASS and 9 BLOCKED. Goldmine followed after its real behavior was proven. That moved the count to 16 PASS and 8 BLOCKED.

Then came Lovers and the four bundled races: Dwarf, Elf, Human, and Troll. Those were not promoted merely because a race name could be stored on the account. Their onboarding paths had to prove that the server, not a forged request, controlled the initial state. Persistence had to work. Replay and rollback had to work. Unsupported or stale transitions had to fail correctly.

Once Lovers and all four races passed, the project reached:

21 PASS.

0 PASS WITH DOCUMENTED LIMITATION.

3 BLOCKED.

The only remaining blocked bundled modules were Dark Arts, Mystical Powers, and Thieving Skills. All 24 lifecycle checks were still passing. The three specialties were blocked because lifecycle was no longer the problem. Combat authority was.

I would rather have three modules marked BLOCKED with a precise list of what remains than promote them because most of the interesting cases now pass.

The Three Specialties Became the Real Test

The specialty modules are where the project stopped feeling like a module-certification exercise and became a general combat-state investigation. Each specialty gives the player abilities that can consume uses, modify attack or defense, damage enemies, heal the player, or create temporary effects.

Dark Arts can create a Skeleton companion. Mystical Powers can create regeneration, aura effects, Lifetap behavior, and damage reflection. Thieving Skills can manipulate attack and defense values in ways that directly change the order and result of a combat round. If I wanted to certify those modules honestly, I could not test them outside combat and then assume the combat engine would do the right thing. I had to prove the whole transaction.

The first specialty onboarding work closed an important boundary before any ability was used. A newly selected specialty had to begin with server-owned skill and use state. The request could not simply tell the server how many uses the player deserved. The onboarding request had to be authenticated, active, POST-based, CSRF-protected, intent-bound, replay-resistant, and persistent.

That work passed for all three specialties, but the certification count did not change. They stayed blocked, intentionally. Selecting a specialty safely is not the same thing as using it safely.

Dark Arts

Dark Arts became one of the clearest examples of why this work needed business-state testing. The supported combat actions include effects such as a Skeleton companion, Voodoo damage, attack or defense manipulation, and effects with limited duration. It was not enough to prove that an ability reduced a use counter. The tests had to prove the exact state produced by that use.

The Skeleton companion eventually received its own validator, SkeletonCompanionState. At a representative level 10 state, the companion was being validated with exact expected business values: 43 hit points, 26.5 attack, and 14.5 defense. That matters because a serialized companion is not harmless text. It is combat authority stored for later use. If stored state could inject arbitrary attack, defense, or health, then a future combat request could become the point where corrupted state turns into an advantage.

The tests also had to handle what happens when the player loses. One of the newer adverse tests proved that a lethal player riposte can happen before the Skeleton gets a chance to act. In that case, the Skeleton remains at 43 of 43 instead of taking some invented post-defeat turn. After the player enters shades, the companion state is retained but suspended, preventing a second settlement path from treating it as active combat state.

Dark Arts duration behavior also exposed an important rule. A buff does not necessarily consume a round just because a combat request happened. If the relevant combat phase was skipped because the player died earlier in the ordering, a defense-only effect can retain its duration.

That is an odd old-game behavior. It is also historical behavior. The job of Resurrection is not to “fix” every strange outcome into something I personally find cleaner. The job is to determine whether the behavior is intentional enough to preserve, then make the state transition trustworthy.

Mystical Powers

Mystical Powers pushed the accounting problem in different directions. Regeneration has to heal the player without violating maximum hit points. Aura has to affect the correct companion state and stop at the correct cap. Earth Fist has to apply its area behavior to supported targets. Lifetap has to heal only from legitimate damage and must not turn a failed or negative damage calculation into free healing. Damage Shield has to reflect the correct amount of incoming damage and settle simultaneous outcomes correctly.

These are not abstract unit-test concerns. They change whether a character lives, dies, earns a reward, loses gold and experience, or enters a terminal combat state.

One of the strongest current cases is the damage shield. The accepted tests prove the ordinary weapon damage, incoming damage, reflected damage, victory and defeat boundaries, zero or negative enemy-health cases, and the historical simultaneous-lethal behavior in the Forest. That simultaneous-lethal behavior is especially useful because it is not what I would have invented if I were designing a new game from scratch.

When the shield reflection kills the final enemy at the same time the player is killed, the existing Forest path can invoke the game’s mushroom recovery and leave the player at 1 HP. The test preserves that behavior instead of flattening it into a new “clean” result.

That is the balance I want from this project: a modern trust boundary around historical game behavior.

Thieving Skills

Thieving Skills showed how quickly a small-looking modifier can become an ordering problem. A skill that reduces enemy attack changes incoming damage. A skill that multiplies player attack changes weapon damage. A skill that zeros enemy attack does not automatically erase every other defensive event in the round. A skill that increases both player attack and defense affects different phases.

The current tests prove the exact effect accounting for the supported levels, including lethal cases.

For example, the level 1 attack reduction produces 70 incoming damage instead of 177 in the deterministic test setup. The level 2 attack increase produces 88 weapon damage while leaving the ordinary 177 incoming damage intact. The level 3 zero-enemy-attack effect still does not suppress a separate defensive riposte that occurs earlier in the established ordering. The level 5 combined attack and defense effect produces 135 weapon damage and 104 incoming damage in the accepted deterministic case.

Those numbers are not there to make the receipt sound technical. They are evidence that the test is checking the business result, not merely whether a function returned true.

Testing Failure, Not Just Success

A large part of this project has been learning that success-path tests are the easy half. I need to know what happens when a request is wrong, stale, or replayed; when the module is gone or the handler is missing; and when the user tries to spend an ability they do not own.

I also need to know what happens when a serialized state object has the right-looking outer shape and a malicious inner value, when the player dies halfway through a round, when the enemy dies first, when both die, or when another target is waiting. Those failures still have to leave the database in a state I can explain.

That is why replay protection and rollback became recurring themes. An action that fails should not consume a use and then pretend it never happened. A duplicate action should not apply its effect twice. A stale form should not mutate a newer combat state. A failed transaction should not partially write the player’s account and leave the fight somewhere between two worlds. Those are the kinds of bugs that can hide for years in a browser game because the ordinary path looks fine.

The Transmutation Regression

One of the most useful moments in the recent work was a failure that had nothing to do with the new specialty behavior being tested. A full suite run reached all 46 HTTP tests per target. The new specialty tests passed. The suite still failed.

An existing Transmutation case expected HTTP 400 and received HTTP 409. That mattered. It meant the new shared validation behavior had changed the response boundary for an older potion path.

It would have been easy to look at the specialty work, see green specialty tests, and call the execution successful. I did not. The failed run remains preserved as a failed run. It is not acceptance evidence.

The correction restored the existing potion validator’s 400 response before the new shared collection validation could change it. The specialty validator itself did not need to be weakened, and the older regression test did not need to be rewritten to make the pipeline green. Then the full supported matrix was run again.

That is exactly the kind of failure I want a project like this to expose. The point of adding validation is not to break older contracts and redefine success after the fact. The point is to make the boundary stronger without quietly changing unrelated behavior.

Validating Persisted Buff State

The newer specialty work also added a business validator called SpecialtyBuffState. This exists because temporary combat effects are state, and state can become an attack surface if the application blindly trusts what it previously serialized.

The validator checks the specialty-specific shapes the current work understands. That includes identifiers, expected text, allowed modifiers, duration rules, runtime flags, and finite numeric values. The HTTP tests deliberately corrupt persisted buff state and then try to enter gameplay with it. The rejected cases include wrong roots, malformed entries, missing fields, injected modifiers, injected text, unexpected sources, invalid durations, invalid flags, object injection, malformed serialization, excessive nesting, and non-finite values such as infinity or NaN.

The important behavior is not simply “bad data throws an error.” The important behavior is that corrupted specialty state is rejected before it becomes combat authority.

The broader old-game buff universe is not fully certified yet. I am not claiming that it is. SpecialtyBuffState closes the boundary I can currently describe and prove. That is the pattern throughout Resurrection: validate the business state I understand, do not pretend a narrow validator proves the entire legacy application.

Combat Ordering Became a Specification

Old combat code often contains rules that were never written as a formal specification. The code is the specification. That is dangerous when modernizing it because refactoring can accidentally turn an implementation detail into a gameplay change. The specialty work forced me to write down the ordering in tests.

At the current supported Forest boundary, the tests establish a sequence that includes round-start effects, the player’s attack and possible riposte behavior, the enemy response only if both sides remain alive, shield behavior after damage, and phase-based duration consumption. Lifetap is clamped so it cannot turn negative damage into healing. Temporary effects do not necessarily decrement unless their relevant phase actually activated. Victory and defeat settlement happen in the transaction that owns the specialty action.

That has an important side effect beyond the specialties. The tests are gradually turning unwritten old-game behavior into an executable contract. That is exactly what I need before touching the more general combat engine.

The Test Floor Kept Growing Because the Questions Kept Getting Better

The raw test counts are not the project, but the progression tells the story. The early public modernization foundation was proving 16 PHPUnit tests with 73 assertions, nine Python tests, and lint across 264 PHP files. By the Phase 2 checkpoint, the floor had reached 39 PHPUnit tests with 299 assertions, 12 Python tests, and 277 linted PHP files. When Phase 3 began treating bundled modules as untrusted until proven, the floor reached 47 PHPUnit tests with 1,393 assertions, 13 Python tests, and 282 linted files. When the certification count reached 15 PASS, the project was at 74 PHPUnit tests with 2,227 assertions and 29 Python/HTTP tests.

By the time Lovers and all four races had been certified, the bundled count was 21 PASS and the HTTP suite had grown again. Specialty onboarding, specialty combat authority, adverse outcomes, duration semantics, defeat settlement, and persisted buff validation kept expanding the evidence floor. The accepted matrix for this checkpoint is captured below.

What matters is that each harder question forced the test suite to prove more of the application instead of merely accumulating tests around the same happy path.

The Dragon Boundary Is Still Open

The largest specialty blocker is no longer ordinary Forest combat. It is the Dragon path. The current legacy Dragon flow still contains authority problems I have not closed: Dragon entry writes a single-target encounter on GET, fight and run behavior invokes battle through old routing, and victory issues a prologue1 GET link.

The prologue reads flawless from the request, strips buffs, resets account state and rewards, clears companions, and runs dragon-kill hooks. That is exactly the kind of boundary I do not want to certify through assumption.

A Dragon kill is a major character-state transition. It needs explicit POST and CSRF authority where state changes, state-bound one-use intent behavior, stale-form and replay rejection, server-owned outcome authority, and a transition I can test from the start of the encounter through the prologue and resulting account state.

Until that is done, all three specialties remain blocked from independent promotion.

Defeated-Target Progression Is Still Open

The next unresolved combat boundary is multi-target progression. The current specialty combat state can prove a live target binding, single-target terminal behavior, and some area behavior, including the currently supported Earth Fist path. What it does not yet prove is the complete transition from a defeated target A to a still-live target B.

That sounds narrow. It is not. Once a target is dead but the encounter is not over, several pieces of state have to agree about what happens next:

  • Which target is authoritative
  • Whether a temporary effect continues
  • Whether its duration is consumed
  • Whether a companion acts
  • Whether area effects see the next target
  • Whether reward settlement waits until the final target
  • Whether stale forms against the dead target are rejected
  • Whether the final-target transition settles exactly once

The current SpecialtyCombatState is deliberately narrow enough that it still rejects some defeated-enemy envelopes instead of pretending that transition is solved. That is why the project has not reached 24 of 24 bundled certification.

The General Combat Schema Is Still Open

There is another temptation I have tried to avoid. The specialty tests are now strong enough that it would be easy to mistake them for proof of the entire combat engine. They are not.

The specialty transaction has proven a great deal of Forest behavior, but ordinary Forest authority still contains old GET-driven behavior that needs closure. General reward authority, defeat authority across every caller, multi-target terminal behavior, and broader enemy and buff schemas are not fully certified. The Dragon path is not certified. Training and master combat remain tied to broader combat closure, and PvP has only limited evidence compared with what a full public-hosting decision would require.

The right conclusion is not “combat works.” The right conclusion is that I now have enough proven combat behavior to see the remaining boundary clearly. That is a much more useful place to be.

Serialization Is Now an Inventory, Not a Mystery

Part of the broader Phase 3 work has been cataloging where serialized state actually exists. The current inventory identifies 89 serialization-related sites across 44 files, including 32 writer or check locations and 56 ScalarState reads, with one centralized raw unserialize path.

That inventory matters because legacy state work can otherwise turn into whack-a-mole. Fix one serialized object. Miss three others. Harden one endpoint. Leave another caller trusting the same state differently.

Resurrection is gradually replacing that ambiguity with named business-state boundaries. ScalarState handles the constrained scalar envelope. SkeletonCompanionState handles the known companion business state. SpecialtyCombatState handles the current specialty combat envelope. SpecialtyBuffState handles the supported specialty buff shapes.

Those validators are not a claim that every serialized state structure in LoGD is solved. They are a way to stop raw historical state from remaining anonymous.

What I Have Refused to Call Done

The easiest way to make this project look further along would be to lower the standard. I could count lifecycle as certification, treat a green happy path as enough, remove a regression test when new validation changed an old response, treat PHP lint as runtime proof, treat HTTP 200 as application health, or call three specialties “basically done.”

I could merge the modernization branch because the test numbers are already substantial. I could deploy it privately and use that as evidence that public hosting is close enough. I have deliberately not done those things. The project is still Phase 3. PR #1 is still a draft. The modern-core merge verdict is still NO. Public hosting is still NO.

That matters more to me than making a progress post sound impressive. There is already enough real progress here. I do not need to invent completion.

The Broader Phase 3 Work Still Waiting

The specialty and combat work is currently the deepest active boundary, but Phase 3 is larger than combat. The current project report still identifies unclosed authority in mail send, reply, and system mail behavior; petition administration; clans; bank and economy behavior; weapons and armor; mounts and stables; training and masters; broader PvP; administrator and content-editor paths; expiration cleanup around last_char_expire; account deletion failure semantics; and New Day dragon-point authority around old dk and pdk boundaries.

There are still meaningful GET mutations in ordinary Forest behavior, the Dragon and prologue path, petitions, clans, equipment, stables, training, and legacy editors. That is why Phase 4 has not started. The project does not get to skip the boring old routes just because the specialty combat work is more interesting.

The Checkpoint This Receipt Covers

At the checkpoint covered by this receipt, the project state is straightforward:

  • Repository: jimlunsford/legend-of-the-green-dragon-resurrection
  • Foundation main: 999cec6f9c655a840320d982d673bb863c68c2b2
  • Modernization branch: modernization/core-modernization
  • Current branch head: ebcd70988478553a67d645dcffa6f05573e0338e
  • Pull request: PR #1, “Modernize LoGD core runtime and security foundation”
  • PR state: OPEN, DRAFT, NOT READY, NOT MERGED
  • Bundled certification: 21 PASS, 0 PASS WITH DOCUMENTED LIMITATION, 3 BLOCKED
  • Blocked specialties: Dark Arts, Mystical Powers, Thieving Skills
  • Lifecycle: 24 of 24 PASS
  • Supported CI: PHP 8.4.25 + MariaDB 11.4.13 and PHP 8.5.10 + MySQL 8.4.11
  • Accepted test floor per target: 83 PHPUnit tests, 2,398 assertions, 46 Python/HTTP tests, 317 PHP files linted, 0 failures, 0 skips
  • Static analysis: legacy PHPStan level 0 with seven retained findings and zero new errors; infrastructure PHPStan level 6 with zero errors
  • Composer: strict validation PASS, locked installation PASS, audit PASS, no security advisories
  • Historical integrity: 417 historical files, 11 preservation commits, historical tag intact
  • Deployment: none
  • Public runtime: none
  • Release: none
  • Phase 3: incomplete
  • Modern-core merge: no
  • Public hosting: no

The two accepted final GitHub Actions runs for the current head are modern-core workflow 34963561865 and baseline-integrity workflow 34963561770.

What This Work Has Taught Me

The first lesson is that preserving software is not the same as freezing implementation. If I preserved every insecure assumption because it was old, I would not be preserving the game. I would be preserving the conditions under which the game once happened to run. The game is the rules, the progression, the strange interactions, the character state, the modules, the combat outcomes, and the little behaviors players experienced. The implementation is how those things were expressed in the technology of the time. Those are related, but they are not identical.

The second lesson is that old code needs behavioral tests before it needs aggressive cleanup. There are places in LoGD where the historical behavior is weird. A defense-only buff can retain duration if the player dies before the defensive phase activates. A damage shield can create a simultaneous-lethal result that flows into an existing Forest recovery path. A Skeleton can remain intact when its owner dies before it acts. If I “cleaned up” those paths before I understood them, I could easily produce a more elegant program and a less faithful game. The tests give me a way to separate ugly implementation from intentional behavior.

The third lesson is that authority matters more than syntax. Modern PHP compatibility is necessary. It is not the finish line. The real work is deciding what state belongs to the server, what the client is allowed to request, what can happen once, what can be replayed, what persists, what rolls back, and what evidence proves the answer.

The fourth lesson is that failure is useful when the test suite is honest. The Transmutation regression was not wasted time. It was evidence that the new validation path reached farther than intended.

A stale-form rejection is not an annoyance. It is proof that an old browser request cannot silently mutate a newer game state. A blocked module is not a project failure. It is a statement that I know exactly where my evidence stops. That is much more valuable than a green badge attached to an undefined claim.

Why This Is Worth the Trouble

This project is not the most commercially sensible thing I could be working on. That is fine. Legend of the Green Dragon: Resurrection is a passion project. I preserved the source because I cared about it. I came back because preservation without a path to safe execution felt incomplete. And once I started pulling on the thread, I wanted to know whether I could actually do the work properly.

That means respecting the history without worshipping obsolete implementation. It means using modern tooling without turning the game into a modern rewrite that merely borrows the name. It means keeping the historical repositories untouched while the Resurrection repository changes aggressively where it needs to. It means accepting that some parts of the project will stay blocked longer than I would like. It means being willing to run one more test when the obvious behavior already looks correct. It means not publishing a public server simply because I can.

There is also something I like about the shape of the challenge. The code is old enough that many of its assumptions are visible. The game is complicated enough that those assumptions matter. The module architecture creates enough variation that a shallow modernization falls apart quickly. And the gameplay has enough personality that preserving its behavior is worth the trouble.

That makes it a good test of the kind of builder I want to be. The work is not just getting code to run. It is defining what should be trusted, testing the boundary, reading the failure, changing the implementation, and refusing to widen the claim beyond the evidence.

What Comes Next

The next work is not Phase 4, a redesign, public hosting, a second modernization branch, or a second pull request. It stays exactly where the evidence says it belongs.

First, the Dragon path needs modern transaction authority. Then the defeated-target and next-target transition needs to be proven. Then the remaining companion, area-effect, duration, reward, and final-settlement behavior needs to be closed. Only after each specialty has its full supported boundary proven should Dark Arts, Mystical Powers, and Thieving Skills move from BLOCKED to PASS.

If that reaches 24 of 24 bundled certification, the project still does not automatically become merge-ready. The remaining general combat, economy, social, administration, expiration, deletion, and legacy mutation boundaries still have to be handled honestly.

That may sound slow. Compared with the original goal of “make an old PHP game run on modern PHP,” it is slow. But that is no longer the goal. The goal is to bring Legend of the Green Dragon forward without losing what made it Legend of the Green Dragon.

I have the historical source. I have a modern branch. I have a growing executable specification for behavior that used to exist only in old code. I have 21 certified bundled modules and three that are blocked for specific, known reasons. I have a test floor that now catches regressions outside the feature being worked on. I have a clean line between what is proven and what is not. That is the receipt so far.

Resurrection is not finished. It is becoming trustworthy.


New Here?

Read Next:

Reference Links:


Get the Work
Articles on discipline, recovery, identity, and ownership. Delivered when published.