Skip to content

Commit a15244f

Browse files
committed
feat(cli): expose GUI batch editing operations
1 parent e6b4079 commit a15244f

3 files changed

Lines changed: 115 additions & 3 deletions

File tree

‎FethCli/Program.cs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,27 @@ private static void ApplyOperations(SaveBuffer save, Dictionary<string, object>
148148
case "sortBattalions":
149149
save.SortBattalions();
150150
break;
151+
case "inventoryDurability":
152+
save.SetInventoryDurability(Value<string>(item, "mode"));
153+
break;
154+
case "characterItemDurability":
155+
save.SetCharacterItemDurability(Value<int>(item, "slot"));
156+
break;
157+
case "maxSkillExp":
158+
save.MaxSkillExperience(Value<int>(item, "slot"));
159+
break;
160+
case "maxClassExp":
161+
save.MaxClassExperience(Value<int>(item, "slot"));
162+
break;
163+
case "unlockAll":
164+
save.UnlockAll(Value<int>(item, "slot"), Value<string>(item, "kind"));
165+
break;
166+
case "fillItems":
167+
save.FillItems(Value<string>(item, "kind"), checked((byte)Value<int>(item, "amount")));
168+
break;
169+
case "addEssentialItems":
170+
save.AddEssentialItems();
171+
break;
151172
case "importCharacter":
152173
save.ImportCharacter(Value<int>(item, "slot"), File.ReadAllBytes(Value<string>(item, "file")));
153174
break;

‎FethCli/SaveBuffer.cs‎

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,97 @@ public void SortBattalions()
180180
Buffer.BlockCopy(entries[i], 0, bytes, baseLoc.Offset + 8 * i, 8);
181181
}
182182

183+
public void SetInventoryDurability(string mode)
184+
{
185+
if (mode != "normal" && mode != "unlimited" && mode != "weapons-unlimited")
186+
throw new ArgumentException("Durability mode must be normal, unlimited, or weapons-unlimited.");
187+
for (int i = 0; i < SaveData_V23.ITEM_COUNT; i++)
188+
{
189+
short id = checked((short)(long)Get($"Items[{i}].Id"));
190+
if (id == -1) continue;
191+
int durability = mode == "unlimited" || (mode == "weapons-unlimited" && id >= 10 && id < 510)
192+
? 100 : Database.GetItemDurability(id);
193+
Set($"Items[{i}].Durability", durability);
194+
}
195+
}
196+
197+
public void SetCharacterItemDurability(int slot)
198+
{
199+
int count = checked((int)(long)Get($"Characters[{slot}].data.ItemCount"));
200+
if (count > Database.MAX_CHARA_ITEMS)
201+
throw new InvalidDataException("Character item count is invalid.");
202+
for (int i = 0; i < count; i++)
203+
if ((long)Get($"Characters[{slot}].data.Items[{i}].Id") != -1)
204+
Set($"Characters[{slot}].data.Items[{i}].Durability", 100);
205+
}
206+
207+
public void MaxSkillExperience(int slot)
208+
{
209+
for (int i = 0; i < Database.MAX_SKILLS; i++)
210+
{
211+
int rank = checked((int)(long)Get($"Characters[{slot}].data.SkillLevel[{i}]"));
212+
if (rank < 0 || rank >= Database.SkillLevelupRank.Length)
213+
throw new InvalidDataException("Character skill rank is outside the game's table.");
214+
Set($"Characters[{slot}].data.SkillExp[{i}]", Database.SkillLevelupRank[rank] - 1);
215+
}
216+
}
217+
218+
public void MaxClassExperience(int slot)
219+
{
220+
for (int i = 0; i < Database.MAX_CLASS; i++)
221+
Set($"Characters[{slot}].data.ClassExp[{i}]", Database.GetMaxClassExp(i));
222+
}
223+
224+
public void UnlockAll(int slot, string kind)
225+
{
226+
string path;
227+
int count;
228+
if (kind == "abilities")
229+
{
230+
path = $"Characters[{slot}].data.Abilities";
231+
count = Database.MAX_ABILITIES * 8;
232+
}
233+
else if (kind == "combat-arts")
234+
{
235+
path = $"Characters[{slot}].data.CombatArts";
236+
count = Database.COMBAT_ARTS_COUNT;
237+
}
238+
else
239+
throw new ArgumentException("Unlock kind must be abilities or combat-arts.");
240+
for (int i = 0; i < count; i++) SetBit(path, i, true);
241+
}
242+
243+
public void FillItems(string kind, byte amount)
244+
{
245+
if (kind == "misc")
246+
for (int i = 0; i < Player_V23.COUNT_MISC_ITEMS; i++) Set($"Player.MiscItems[{i}]", amount);
247+
else if (kind == "gifts")
248+
for (int i = 0; i < Player_V23.COUNT_GIFT_ITEMS; i++) Set($"gifts[{i}]", amount);
249+
else
250+
throw new ArgumentException("Fill kind must be misc or gifts.");
251+
}
252+
253+
public void AddEssentialItems()
254+
{
255+
foreach (short id in Database.EssentialItems.Distinct())
256+
{
257+
int slot = -1;
258+
int free = -1;
259+
for (int i = 0; i < SaveData_V23.ITEM_COUNT; i++)
260+
{
261+
int itemId = checked((int)(long)Get($"Items[{i}].Id"));
262+
if (itemId == id) { slot = i; break; }
263+
if (itemId == -1 && free == -1) free = i;
264+
}
265+
if (slot == -1) slot = free;
266+
if (slot == -1) throw new InvalidOperationException("Inventory has no free slot for essential items.");
267+
Set($"Items[{slot}].Id", id);
268+
Set($"Items[{slot}].Durability", Database.GetItemDurability(id));
269+
Set($"Items[{slot}].Amount", 99);
270+
}
271+
SortItems();
272+
}
273+
183274
public IReadOnlyList<object> Differences()
184275
{
185276
var result = new List<object>();
@@ -295,8 +386,8 @@ private static void Validate(string path, long value)
295386
throw new ArgumentOutOfRangeException(nameof(value), "Professor experience exceeds the GUI limit.");
296387
if (Regex.IsMatch(path, @"\.classlevel\[\d+\]$|\.currentclasslevel$", RegexOptions.IgnoreCase) && (value < 0 || value > Database.MAX_CLASS_LEVEL))
297388
throw new ArgumentOutOfRangeException(nameof(value), "Class mastery must be 0 or 1.");
298-
if (Regex.IsMatch(path, @"^activities\.queststatelist\[\d+\]$", RegexOptions.IgnoreCase) && (value < 0 || value > 3))
299-
throw new ArgumentOutOfRangeException(nameof(value), "Quest state must be 0 through 3.");
389+
if (Regex.IsMatch(path, @"^activities\.queststatelist\[\d+\]$", RegexOptions.IgnoreCase) && (value < 0 || value > 6))
390+
throw new ArgumentOutOfRangeException(nameof(value), "Quest state must be 0 through 6.");
300391
}
301392

302393
private sealed class Location

‎tools/test_cli.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def main() -> None:
4848
assert summary["summary"]["Money"] == 0
4949
assert run(cli, "get", "--input", str(source), "--path", "Items[0].Id")["value"] == -1
5050
catalog = run(cli, "catalog", "--type", "classes")
51-
assert len(catalog) == 100
51+
assert len(catalog) == 101 # 100 classes plus the GUI's "none" sentinel.
5252

5353
patch_file.write_text(json.dumps({
5454
"expectedSha256": summary["sha256"],

0 commit comments

Comments
 (0)