Skip to content

Commit 508fcd7

Browse files
committed
feat(battalions): fill missing obtainable battalions
1 parent d13e174 commit 508fcd7

12 files changed

Lines changed: 297 additions & 22 deletions

File tree

‎CLI.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ The CLI patches only named, mapped fields. Unknown bytes are preserved. `inspect
1818
.\FethEditor.Cli.exe inspect-system --input C:\saves\system --section flags
1919
```
2020

21-
`inspect --section` accepts `summary`, `characters`, `inventory`, `battalions`, `activities`, `supports`, `inheritance`, or `all`. `inspect-system --section` accepts `slots`, `flags`, or `all`. `catalog --type` accepts `items`, `characters`, `classes`, `battalions`, `battalion-skills`, `abilities`, `arts`, `quests`, `supports`, or `support-ranks`. `catalog --type characters|classes|items --id N` returns the database entry's details. Output and errors are JSON. Optional `--language` selects a game database language enum such as `en_u`.
21+
`inspect --section` accepts `summary`, `characters`, `inventory`, `battalions`, `activities`, `supports`, `inheritance`, or `all`. `inspect-system --section` accepts `slots`, `flags`, or `all`. `catalog --type` accepts `items`, `characters`, `classes`, `battalions`, `obtainable-battalions`, `battalion-skills`, `abilities`, `arts`, `quests`, `supports`, or `support-ranks`. `catalog --type characters|classes|items --id N` returns the database entry's details. Output and errors are JSON. Optional `--language` selects a game database language enum such as `en_u`.
2222

2323
`inheritance` reads the separate NG+ journal history: professor rank, 270 historical support-point values, 45 records of 11 skill ranks, and 45 class-mastery bitsets. These are not the current-run values. Each support entry also reports its game's `maximumRank`; `catalog --type supports` lists the available conversation ranks per pair. Character names follow the game's character database order; records without a confirmed playable character keep their numeric `recordIndex`. Support names come from the game's support table.
2424

@@ -49,7 +49,7 @@ Save this as `patch.json`:
4949

5050
The output path must not already exist. `--in-place` is available instead of `--output`; it creates a timestamped backup next to the source before replacement. Prefer a new output file and keep the original. `expectedSha256` prevents applying a patch to a different save. `--dry-run` reports byte-level differences without writing.
5151

52-
Other operations: `setName` (`value`), `sortItems`, `sortBattalions`, `inventoryDurability` (`mode`: `normal`, `unlimited`, or `weapons-unlimited`), `characterItemDurability` (`slot`, restores each carried item's normal durability maximum), `setCharacterItem` (`slot`, `itemSlot`, `id`, optional `durability`; default is that item's normal maximum), `maxSkillExp` (`slot`), `maxClassExp` (`slot`), `unlockAll` (`slot`, `kind`: `abilities` or `combat-arts`), `fillItems` (`kind`: `misc` or `gifts`, `amount`), and `addEssentialItems`. `maxClassExp` changes experience only for classes available to that character; it does not set mastery flags or grant rewards. Single-field edits remain available for unusual save data. Bulk unlocking or filling may produce game-invalid combinations; test those changes separately.
52+
Other operations: `setName` (`value`), `sortItems`, `sortBattalions`, `fillMissingBattalions`, `inventoryDurability` (`mode`: `normal`, `unlimited`, or `weapons-unlimited`), `characterItemDurability` (`slot`, restores each carried item's normal durability maximum), `setCharacterItem` (`slot`, `itemSlot`, `id`, optional `durability`; default is that item's normal maximum), `maxSkillExp` (`slot`), `maxClassExp` (`slot`), `unlockAll` (`slot`, `kind`: `abilities` or `combat-arts`), `fillItems` (`kind`: `misc` or `gifts`, `amount`), and `addEssentialItems`. `fillMissingBattalions` adds one of each missing type from the curated 128-type collection, counting equipped battalions as owned, preserving existing records, and refusing to write if there is insufficient barracks space. It uses the full-stamina, skill, and 400-experience values preserved in the curated save. Example: `{ "op": "fillMissingBattalions" }`. `maxClassExp` changes experience only for classes available to that character; it does not set mastery flags or grant rewards. Single-field edits remain available for unusual save data. Bulk unlocking or filling may produce game-invalid combinations; test those changes separately.
5353

5454
To change system flags, use a separate patch file with operations such as `{ "op": "setSystemFlag", "index": 8, "value": true }`, then run `apply-system --input C:\saves\system --patch patch.json --output C:\saves\edited-system`. `--dry-run` and `--in-place` work here too. An unchanged patch does not write a file. A written version-5 input is upgraded to version 7.
5555

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Edit Fire Emblem: Three Houses saves, including inherited New Game+ progress.
99
- **Main:** Edit player details, money, renown, professor experience and rank, activity points, goddess statues, game values, and monthly statistics. The inherited professor rank is available here too.
1010
- **Items:** Edit inventory items and durability, restore durability to each item's normal maximum, sort the inventory, add essential items, and change miscellaneous item and gift quantities.
1111
- **Roster:** Edit character stats, equipped abilities and combat arts, held items, skill experience, learned magic, flags, class experience and mastery, unlocked abilities and combat arts, and import or export individual characters.
12-
- **Battalions:** Change battalion type, assigned character, experience, stamina, and skill; sort the list.
12+
- **Battalions:** Change battalion type, assigned character, experience, stamina, and skill; sort the list or fill missing battalions from the curated collection.
1313
- **Quests:** Search quests and change their state.
1414
- **Support:** Edit the current run's support ranks and underlying points.
1515
- **Database:** Look up characters, classes, and items without changing the save.

‎cli/Program.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ private static void ApplyOperations(SaveBuffer save, Dictionary<string, object>
167167
case "sortBattalions":
168168
save.SortBattalions();
169169
break;
170+
case "fillMissingBattalions":
171+
save.FillMissingBattalions();
172+
break;
170173
case "inventoryDurability":
171174
save.SetInventoryDurability(Value<string>(item, "mode"));
172175
break;
@@ -499,6 +502,11 @@ private static object Catalog(string type)
499502
}).ToArray();
500503
case "classes": return Database.ClassList.Select(entry => new { id = entry.Key, name = entry.Value }).ToArray();
501504
case "battalions": return Database.BattalionList.Select(entry => new { id = entry.Key, name = entry.Value }).ToArray();
505+
case "obtainable-battalions": return ObtainableBattalions.All.Select(entry => new
506+
{
507+
id = entry.Type, name = Database.GetBattalionName(entry.Type),
508+
experience = ObtainableBattalions.Experience, stamina = entry.Stamina, skill = entry.Skill
509+
}).ToArray();
502510
case "battalion-skills": return Database.BattalionSkillList.Select(entry => new { id = entry.Key, name = entry.Value }).ToArray();
503511
case "abilities": return Database.AbilityList.Select(entry => new { id = entry.Key, name = entry.Value }).ToArray();
504512
case "arts": return Database.CombatArtList.Select(entry => new { id = entry.Key, name = entry.Value }).ToArray();

‎core/Core.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<Compile Include="ClassEligibility.cs" />
1616
<Compile Include="SupportRankPresets.cs" />
1717
<Compile Include="SupportPairRanks.cs" />
18+
<Compile Include="ObtainableBattalions.cs" />
1819
<Compile Include="SystemBuffer.cs" />
1920
<Compile Include="VerifiedFileWriter.cs" />
2021
<Compile Include="NgPlusJournal.cs" />

‎core/ObtainableBattalions.cs‎

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace FethEditor.Core
5+
{
6+
/// <summary>One of each obtainable battalion from the curated all-battalions save.</summary>
7+
public static class ObtainableBattalions
8+
{
9+
public const ushort Experience = 400;
10+
11+
// Type, full stamina, and skill copied from slot00-all-battalions.
12+
// The user's save itself is not distributed with the application.
13+
private static readonly (byte Type, ushort Stamina, byte Skill)[] Templates =
14+
{
15+
(0, 30, 4),
16+
(1, 30, 0),
17+
(2, 30, 25),
18+
(3, 30, 13),
19+
(4, 30, 32),
20+
(5, 60, 1),
21+
(6, 60, 5),
22+
(7, 75, 7),
23+
(8, 105, 24),
24+
(9, 105, 12),
25+
(10, 30, 3),
26+
(11, 30, 36),
27+
(12, 30, 32),
28+
(13, 30, 6),
29+
(14, 45, 7),
30+
(15, 60, 5),
31+
(16, 60, 21),
32+
(17, 60, 14),
33+
(18, 45, 10),
34+
(19, 75, 5),
35+
(20, 60, 15),
36+
(21, 60, 2),
37+
(22, 75, 31),
38+
(23, 60, 26),
39+
(24, 75, 8),
40+
(25, 75, 5),
41+
(26, 75, 1),
42+
(27, 105, 0),
43+
(28, 105, 16),
44+
(29, 105, 24),
45+
(30, 30, 3),
46+
(31, 30, 15),
47+
(32, 30, 33),
48+
(33, 30, 24),
49+
(34, 45, 25),
50+
(35, 60, 7),
51+
(36, 60, 21),
52+
(37, 60, 6),
53+
(38, 45, 11),
54+
(39, 60, 0),
55+
(40, 60, 1),
56+
(41, 60, 19),
57+
(42, 90, 15),
58+
(43, 75, 16),
59+
(44, 90, 22),
60+
(45, 75, 31),
61+
(46, 60, 26),
62+
(47, 105, 16),
63+
(48, 105, 11),
64+
(49, 105, 27),
65+
(50, 30, 3),
66+
(51, 30, 35),
67+
(52, 30, 34),
68+
(53, 30, 9),
69+
(54, 45, 7),
70+
(55, 60, 5),
71+
(56, 60, 4),
72+
(57, 60, 6),
73+
(58, 45, 12),
74+
(59, 60, 8),
75+
(60, 60, 1),
76+
(61, 60, 21),
77+
(62, 90, 15),
78+
(63, 60, 13),
79+
(64, 60, 26),
80+
(65, 60, 14),
81+
(66, 75, 17),
82+
(67, 105, 7),
83+
(68, 105, 17),
84+
(69, 105, 16),
85+
(70, 120, 29),
86+
(71, 120, 30),
87+
(72, 120, 28),
88+
(73, 75, 12),
89+
(74, 105, 7),
90+
(75, 75, 26),
91+
(76, 105, 0),
92+
(77, 75, 6),
93+
(78, 75, 27),
94+
(79, 105, 17),
95+
(80, 120, 31),
96+
(81, 105, 0),
97+
(82, 105, 7),
98+
(83, 105, 25),
99+
(84, 75, 13),
100+
(85, 75, 10),
101+
(86, 105, 3),
102+
(87, 105, 7),
103+
(88, 105, 5),
104+
(89, 105, 1),
105+
(90, 75, 12),
106+
(91, 105, 14),
107+
(92, 105, 7),
108+
(93, 75, 9),
109+
(94, 105, 7),
110+
(95, 105, 13),
111+
(96, 75, 7),
112+
(97, 105, 7),
113+
(98, 105, 25),
114+
(100, 30, 4),
115+
(101, 30, 4),
116+
(102, 30, 4),
117+
(103, 30, 4),
118+
(105, 30, 4),
119+
(106, 30, 6),
120+
(107, 30, 0),
121+
(108, 45, 12),
122+
(109, 30, 4),
123+
(110, 30, 4),
124+
(111, 30, 4),
125+
(112, 30, 4),
126+
(113, 60, 0),
127+
(114, 45, 6),
128+
(115, 60, 26),
129+
(116, 60, 7),
130+
(170, 75, 7),
131+
(172, 75, 17),
132+
(173, 75, 4),
133+
(174, 75, 4),
134+
(175, 75, 4),
135+
(176, 75, 4),
136+
(190, 105, 8),
137+
(191, 105, 0),
138+
(192, 105, 12),
139+
(193, 105, 10),
140+
(197, 45, 38),
141+
(198, 45, 37),
142+
(199, 45, 39),
143+
};
144+
145+
public static IReadOnlyList<(byte Type, ushort Stamina, byte Skill)> All { get; } =
146+
Array.AsReadOnly(Templates);
147+
}
148+
}

‎core/SaveBuffer.cs‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,40 @@ public void SortBattalions()
195195
Buffer.BlockCopy(entries[i], 0, bytes, baseLoc.Offset + 8 * i, 8);
196196
}
197197

198+
public int FillMissingBattalions()
199+
{
200+
SaveData_V23 data = Data;
201+
var ownedTypes = new HashSet<int>(data.Player.Battalions
202+
.Where(battalion => battalion.Type < Database.BATTALION_COUNT)
203+
.Select(battalion => (int)battalion.Type));
204+
foreach (Character_V23 character in data.Characters)
205+
if (character.data.Id >= 0 && character.data.Level > 0
206+
&& character.data.EquippedBattalion.Type < Database.BATTALION_COUNT)
207+
ownedTypes.Add(character.data.EquippedBattalion.Type);
208+
209+
var missing = ObtainableBattalions.All
210+
.Where(template => !ownedTypes.Contains(template.Type)).ToArray();
211+
int[] freeSlots = data.Player.Battalions
212+
.Select((battalion, index) => (battalion, index))
213+
.Where(entry => entry.battalion.Type == Database.BATTALION_COUNT)
214+
.Select(entry => entry.index).ToArray();
215+
if (missing.Length > freeSlots.Length)
216+
throw new InvalidOperationException(
217+
$"Need {missing.Length} free battalion slots, but only {freeSlots.Length} remain.");
218+
219+
for (int i = 0; i < missing.Length; i++)
220+
{
221+
string path = $"Player.Battalions[{freeSlots[i]}].";
222+
Set(path + "CharacterId", -1);
223+
Set(path + "Exp", ObtainableBattalions.Experience);
224+
Set(path + "Stamina", missing[i].Stamina);
225+
Set(path + "Type", missing[i].Type);
226+
Set(path + "Skill", missing[i].Skill);
227+
}
228+
if (missing.Length > 0) SortBattalions();
229+
return missing.Length;
230+
}
231+
198232
public void SetInventoryDurability(string mode)
199233
{
200234
if (mode != "normal" && mode != "unlimited" && mode != "weapons-unlimited")

‎gui/Localization/zh_hans.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@
9797
"Stamina": "体力",
9898
"Skill": "技能",
9999
"Sort Battalion": "整理骑士团",
100+
"Fill Missing Battalions": "补齐缺少的骑士团",
101+
"Added {0} missing battalions.": "已补齐 {0} 个骑士团。",
102+
"All obtainable battalions are already owned.": "可获得的骑士团已全部拥有。",
100103
"Quest List": "任务列表",
101104
"Quest Editor": "任务编辑",
102105
"Set State": "设置状态",

‎gui/MainWindow.Battalions.cs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,25 @@ private void SortBattalion_Click(object? sender, RoutedEventArgs e)
111111
Status.Text = error.Message;
112112
}
113113
}
114+
115+
private void FillMissingBattalions_Click(object? sender, RoutedEventArgs e)
116+
{
117+
if (_save is null) return;
118+
try
119+
{
120+
int added = _save.FillMissingBattalions();
121+
if (added > 0)
122+
{
123+
RefreshBattalions();
124+
MarkChanged();
125+
}
126+
Status.Text = added == 0
127+
? UiStrings.Translate("All obtainable battalions are already owned.", _databaseLanguage)
128+
: UiStrings.Format("Added {0} missing battalions.", _databaseLanguage, added);
129+
}
130+
catch (Exception error)
131+
{
132+
Status.Text = error.Message;
133+
}
134+
}
114135
}

‎gui/MainWindow.axaml‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@
382382
<TabItem Name="BattalionTab" Header="Battalions">
383383
<Grid Margin="8" ColumnDefinitions="320,*" ColumnSpacing="10">
384384
<suki:GlassCard Name="BattalionListCard" Classes="section" Grid.Column="0">
385-
<Grid RowDefinitions="Auto,Auto,*" RowSpacing="8">
385+
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto" RowSpacing="8">
386386
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="8">
387387
<TextBlock Text="Battalion List" FontWeight="SemiBold" />
388388
<TextBlock Name="BattalionUsage" Grid.Column="1" HorizontalAlignment="Right" />
@@ -391,6 +391,10 @@
391391
TextChanged="SearchableList_TextChanged" />
392392
<ListBox Classes="Small" Name="BattalionList" Grid.Row="2"
393393
SelectionChanged="BattalionList_SelectionChanged" />
394+
<Button Classes="Small" Name="SortBattalionButton" Grid.Row="3"
395+
Content="Sort Battalion" Click="SortBattalion_Click" />
396+
<Button Classes="Small" Name="FillMissingBattalionsButton" Grid.Row="4"
397+
Content="Fill Missing Battalions" Click="FillMissingBattalions_Click" />
394398
</Grid>
395399
</suki:GlassCard>
396400
<suki:GlassCard Name="BattalionEditorCard" Classes="section" Grid.Column="1">
@@ -407,7 +411,6 @@
407411
<TextBlock Text="Skill" />
408412
<ComboBox Classes="Small" Name="BattalionSkill" />
409413
<Button Classes="Small" Name="SaveBattalionButton" Content="Apply Changes" Click="SaveBattalion_Click" />
410-
<Button Classes="Small" Content="Sort Battalion" Click="SortBattalion_Click" />
411414
</StackPanel>
412415
</suki:GlassCard>
413416
</Grid>
@@ -528,7 +531,7 @@
528531
<TabControl Name="InheritanceTabs">
529532
<TabItem Name="InheritedSkillsTab" Header="Skills">
530533
<StackPanel Margin="0,8,0,0" VerticalAlignment="Top">
531-
<UniformGrid Name="SkillRows" Columns="2" />
534+
<UniformGrid Name="SkillRows" Columns="2" MaxWidth="760" HorizontalAlignment="Center" />
532535
</StackPanel>
533536
</TabItem>
534537
<TabItem Name="InheritedClassesTab" Header="Class Mastery">

‎gui/MainWindow.axaml.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,9 +442,9 @@ private void ShowCharacter()
442442
int skillIndex = skill;
443443
var row = new Grid
444444
{
445-
ColumnDefinitions = new ColumnDefinitions("56,200"),
445+
ColumnDefinitions = new ColumnDefinitions("110,200"),
446446
ColumnSpacing = 8,
447-
Margin = new Avalonia.Thickness(0, 0, 0, 12)
447+
Margin = new Avalonia.Thickness(0, 0, 32, 12)
448448
};
449449
row.Children.Add(new TextBlock
450450
{

0 commit comments

Comments
 (0)