Maple.StringPool 0.2.0

dotnet add package Maple.StringPool --version 0.2.0
                    
NuGet\Install-Package Maple.StringPool -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Maple.StringPool" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Maple.StringPool" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Maple.StringPool" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Maple.StringPool --version 0.2.0
                    
#r "nuget: Maple.StringPool, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Maple.StringPool@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Maple.StringPool&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Maple.StringPool&version=0.2.0
                    
Install as a Cake Tool

Maple.StringPool

.NET C# Build Status codecov Nuget License

Zero-allocation decoder for the MapleStory GMS v95 StringPool singleton. Cross-platform, trimmable and AOT/NativeAOT compatible.

The library also includes allocator-backed mutation helpers for live runtime substitution scenarios where an injected tool needs to replace an existing StringPool slot or seed the live runtime caches with a new ZXString.

⭐ Please star this project if you like it. ⭐

Example | Example Catalogue | CLI Tool (sp) | Public API Reference

Runtime Mutation

The original client does not expose a growable StringPool. In v95, ms_aString is a fixed const char*[6883] table in .data, and ms_nSize is fixed as well. That means adding a new string in practice means reusing an existing slot unless you also patch the client code that assumes the original table shape.

For runtime tooling, the preferred operation is a synchronized whole-slot substitution:

using Maple.Native;
using Maple.StringPool;

INativeRuntimeAllocator allocator = /* injected runtime allocator */;

StringPoolRuntimeSubstitution substitution = StringPoolMutator.SubstituteSlot(
    allocator,
    KnownLayouts.GmsV95,
    stringPoolAddress,
    index: 25,
    value: "Telemetry/Hero",
    masterKey: masterKeyBytes,
    seed: 0);

That path validates the live cache shape against ms_nSize, acquires StringPool::m_lock, and updates the static entry plus both runtime caches as one logical operation. It requires a runtime allocator that can participate in the target thread's lock ownership semantics; a raw out-of-process handle is not enough for this API by itself.

For raw remote memory patching from an attached Windows tool, Maple.Native now also exposes a Windows-only remote-process backend:

using Maple.Memory;
using Maple.Native;
using Maple.Process;
using Maple.StringPool;

using WindowsProcessMemory processMemory = WindowsProcessMemory.Open(processId);
using RemoteProcessAllocator allocator = new(processMemory);

uint entryAddress = StringPoolMutator.ReplaceStaticSlot(
    allocator,
    KnownLayouts.GmsV95,
    index: 25,
    value: "Telemetry/Hero",
    masterKey: masterKeyBytes,
    seed: 0);

That remote allocator is suitable for allocator-backed native object creation and raw pointer replacement. The synchronized SubstituteSlot path still needs an injected INativeRuntimeAllocator implementation.

Lower-level operations still exist when you explicitly want raw pointer writes:

using Maple.Native;
using Maple.StringPool;

INativeAllocator allocator = /* future remote allocator */;

// Replace the encoded entry used by the static table.
uint entryAddress = StringPoolMutator.ReplaceStaticSlot(
    allocator,
    KnownLayouts.GmsV95,
    index: 25,
    value: "Telemetry/Hero",
    masterKey: masterKeyBytes,
    seed: 0);

// Or inject a ready-made runtime cache entry for immediate GetString() use.
uint liveNarrow = StringPoolMutator.SetNarrowCacheSlot(
    allocator,
    stringPoolAddress,
    index: 25,
    value: "Telemetry/Hero");

uint liveWide = StringPoolMutator.SetWideCacheSlot(
    allocator,
    stringPoolAddress,
    index: 25,
    value: "Telemetry/Hero");

The static-slot path updates ms_aString[index]. The raw live-cache path writes directly to m_apZMString[index] and m_apZWString[index] without taking m_lock, so it is mainly for controlled tooling and tests. For live-client replacement, SubstituteSlot is the safer API.

Example

// Demonstration — requires a real MapleStory.exe; skip if not present.
const string exePath = "MapleStory.exe";
if (!File.Exists(exePath))
    return;

using var pool = StringPoolDecoder.Open(exePath);

// Single slot by index (decimal or hex)
string hero = pool.GetString(25); // "Hero"
Console.WriteLine(hero);

// Enumerate all entries
foreach (StringPoolEntry e in pool.GetAll())
    Console.WriteLine(e); // SP[0x19] (25): Hero

// Case-insensitive substring search
foreach (StringPoolEntry e in pool.Find("warrior"))
    Console.WriteLine(e);

// Slice [start, end)
foreach (StringPoolEntry e in pool.GetRange(0, 100))
    Console.WriteLine(e);

For more examples see Example Catalogue.

Benchmarks

Benchmarks.

Detailed Benchmarks

Comparison Benchmarks
TestBench Benchmark Results

Example Catalogue

The following examples are available in ReadMeTest.cs.

Example - Empty

// Demonstration — requires a real MapleStory.exe; skip if not present.
const string exePath = "MapleStory.exe";
if (!File.Exists(exePath))
    return;

using var pool = StringPoolDecoder.Open(exePath);

// Single slot by index (decimal or hex)
string hero = pool.GetString(25); // "Hero"
Console.WriteLine(hero);

// Enumerate all entries
foreach (StringPoolEntry e in pool.GetAll())
    Console.WriteLine(e); // SP[0x19] (25): Hero

// Case-insensitive substring search
foreach (StringPoolEntry e in pool.Find("warrior"))
    Console.WriteLine(e);

// Slice [start, end)
foreach (StringPoolEntry e in pool.GetRange(0, 100))
    Console.WriteLine(e);

CLI Tool (sp)

The sp command-line tool wraps StringPoolDecoder for interactive use by humans and agents.

Installation

Download the latest sp.exe from GitHub Releases or build from source:

dotnet publish src/Maple.StringPool.Cli/Maple.StringPool.Cli.csproj -c Release -r win-x64

Usage

sp <MapleStory.exe> <command> [options]

Commands:
  get   <index>              Decode SP[index] — accepts decimal or 0x hex
  range <start> <end>        Decode slots [start, end)
  find  <term>               Search all slots for <term> (case-insensitive)
  dump                       Dump every slot
  info                       Print metadata: count, key-size, master-key hex

Options (range, find, dump):
  --format text|json|csv     Output format (default: text)
  --filter <term>            Only emit entries containing <term> (range, dump)
  --out <file>               Write output to <file> instead of stdout

Examples

sp MapleStory.exe get 8
sp MapleStory.exe get 0x19
sp MapleStory.exe range 0 100
sp MapleStory.exe range 0 100 --format json --out slice.json
sp MapleStory.exe find Warrior
sp MapleStory.exe find Warrior --format csv
sp MapleStory.exe dump --format csv --out strings.csv
sp MapleStory.exe dump --filter UI/Login
sp MapleStory.exe info

Public API Reference

See docs/PublicApi.md for the full generated public API surface.

Design Notes

  • Zero heap allocations per decodeRotatedKey lives on the stack via [InlineArray(256)]; the XOR body is decrypted into a stackalloc buffer. The only allocation is the final string.
  • Thread safetyGetString uses Interlocked.CompareExchange to publish decoded strings; concurrent callers may decode redundantly but only one result is stored.
  • Cipher — circular left-rotation of the 16-byte master key by the per-entry seed byte, then XOR with zero-collision rule.

Verified Addresses (GMS v95 PDB)

Field Address Value
ms_aString 0xC5A878 const char*[6883] pointer table
ms_aKey 0xB98830 16-byte master XOR key
ms_nKeySize 0xB98840 16
ms_nSize 0xB98844 6883 (0x1AE3)
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 37 4/2/2026
0.1.0 78 3/23/2026