Skip to content

Listpatch Updates#1258

Merged
shananas merged 16 commits into
OpenKH:masterfrom
Zurphing:listpatch_updates_alt
Apr 30, 2026
Merged

Listpatch Updates#1258
shananas merged 16 commits into
OpenKH:masterfrom
Zurphing:listpatch_updates_alt

Conversation

@Zurphing
Copy link
Copy Markdown
Contributor

@Zurphing Zurphing commented Apr 16, 2026

The following updates have been made for Listpatching:

Lvup has been updated to no longer be hardcoded to 99 entries. Entries are able to be added as specified, and the pointers in the header will be adjusted to reflect this. Mods from prior versions should remain unaffected by this change,.

Went has been added as a listpatchable field, which should improve compatibility with mods that add multiple weapons..

The other pref files Prty and Sstm have been added as valid listpatches, in addition to 14mission.bar's 'slct' file.

Tests have been added for each listpatch.

Due to how large the listpatch section has grown over time, I ended up separating all of the listpatch examples out into their own document, which should make the creatingMods page easier to browse through.

Summary by CodeRabbit

  • New Features

    • Added support for modifying four new Kingdom Hearts II data structures: weapon entries, party preferences, system settings, and choice menus via the mod patcher.
    • Enhanced character level modification capabilities.
  • Tests

    • Added comprehensive test coverage for new data structure patching.
  • Documentation

    • Added detailed guide documenting all supported mod patch types with examples for mod creators.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

This pull request adds comprehensive support for patching additional Kingdom Hearts II system data structures. It introduces custom binary serialization for LVUP data, creates new table definitions for SLCT/WENT/PRTY/SSTM data, extends the patcher processor to support these new formats, adds test coverage, and updates documentation with new listpatch examples.

Changes

Cohort / File(s) Summary
Custom Binary Serialization
OpenKh.Kh2/Battle/Lvup.cs
Replaced Xe.BinaryMapper decorators with custom Read(Stream) and Write(Stream) implementations using explicit binary parsing, offset table construction, and manual stream seeking for character-level data.
New Table Definitions
OpenKh.Kh2/Slct.cs, OpenKh.Kh2/SystemData/Went.cs
Added Slct table with ChoiceEntry structure and Went class supporting offset-based set serialization with weapon ID list management and deduplication logic.
Enhanced Pref Files
OpenKh.Kh2/SystemData/Pref/Prty.cs, OpenKh.Kh2/SystemData/Pref/Sstm.cs
Added PrtyFile wrapper with offset-based unique entry deserialization, and extended Sstm with MagicCode/Unknown fields plus custom stream IO helpers with stream rewinding and truncation.
Patcher Extension
OpenKh.Patcher/PatcherProcessor.cs
Extended PatchList to support patching went (offset mapping with weapon ID expansion), sstm (reflection-based property setting), prty (offset-indexed character entry updates), and slct (upsert by ID); added wentNameMap for header name resolution.
Test Coverage
OpenKh.Tests/Patcher/PatcherTests.cs
Added four new xUnit test cases verifying patcher behavior for slct, went, sstm, and prty patches, including in-memory asset construction, YAML patch writing, and binary output verification.
Documentation
docs/tool/GUI.ModsManager/creatingMods.md, docs/tool/GUI.ModsManager/listpatch_information.md
Refactored creatingMods.md by consolidating detailed listpatch examples into new listpatch_information.md, added documentation for went, prty, sstm, and slct with YAML examples and usage notes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • shananas
  • Delta-473
  • kenjiuno

Poem

🐰 A patcher's tale, offset by offset we go,
WENT weapons, SSTM settings, all in a row!
Custom streams and seeks, binary dances fine,
Four new formats patched, the data's divine!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Listpatch Updates' is generic and vague—it mentions the feature area but does not convey the specific scope or main accomplishment of the PR (e.g., removing lvup limits, adding went/prty/sstm/slct support). Consider a more descriptive title such as 'Add listpatch support for went, prty, sstm, slct and expand lvup capacity' to better convey the PR's main changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (5)
OpenKh.Kh2/Slct.cs (1)

1-8: Unused imports can be removed.

OpenKh.Common, OpenKh.Kh2.Extensions, OpenKh.Kh2.Utils, and System.Linq appear to be unused in this file.

♻️ Proposed fix
-using OpenKh.Common;
-using OpenKh.Kh2.Extensions;
-using OpenKh.Kh2.Utils;
-using System;
 using System.Collections.Generic;
 using System.IO;
-using System.Linq;
 using Xe.BinaryMapper;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@OpenKh.Kh2/Slct.cs` around lines 1 - 8, The file contains unused using
directives—remove the unnecessary imports OpenKh.Common, OpenKh.Kh2.Extensions,
OpenKh.Kh2.Utils, and System.Linq from the top of Slct.cs and keep only the
required usings (e.g., System, System.Collections.Generic, System.IO,
Xe.BinaryMapper); update any code references if removal reveals missing types
and rebuild to ensure no compilation errors in the Slct class or its methods.
OpenKh.Kh2/SystemData/Pref/Prty.cs (1)

126-138: Consider removing commented-out code.

Similar to Sstm.cs, the commented-out Read/Write methods appear to be dead code. Consider removing them if the functionality has been replaced.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@OpenKh.Kh2/SystemData/Pref/Prty.cs` around lines 126 - 138, Remove the dead
commented-out Read/Write block inside the Prty class: delete the multi-line
commented methods that call BaseTableOffsetWithPaddings<Prty>.ReadWithOffsets
and WriteWithOffsets so the file matches the cleaned Sstm.cs style; before
removal, search for any references to these methods (Prty.Read / Prty.Write or
BaseTableOffsetWithPaddings<Prty>) to ensure they are not relied upon or, if
needed, replace with the new implementation rather than leaving commented code.
docs/tool/GUI.ModsManager/listpatch_information.md (1)

39-42: Add language identifiers to fenced code blocks.

Multiple code blocks throughout this documentation lack language specifiers (flagged by markdownlint MD040). Adding yaml as the language identifier will enable proper syntax highlighting for mod authors.

📝 Example fix
-```
+```yaml
 2:
   ItemId: 347

Apply this pattern to all code blocks in the document.
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/tool/GUI.ModsManager/listpatch_information.md around lines 39 - 42, The
fenced code blocks in listpatch_information.md (e.g., the block containing the
snippet "2:\n ItemId: 347") lack a language tag causing markdownlint MD040;
update every triple-backtick block to include the yaml identifier (change ``` to

with proper YAML highlighting.
OpenKh.Kh2/SystemData/Pref/Sstm.cs (1)

123-133: Consider removing commented-out code.

The commented-out SstmPatch class and associated Read/Write methods appear to be dead code. If this functionality is no longer needed, consider removing it entirely rather than leaving it commented out.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@OpenKh.Kh2/SystemData/Pref/Sstm.cs` around lines 123 - 133, Remove the dead
commented-out block containing the SstmPatch class and the commented Read/Write
wrappers (SstmPatch, Read(Stream) => BaseTableSstm<Sstm>.Read, Write(Stream,
IEnumerable<Sstm>) => BaseTableSstm<Sstm>.Write); if you still need that
functionality either fully implement SstmPatch and restore the Read/Write
methods with working logic or delete the commented code to keep the file clean,
then build and run tests to ensure no references remain to SstmPatch or those
methods.
OpenKh.Kh2/SystemData/Went.cs (1)

47-48: Consider extracting magic number 70 to a constant.

The header size of 70 entries is hardcoded in multiple places. A named constant would improve readability and maintainability.

♻️ Proposed refactor
 public class Went
 {
+    private const int HeaderEntryCount = 70;
+
     public List<uint> Offsets { get; set; }
     public List<WentSet> Sets { get; set; }
     
     // ...
     
     public static Went Read(Stream stream)
     {
         // ...
-        for (int i = 0; i < 70; i++)
+        for (int i = 0; i < HeaderEntryCount; i++)
             went.Offsets.Add(ReadUInt32(stream));

Also applies to: 82-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@OpenKh.Kh2/SystemData/Went.cs` around lines 47 - 48, Replace the hardcoded
magic number 70 with a named constant in the Went class: declare a private const
int (e.g., OffsetCount = 70) near the top of the Went class and use that
constant wherever the loop uses 70 (the for loops that call
went.Offsets.Add(ReadUInt32(stream)) and the other occurrences around the 82-83
region) so all header-size references use the named constant for clarity and
maintainability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/tool/GUI.ModsManager/listpatch_information.md`:
- Around line 5-6: Remove the empty link in the Table of Contents header by
replacing the markdown "[Table of Contents]()" with either plain text "Table of
Contents" or a valid anchor like "[Table of Contents](`#table-of-contents`)";
update the line containing the "[Table of Contents]()" header in
listpatch_information.md accordingly so markdownlint MD042 is resolved.
- Around line 183-184: The example uses two inconsistent property names: change
the incorrect FlagNegationForWorld occurrence to NegationFlagForWorld so it
matches the Memt class property and the earlier example usage; update the
example entry that currently uses FlagNegationForWorld to use
NegationFlagForWorld to ensure consistency with the Memt class and the prior
NegationFlagForWorld line.

In `@OpenKh.Kh2/Battle/Lvup.cs`:
- Around line 111-119: WriteCharacter computes ch.NumLevels using ch.Levels but
then iterates ch.Levels without a null check, risking a NullReferenceException;
update WriteCharacter (the ch.NumLevels assignment, WriteInt32 call and the
foreach that calls BinaryMapping.WriteObject) to safely handle a null Levels by
either iterating over ch.Levels only when it is not null (e.g., if (ch.Levels !=
null) foreach ...) or by iterating over a safe empty sequence (e.g., ch.Levels
?? Enumerable.Empty<LevelType>()), so NumLevels remains correct and
BinaryMapping.WriteObject is only called for existing levels.

In `@OpenKh.Kh2/SystemData/Went.cs`:
- Around line 96-111: The loop over uniqueOffsets uses Sets.Find(x =>
x.OriginalOffset == oldOffset) which can return null, then dereferences set
(set.SetSize, set.WeaponIds) causing a NullReferenceException; update the loop
in the method that writes WENT entries to null-check the result of Sets.Find
(the local variable set) and handle the missing case—either skip this offset,
log/throw a descriptive exception referencing the bad OriginalOffset, or create
a default Set object—before mutating set.SetSize, updating offsetMap and calling
WriteUInt32; ensure subsequent uses of set.WeaponIds are safe after the
null-handling change.

In `@OpenKh.Patcher/PatcherProcessor.cs`:
- Around line 871-893: The sstm branch reads and mutates a Kh2.SystemData.Sstm
instance but forgets to reset the stream before writing, so the
sstm.Write(stream) will append instead of overwrite; update the case handling
(around the deserializer.Deserialize and sstm variable) to call
stream.SetLength(0) and stream.Position = 0 (same reset used in the went case)
immediately before invoking sstm.Write(stream) to ensure the stream is truncated
and writing starts at the beginning.
- Around line 811-823: Replace the direct index access to characterMap with a
TryGetValue check to validate character.Key before using it: call
characterMap.TryGetValue(character.Key, out var mapIndex) and if it returns
false throw a descriptive exception naming the invalid character key and the
YAML source; use mapIndex - 1 to compute charIndex and proceed as before. Also
remove the redundant assignment to characterData.NumLevels (since
Lvup.WriteSingleCharacter recalculates it), and keep the loop that ensures
Levels has enough entries and assigns level.Value to
characterData.Levels[levelIndex].
- Around line 895-928: The prty case handler must reset the stream position
before writing and validate dictionary/list accesses to avoid runtime
exceptions: after reading and applying changes to the Kh2.SystemData.PrtyFile
(via Kh2.SystemData.PrtyFile.Read and the deserialized mod), set the stream
position (and truncate if necessary) before calling file.Write(stream); and
before using file.Offsets[realIndex] and file.UniqueEntries[offset] ensure the
keys/indices exist (use bounds checks or ContainsKey/TryGetValue on file.Offsets
and file.UniqueEntries) and throw a clear Exception mentioning the character key
or missing offset/entry when validation fails instead of letting
IndexOutOfRange/KeyNotFound propagate; keep the existing use of
Kh2.SystemData.Prty.CharacterMap.TryGetValue to resolve character -> index.

---

Nitpick comments:
In `@docs/tool/GUI.ModsManager/listpatch_information.md`:
- Around line 39-42: The fenced code blocks in listpatch_information.md (e.g.,
the block containing the snippet "2:\n  ItemId: 347") lack a language tag
causing markdownlint MD040; update every triple-backtick block to include the
yaml identifier (change ``` to ```yaml) so all examples (including the shown "2:
ItemId: 347" block) render with proper YAML highlighting.

In `@OpenKh.Kh2/Slct.cs`:
- Around line 1-8: The file contains unused using directives—remove the
unnecessary imports OpenKh.Common, OpenKh.Kh2.Extensions, OpenKh.Kh2.Utils, and
System.Linq from the top of Slct.cs and keep only the required usings (e.g.,
System, System.Collections.Generic, System.IO, Xe.BinaryMapper); update any code
references if removal reveals missing types and rebuild to ensure no compilation
errors in the Slct class or its methods.

In `@OpenKh.Kh2/SystemData/Pref/Prty.cs`:
- Around line 126-138: Remove the dead commented-out Read/Write block inside the
Prty class: delete the multi-line commented methods that call
BaseTableOffsetWithPaddings<Prty>.ReadWithOffsets and WriteWithOffsets so the
file matches the cleaned Sstm.cs style; before removal, search for any
references to these methods (Prty.Read / Prty.Write or
BaseTableOffsetWithPaddings<Prty>) to ensure they are not relied upon or, if
needed, replace with the new implementation rather than leaving commented code.

In `@OpenKh.Kh2/SystemData/Pref/Sstm.cs`:
- Around line 123-133: Remove the dead commented-out block containing the
SstmPatch class and the commented Read/Write wrappers (SstmPatch, Read(Stream)
=> BaseTableSstm<Sstm>.Read, Write(Stream, IEnumerable<Sstm>) =>
BaseTableSstm<Sstm>.Write); if you still need that functionality either fully
implement SstmPatch and restore the Read/Write methods with working logic or
delete the commented code to keep the file clean, then build and run tests to
ensure no references remain to SstmPatch or those methods.

In `@OpenKh.Kh2/SystemData/Went.cs`:
- Around line 47-48: Replace the hardcoded magic number 70 with a named constant
in the Went class: declare a private const int (e.g., OffsetCount = 70) near the
top of the Went class and use that constant wherever the loop uses 70 (the for
loops that call went.Offsets.Add(ReadUInt32(stream)) and the other occurrences
around the 82-83 region) so all header-size references use the named constant
for clarity and maintainability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d142ff25-35ea-4545-9477-b7f3cb2cdc90

📥 Commits

Reviewing files that changed from the base of the PR and between 96a032b and 70a6521.

📒 Files selected for processing (9)
  • OpenKh.Kh2/Battle/Lvup.cs
  • OpenKh.Kh2/Slct.cs
  • OpenKh.Kh2/SystemData/Pref/Prty.cs
  • OpenKh.Kh2/SystemData/Pref/Sstm.cs
  • OpenKh.Kh2/SystemData/Went.cs
  • OpenKh.Patcher/PatcherProcessor.cs
  • OpenKh.Tests/Patcher/PatcherTests.cs
  • docs/tool/GUI.ModsManager/creatingMods.md
  • docs/tool/GUI.ModsManager/listpatch_information.md

Comment on lines +5 to +6

# [Table of Contents]()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Empty link in Table of Contents header.

The Table of Contents header has an empty link [Table of Contents]() which is flagged by markdownlint (MD042). Either add a valid anchor or remove the link syntax.

📝 Proposed fix
-# [Table of Contents]()
+# Table of Contents
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# [Table of Contents]()
# Table of Contents
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 5-5: No empty links

(MD042, no-empty-links)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tool/GUI.ModsManager/listpatch_information.md` around lines 5 - 6,
Remove the empty link in the Table of Contents header by replacing the markdown
"[Table of Contents]()" with either plain text "Table of Contents" or a valid
anchor like "[Table of Contents](`#table-of-contents`)"; update the line
containing the "[Table of Contents]()" header in listpatch_information.md
accordingly so markdownlint MD042 is resolved.

Comment on lines +183 to +184
NegationFlagForWorld: Twilight Town #Specify world name
CheckArea: 2
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the correct property name in the Memt class
rg -n "FlagNegationForWorld|NegationFlagForWorld" --type=cs

Repository: OpenKH/OpenKh

Length of output: 654


🏁 Script executed:

sed -n '180,200p' docs/tool/GUI.ModsManager/listpatch_information.md

Repository: OpenKH/OpenKh

Length of output: 791


Inconsistent property name in MEMT example.

Line 183 uses NegationFlagForWorld but line 194 uses FlagNegationForWorld. Line 194 should be NegationFlagForWorld to match the actual property name in the Memt class and be consistent with line 183.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tool/GUI.ModsManager/listpatch_information.md` around lines 183 - 184,
The example uses two inconsistent property names: change the incorrect
FlagNegationForWorld occurrence to NegationFlagForWorld so it matches the Memt
class property and the earlier example usage; update the example entry that
currently uses FlagNegationForWorld to use NegationFlagForWorld to ensure
consistency with the Memt class and the prior NegationFlagForWorld line.

Comment thread OpenKh.Kh2/Battle/Lvup.cs
Comment on lines +111 to +119
private static void WriteCharacter(Stream stream, PlayableCharacter ch)
{
ch.NumLevels = ch.Levels?.Count ?? 0;

WriteInt32(stream, ch.NumLevels);

foreach (var level in ch.Levels)
BinaryMapping.WriteObject(stream, level);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential NullReferenceException if ch.Levels is null.

Line 113 handles null with ch.Levels?.Count ?? 0, but line 117 iterates ch.Levels directly without a null check, which will throw if Levels is null.

🐛 Proposed fix
 private static void WriteCharacter(Stream stream, PlayableCharacter ch)
 {
     ch.NumLevels = ch.Levels?.Count ?? 0;

     WriteInt32(stream, ch.NumLevels);

-    foreach (var level in ch.Levels)
+    if (ch.Levels != null)
+    {
+        foreach (var level in ch.Levels)
             BinaryMapping.WriteObject(stream, level);
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static void WriteCharacter(Stream stream, PlayableCharacter ch)
{
ch.NumLevels = ch.Levels?.Count ?? 0;
WriteInt32(stream, ch.NumLevels);
foreach (var level in ch.Levels)
BinaryMapping.WriteObject(stream, level);
}
private static void WriteCharacter(Stream stream, PlayableCharacter ch)
{
ch.NumLevels = ch.Levels?.Count ?? 0;
WriteInt32(stream, ch.NumLevels);
if (ch.Levels != null)
{
foreach (var level in ch.Levels)
BinaryMapping.WriteObject(stream, level);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@OpenKh.Kh2/Battle/Lvup.cs` around lines 111 - 119, WriteCharacter computes
ch.NumLevels using ch.Levels but then iterates ch.Levels without a null check,
risking a NullReferenceException; update WriteCharacter (the ch.NumLevels
assignment, WriteInt32 call and the foreach that calls
BinaryMapping.WriteObject) to safely handle a null Levels by either iterating
over ch.Levels only when it is not null (e.g., if (ch.Levels != null) foreach
...) or by iterating over a safe empty sequence (e.g., ch.Levels ??
Enumerable.Empty<LevelType>()), so NumLevels remains correct and
BinaryMapping.WriteObject is only called for existing levels.

Comment on lines +96 to +111
foreach (var oldOffset in uniqueOffsets)
{
var set = Sets.Find(x => x.OriginalOffset == oldOffset);

long currentPos = stream.Position;
uint newOffset = (uint)((currentPos - fileStart) / 4);

offsetMap[oldOffset] = newOffset;

set.SetSize = (uint)(set.WeaponIds.Count + 1);

WriteUInt32(stream, set.SetSize);

foreach (var id in set.WeaponIds)
WriteUInt32(stream, id);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential NullReferenceException if set not found.

Sets.Find() on line 98 can return null if no matching OriginalOffset is found. Line 105 then accesses set.SetSize without a null check.

🐛 Proposed fix
 foreach (var oldOffset in uniqueOffsets)
 {
     var set = Sets.Find(x => x.OriginalOffset == oldOffset);
+    if (set == null)
+        throw new InvalidOperationException($"WentSet not found for offset {oldOffset}");

     long currentPos = stream.Position;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach (var oldOffset in uniqueOffsets)
{
var set = Sets.Find(x => x.OriginalOffset == oldOffset);
long currentPos = stream.Position;
uint newOffset = (uint)((currentPos - fileStart) / 4);
offsetMap[oldOffset] = newOffset;
set.SetSize = (uint)(set.WeaponIds.Count + 1);
WriteUInt32(stream, set.SetSize);
foreach (var id in set.WeaponIds)
WriteUInt32(stream, id);
}
foreach (var oldOffset in uniqueOffsets)
{
var set = Sets.Find(x => x.OriginalOffset == oldOffset);
if (set == null)
throw new InvalidOperationException($"WentSet not found for offset {oldOffset}");
long currentPos = stream.Position;
uint newOffset = (uint)((currentPos - fileStart) / 4);
offsetMap[oldOffset] = newOffset;
set.SetSize = (uint)(set.WeaponIds.Count + 1);
WriteUInt32(stream, set.SetSize);
foreach (var id in set.WeaponIds)
WriteUInt32(stream, id);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@OpenKh.Kh2/SystemData/Went.cs` around lines 96 - 111, The loop over
uniqueOffsets uses Sets.Find(x => x.OriginalOffset == oldOffset) which can
return null, then dereferences set (set.SetSize, set.WeaponIds) causing a
NullReferenceException; update the loop in the method that writes WENT entries
to null-check the result of Sets.Find (the local variable set) and handle the
missing case—either skip this offset, log/throw a descriptive exception
referencing the bad OriginalOffset, or create a default Set object—before
mutating set.SetSize, updating offsetMap and calling WriteUInt32; ensure
subsequent uses of set.WeaponIds are safe after the null-handling change.

Comment on lines +811 to +823
var charIndex = characterMap[character.Key] - 1;
var characterData = levelList.Characters[charIndex];

foreach (var level in character.Value)
{
levelList.Characters[characterMap[character.Key] - 1].Levels[level.Key - 1] = moddedLevels[character.Key][level.Key];
int levelIndex = level.Key - 1;
while (characterData.Levels.Count <= levelIndex)
{
characterData.Levels.Add(new Kh2.Battle.Lvup.PlayableCharacter.Level());
}
characterData.Levels[levelIndex] = level.Value;
}
characterData.NumLevels = characterData.Levels.Count;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing error handling for unknown character names.

The code directly accesses characterMap[character.Key] without validation, which will throw a KeyNotFoundException with an unhelpful message if an invalid character name is provided in the YAML. Other new cases (e.g., went at line 839, prty at line 903) use TryGetValue with descriptive exceptions.

Additionally, line 823 setting NumLevels is redundant since Lvup.WriteSingleCharacter already recalculates it from Levels.Count (per the context snippet from Lvup.cs).

Suggested improvement for consistency
 foreach (var character in moddedLevels)
 {
-    var charIndex = characterMap[character.Key] - 1;
+    if (!characterMap.TryGetValue(character.Key, out byte charId))
+        throw new Exception($"Invalid character name: {character.Key}");
+    var charIndex = charId - 1;
     var characterData = levelList.Characters[charIndex];

     foreach (var level in character.Value)
     {
         int levelIndex = level.Key - 1;
         while (characterData.Levels.Count <= levelIndex)
         {
             characterData.Levels.Add(new Kh2.Battle.Lvup.PlayableCharacter.Level());
         }
         characterData.Levels[levelIndex] = level.Value;
     }
-    characterData.NumLevels = characterData.Levels.Count;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@OpenKh.Patcher/PatcherProcessor.cs` around lines 811 - 823, Replace the
direct index access to characterMap with a TryGetValue check to validate
character.Key before using it: call characterMap.TryGetValue(character.Key, out
var mapIndex) and if it returns false throw a descriptive exception naming the
invalid character key and the YAML source; use mapIndex - 1 to compute charIndex
and proceed as before. Also remove the redundant assignment to
characterData.NumLevels (since Lvup.WriteSingleCharacter recalculates it), and
keep the loop that ensures Levels has enough entries and assigns level.Value to
characterData.Levels[levelIndex].

Comment on lines +871 to +893
case "sstm":
{
var sstm = Kh2.SystemData.Sstm.Read(stream);

var mod = deserializer.Deserialize<Dictionary<string, object>>(sourceText);

foreach (var entry in mod)
{
var prop = typeof(Kh2.SystemData.Sstm).GetProperty(entry.Key);
if (prop == null)
throw new Exception($"Invalid SSTM field: {entry.Key}");

var converted = Convert.ChangeType(
entry.Value,
Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType
);

prop.SetValue(sstm, converted);
}

sstm.Write(stream);
break;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing stream position reset before write.

Unlike the went case (lines 864-865) which correctly resets the stream with SetLength(0) and Position = 0 before writing, the sstm case writes directly without resetting the stream position. This will append data instead of overwriting, potentially corrupting the output.

Proposed fix
                     prop.SetValue(sstm, converted);
                 }

+                stream.SetLength(0);
+                stream.Position = 0;
                 sstm.Write(stream);
                 break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@OpenKh.Patcher/PatcherProcessor.cs` around lines 871 - 893, The sstm branch
reads and mutates a Kh2.SystemData.Sstm instance but forgets to reset the stream
before writing, so the sstm.Write(stream) will append instead of overwrite;
update the case handling (around the deserializer.Deserialize and sstm variable)
to call stream.SetLength(0) and stream.Position = 0 (same reset used in the went
case) immediately before invoking sstm.Write(stream) to ensure the stream is
truncated and writing starts at the beginning.

Comment on lines +895 to +928
case "prty":
{
var file = Kh2.SystemData.PrtyFile.Read(stream);

var mod = deserializer.Deserialize<Dictionary<string, Dictionary<string, object>>>(sourceText);

foreach (var character in mod)
{
if (!Kh2.SystemData.Prty.CharacterMap.TryGetValue(character.Key, out int index))
throw new Exception($"Invalid PRTY character: {character.Key}");

int realIndex = index + 1;
int offset = file.Offsets[realIndex];

var entry = file.UniqueEntries[offset];

foreach (var field in character.Value)
{
var prop = typeof(Kh2.SystemData.Prty).GetProperty(field.Key);
if (prop == null)
throw new Exception($"Invalid PRTY field: {field.Key}");

var converted = Convert.ChangeType(
field.Value,
Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType
);

prop.SetValue(entry, converted);
}
}

file.Write(stream);
break;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing stream position reset and potential dictionary access issues.

  1. Stream reset missing: Same issue as sstm - file.Write(stream) at line 926 doesn't reset the stream position first.

  2. Unvalidated dictionary access: Lines 907 and 909 access file.Offsets[realIndex] and file.UniqueEntries[offset] without checking if the keys exist, which could throw IndexOutOfRangeException or KeyNotFoundException if the PRTY file structure doesn't match expectations.

Proposed fix
                     if (!Kh2.SystemData.Prty.CharacterMap.TryGetValue(character.Key, out int index))
                         throw new Exception($"Invalid PRTY character: {character.Key}");

                     int realIndex = index + 1;
+                    if (realIndex >= file.Offsets.Count)
+                        throw new Exception($"Character index {realIndex} out of bounds for PRTY offsets");
+
                     int offset = file.Offsets[realIndex];

+                    if (!file.UniqueEntries.TryGetValue(offset, out var entry))
+                        throw new Exception($"No PRTY entry found at offset {offset}");
-                    var entry = file.UniqueEntries[offset];

                     foreach (var field in character.Value)
                     {
                         // ... existing code ...
                     }
                 }

+                stream.SetLength(0);
+                stream.Position = 0;
                 file.Write(stream);
                 break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@OpenKh.Patcher/PatcherProcessor.cs` around lines 895 - 928, The prty case
handler must reset the stream position before writing and validate
dictionary/list accesses to avoid runtime exceptions: after reading and applying
changes to the Kh2.SystemData.PrtyFile (via Kh2.SystemData.PrtyFile.Read and the
deserialized mod), set the stream position (and truncate if necessary) before
calling file.Write(stream); and before using file.Offsets[realIndex] and
file.UniqueEntries[offset] ensure the keys/indices exist (use bounds checks or
ContainsKey/TryGetValue on file.Offsets and file.UniqueEntries) and throw a
clear Exception mentioning the character key or missing offset/entry when
validation fails instead of letting IndexOutOfRange/KeyNotFound propagate; keep
the existing use of Kh2.SystemData.Prty.CharacterMap.TryGetValue to resolve
character -> index.

@shananas shananas merged commit d814753 into OpenKH:master Apr 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants