Skip to content

Commit 3a65d4a

Browse files
committed
docs: document behavior and hardware testing
1 parent 7342287 commit 3a65d4a

4 files changed

Lines changed: 323 additions & 13 deletions

File tree

‎README.md‎

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,76 @@
22

33
Adds fixed growths to Fire Emblem: Three Houses.
44

5+
[![build](https://github.com/jinghaihan/feth-fixed-growths/actions/workflows/build.yml/badge.svg)](https://github.com/jinghaihan/feth-fixed-growths/actions/workflows/build.yml)
6+
57
> [!IMPORTANT]
6-
> This project is in early development. It does not yet install gameplay hooks
7-
> or modify save-backed game data.
8+
> This plugin has not yet been verified on real Nintendo Switch hardware. Back
9+
> up your saves before testing it. It stores fixed-growth state in unused,
10+
> save-backed unit fields; earned stat changes remain after uninstalling it.
811
9-
## Target
12+
## Requirements
1013

1114
- Fire Emblem: Three Houses 1.2.0
12-
- Nintendo Switch
13-
- Skyline plugin runtime
15+
- Atmosphere with Skyline
16+
17+
Aldebaran is not required. Do not run this together with My 3H Plugin's growth
18+
features: both hook the same level-up function and use the same unit storage
19+
range.
20+
21+
## Install
22+
23+
1. Download `feth-fixed-growths.nro` from a successful
24+
[build workflow](https://github.com/jinghaihan/feth-fixed-growths/actions/workflows/build.yml),
25+
or from a release after hardware testing is complete.
26+
2. Copy it to:
27+
28+
```text
29+
sdmc:/atmosphere/contents/010055D009F78000/romfs/skyline/plugins/feth-fixed-growths.nro
30+
```
31+
32+
3. Fully restart the game and load Fire Emblem: Three Houses 1.2.0.
33+
34+
The plugin checks both the title ID and display version before installing its
35+
hook. Unsupported versions are left unchanged.
36+
37+
## Behavior
38+
39+
Each stat starts with growth points equal to that character's personal growth.
40+
On every level, the plugin adds personal growth, class growth, and applicable
41+
growth-skill bonuses. Every 100 points grants one stat point and leaves the
42+
remainder for later levels. Movement does not receive the growth-skill bonus.
43+
44+
Counters and the most recent level-up result are stored in
45+
`Unit.class_level[60..81]`, the same 21-byte range used by the reference
46+
plugin. No extra version marker is written. Invalid or stale state is ignored,
47+
and the owned range is cleared when a unit is initialized at level 1. A
48+
level-1 unit also always recalculates level 2 from a fresh seed, even if stale
49+
bytes survive a save transition.
50+
51+
Existing high-level units begin tracking on their next level; past random
52+
levels are not recalculated. Removing the plugin returns future levels to the
53+
vanilla random system, but it does not undo stats already earned.
54+
55+
## Documentation
56+
57+
- [Development guide](docs/development.md) — local builds, checks, artifacts,
58+
and release preparation
59+
- [Architecture](docs/architecture.md) — hook flow, algorithm, version profile,
60+
and persistence layout
61+
- [Hardware test plan](docs/hardware-testing.md) — the required Switch checks
62+
before the first release
1463

1564
## Development
1665

17-
Install Rust and
18-
[`cargo-skyline`](https://github.com/jam1garner/cargo-skyline), then run:
66+
Install Rust and `cargo-skyline`, then run:
1967

2068
```sh
2169
cargo fmt --check
70+
cargo test
2271
cargo skyline check
2372
cargo skyline build
2473
```
2574

26-
The generated NRO is intended for the following plugin directory:
27-
28-
```text
29-
sdmc:/atmosphere/contents/010055D009F78000/romfs/skyline/plugins/
30-
```
31-
3275
## Prior art
3376

3477
This is an independent implementation informed by publicly documented and
@@ -39,3 +82,7 @@ reverse-engineered behavior from these projects:
3982
- [`triabolicals/fe-growths`](https://github.com/triabolicals/fe-growths)
4083
- [Aldebaran](https://github.com/three-houses-research-team/aldebaran-rs)
4184
- [FETH Overlays](https://github.com/3096/feth-overlays)
85+
86+
Fire Emblem and related names are trademarks of Nintendo and Intelligent
87+
Systems. This unofficial fan project is not affiliated with or endorsed by
88+
them.

‎docs/architecture.md‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Architecture
2+
3+
## Scope
4+
5+
The plugin replaces only Fire Emblem: Three Houses' unit level-up stat-roll
6+
path. It does not provide selectable growth modes, perfect growths, zero growths,
7+
random recruitment, overlays, or ROM file replacement.
8+
9+
The implementation has four boundaries:
10+
11+
- `growth.rs` contains deterministic, game-independent accumulation math.
12+
- `level_up.rs` combines FE3H growth sources and decides whether to calculate,
13+
restore, or defer to vanilla behavior.
14+
- `persistence.rs` owns the save-backed byte layout.
15+
- `game/` and `plugin.rs` contain the FE3H 1.2.0 layouts, offsets, raw memory
16+
access, and Skyline hook.
17+
18+
## Runtime flow
19+
20+
The Skyline entry point verifies the title ID and the application display
21+
version before installing the hook at FE3H 1.2.0 text offset `0x003D3020`.
22+
Every hook call validates its unit pointer, target level, character ID, class
23+
ID, data-table entries, and persistent save unit before using fixed growths.
24+
If any required input is unavailable, the hook calls the original game
25+
function.
26+
27+
For a valid level-up call, the hook:
28+
29+
1. Reads the unit's current level and ten stored stats.
30+
2. Reads personal growths and personal stat caps from `PersonData`.
31+
3. Reads player class growths from `ClassData`.
32+
4. Calls the game's unit ability-parameter function for growth bonus `0x3A`.
33+
5. Loads validated fixed-growth state from the persistent save unit.
34+
6. Restores a cached result for a duplicate target level, or calculates every
35+
missing level through the requested target.
36+
7. Stores normalized counters and the result cache before applying the level
37+
and stats to the unit passed by the game.
38+
39+
Level 1 follows the original game function and then clears the 21 bytes owned
40+
by this plugin for that unit. Level 2 is always calculated from a fresh seed
41+
when the current unit is level 1, so a matching stale cache cannot be restored.
42+
43+
## Stat order and growth formula
44+
45+
FE3H stores the ten fields in this order:
46+
47+
```text
48+
HP, Str, Mag, Dex, Spd, Lck, Def, Res, Mov, Cha
49+
```
50+
51+
The initial counter for each field is:
52+
53+
```text
54+
personal_growth mod 100
55+
```
56+
57+
For every gained level:
58+
59+
```text
60+
effective_growth = max(personal_growth + class_growth + ability_bonus, 0)
61+
points = points + effective_growth
62+
stat_gain = floor(points / 100)
63+
points = points mod 100
64+
```
65+
66+
The ability bonus is omitted for Movement. Stat gains are bounded by the raw
67+
character cap stored in `PersonData`. If a stat is already capped, its counter
68+
is frozen so that it can resume if a later cap change permits another point.
69+
Saint Statue maximum-stat bonuses still need hardware verification; do not
70+
claim exact cap parity for those bonuses before that test passes.
71+
72+
This matches the observable fixed-growth model used by Fire Emblem Engage:
73+
personal growth seeds the hidden counter, then total current growth is added
74+
at each level. It intentionally differs from My 3H Plugin's fixed branch,
75+
which reverse engineering shows seeds a new state by applying one extra total
76+
growth step.
77+
78+
## Persistence
79+
80+
The plugin uses the same unused `Unit.class_level` range as My 3H Plugin:
81+
82+
| Slot | Bytes | Meaning |
83+
| --- | ---: | --- |
84+
| `60` | 1 | Most recent target level |
85+
| `61..70` | 10 | Growth-point remainders, one per stat |
86+
| `71..80` | 10 | Cached HP and nine non-HP stats |
87+
88+
The owned half-open range is `class_level[60..81]`. No separate version byte
89+
or signature is written.
90+
91+
A stored state is accepted only when:
92+
93+
- its target level is between 2 and 99;
94+
- every counter is between 0 and 99;
95+
- cached HP is nonzero;
96+
- cached stats do not exceed the character's caps; and
97+
- it represents the current level or the target being repeated.
98+
99+
Otherwise the state is treated as uninitialized. This provides a natural
100+
fallback for new runs and NG+ without adding another save marker. Save deletion
101+
deletes the state with the save because no external sidecar file exists.
102+
103+
The cached result makes an immediate Divine Pulse replay or another duplicate
104+
call for the same target deterministic. Multi-level rewind behavior still
105+
requires real-hardware testing.
106+
107+
## Confirmed profile facts
108+
109+
The FE3H 1.2.0 profile uses Build ID
110+
`89048449BA238C8CF565518B83BF02D3` and title ID `010055D009F78000`.
111+
The following facts were cross-checked against public FE3H structures and the
112+
reference plugin binary:
113+
114+
- level-up function offset `0x003D3020`;
115+
- ability-parameter function offset `0x000A7E80`;
116+
- persistent-unit lookup offset `0x003CAF30`;
117+
- person and class table offsets and entry layout;
118+
- `Unit` level, stat, and `class_level` offsets;
119+
- storage slots `60..80` and duplicate-result cache behavior; and
120+
- the omission of the ability growth bonus for Movement.
121+
122+
The NRO is compile-tested for the Skyline Switch target. None of these facts
123+
should be described as hardware-verified until the hardware plan passes.

‎docs/development.md‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Development
2+
3+
## Toolchain
4+
5+
Install stable Rust and
6+
[`cargo-skyline`](https://github.com/jam1garner/cargo-skyline). Let
7+
`cargo-skyline` install and link its custom `skyline-v3` toolchain when first
8+
prompted.
9+
10+
## Checks
11+
12+
Run the complete local verification set after changing Rust code:
13+
14+
```sh
15+
cargo fmt --check
16+
cargo test
17+
cargo skyline check
18+
cargo skyline build --release
19+
python3 tools/verify_nro.py \
20+
target/aarch64-skyline-switch/release/libfeth_fixed_growths.nro
21+
```
22+
23+
The release NRO is generated at:
24+
25+
```text
26+
target/aarch64-skyline-switch/release/libfeth_fixed_growths.nro
27+
```
28+
29+
CI renames the downloadable copy to `feth-fixed-growths.nro`.
30+
31+
## Profiles and unsafe code
32+
33+
All FE3H offsets, counts, and version identifiers belong in
34+
`src/game/profile.rs`. Memory layouts belong in `src/game/layout.rs`, and raw
35+
runtime access belongs in `src/game/runtime.rs` or the small hook adapter.
36+
37+
Never add an offset for another update to the 1.2.0 profile. Create a separate
38+
complete profile and validate the title version before installing any hook.
39+
40+
## Releases
41+
42+
`VERSION` is the release tag source, and CI verifies that it matches the Cargo
43+
package version. Tags must use `v<VERSION>`.
44+
45+
The normal release command is:
46+
47+
```sh
48+
uv run tools/release.py
49+
```
50+
51+
The script runs local checks, updates version files, creates a Conventional
52+
Commit and annotated tag, and explicitly pushes `main` and that one tag. A
53+
`v*` tag triggers `.github/workflows/release.yml`, which builds and verifies
54+
the NRO, creates an installable SD-card ZIP, generates checksums and release
55+
notes, and uploads every file to the GitHub Release page.
56+
57+
Do not run the release command or push a release tag until the CI artifact has
58+
passed the real-hardware test plan.

‎docs/hardware-testing.md‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Hardware test plan
2+
3+
No release should be tagged until this plan passes on Fire Emblem: Three
4+
Houses 1.2.0 running on a real Nintendo Switch.
5+
6+
## Preparation
7+
8+
1. Back up every save slot outside the console.
9+
2. Remove My 3H Plugin and any other plugin that hooks unit level-up.
10+
3. Install the CI artifact as:
11+
12+
```text
13+
sdmc:/atmosphere/contents/010055D009F78000/romfs/skyline/plugins/feth-fixed-growths.nro
14+
```
15+
16+
4. Fully restart the console or game process.
17+
5. Keep an unmodified copy of the pre-test save for byte and stat comparison.
18+
19+
## Required checks
20+
21+
### Startup and version gate
22+
23+
- Confirm FE3H 1.2.0 reaches the title screen and loads a save normally.
24+
- Confirm the game starts with only this plugin installed.
25+
- If another FE3H update is available for testing, confirm the plugin logs an
26+
unsupported-version message and leaves vanilla level-ups unchanged.
27+
28+
### First fixed level
29+
30+
- Record a level-1 unit's personal growths, class growths, current stats, and
31+
any growth ability bonus.
32+
- Seed each expected counter with personal growth.
33+
- Add the current total growth once and record every field reaching 100.
34+
- Gain exactly one level and compare all ten stored fields. Movement must not
35+
receive the ability growth bonus; Charm must receive it.
36+
37+
### Carry and class changes
38+
39+
- Gain several levels and verify fractional points carry between levels.
40+
- Test a 100% total growth and an over-100% total growth if a controlled setup
41+
is available.
42+
- Change class, gain another level, and verify existing counters continue with
43+
the new class growths.
44+
- Confirm a capped stat does not increase and its counter resumes correctly if
45+
a legal cap increase becomes available.
46+
- Specifically test a Saint Statue maximum-stat bonus near the raw personal
47+
cap; the first release is blocked until the effective in-game cap behavior
48+
is confirmed or implemented.
49+
50+
### Save lifecycle
51+
52+
- Save, close the game completely, reload, and verify the next level continues
53+
the same counters.
54+
- Copy the save to another slot and verify the copied slot keeps its own state.
55+
- Delete a disposable save slot and confirm no external plugin file or state
56+
remains for it.
57+
- Start a new game and an NG+ file, then verify their first tracked level does
58+
not reuse counters from the previous run.
59+
60+
### Repeated calculations and Divine Pulse
61+
62+
- Trigger a level-up, use Divine Pulse to rewind it, then repeat the same level
63+
and verify the exact same stats return.
64+
- If possible, rewind across more than one level-up and verify each repeated
65+
target. Record any mismatch before continuing the release process.
66+
- Confirm the level-up presentation reports the same gains that remain on the
67+
unit afterward.
68+
69+
### Removal and conflicts
70+
71+
- Remove the plugin, restart the game, and confirm future levels use vanilla
72+
random growths without a crash.
73+
- Confirm previously earned stats remain; uninstalling is not a stat rollback.
74+
- Do not enable My 3H Plugin's growth feature at the same time. A conflict test
75+
is not required because both plugins replace the same function.
76+
77+
## Evidence to retain
78+
79+
For each test, retain the CI run URL, artifact commit, FE3H version, Skyline
80+
version, before/after stat screenshots, and whether the save was new, existing,
81+
or NG+. Hardware verification applies only to the exact commit and artifact
82+
that passed.

0 commit comments

Comments
 (0)