Skip to content

Commit 3124181

Browse files
committed
Create SpriteEdit project
1 parent 3b47520 commit 3124181

4 files changed

Lines changed: 297 additions & 0 deletions

File tree

SpriteEdit/GlobalSuppressions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// This file is used by Code Analysis to maintain SuppressMessage
2+
// attributes that are applied to this project.
3+
// Project-level suppressions either have no target or are given
4+
// a specific target and scoped to a namespace, type, member, etc.
5+
6+
using System.Diagnostics.CodeAnalysis;
7+
8+
[assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "<Pending>", Scope = "namespace", Target = "~N:Z2Randomizer.ValidateRooms")]

SpriteEdit/Program.cs

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
// See https://aka.ms/new-console-template for more information
2+
3+
using Z2Randomizer.RandomizerCore;
4+
5+
string VANILLA_ROM_PATH = @"C:\emu\NES\roms\Zelda2.nes";
6+
string spritesFolder = @"O:\source\Z2Randomizer\RandomizerCore\Sprites";
7+
8+
byte[] vanillaRomData = File.ReadAllBytes(VANILLA_ROM_PATH);
9+
10+
11+
var files = Directory.GetFiles(spritesFolder, "Old*.ips");
12+
foreach (var f in files)
13+
{
14+
RecreateSanitizedPatch(vanillaRomData, f, f);
15+
}
16+
17+
18+
19+
#pragma warning disable CS8321 // Local function is declared but never used
20+
static void SetCredit(ROM rom, string credit, string credit2="")
21+
{
22+
if (credit.Length > 0x1c) { throw new ArgumentException($"credit string too long, {credit.Length}"); }
23+
rom.Put(0x16abb, ROM.StringToZ2Bytes(credit.ToUpper().PadCenter(0x1c)));
24+
if (credit2.Length > 0x1c) { throw new ArgumentException($"credit2 string too long, {credit2.Length}"); }
25+
rom.Put(0x16ad8, ROM.StringToZ2Bytes(credit2.ToUpper().PadCenter(0x1c)));
26+
}
27+
28+
static void FixGoomaArmPit(byte[] vanillaRomData, byte[] romData)
29+
{
30+
var chrAddr = ROM.VanillaChrRomOffs + 0x164e0;
31+
for (int a = chrAddr; a < chrAddr + 64; a++)
32+
{
33+
romData[a] = vanillaRomData[a];
34+
}
35+
}
36+
37+
static void RecreateSanitizedPatch(byte[] vanillaRomData, string filename, string outputFilename)
38+
{
39+
byte[] ipsData = File.ReadAllBytes(filename);
40+
byte[] romData = [.. vanillaRomData];
41+
42+
SpritePatcher.PatchSpriteSanitized(filename, romData, ipsData, false, true, true, false, false);
43+
44+
var rom = new ROM(romData);
45+
//SetCredit(rom, "Sprite by chibiplan");
46+
//SetCredit(rom, "Sprite by Jackimus");
47+
//SetCredit(rom, "Sprite by Irenepunmaster");
48+
//SetCredit(rom, "Sprite by Schmiddty,", "Irenepunmaster and VTSlacker");
49+
//SetCredit(rom, "Sprite by Irenepunmaster", "and VTSlacker");
50+
//SetCredit(rom, "Sprite by VTSlacker");
51+
//SetCredit(rom, "Sprite by Mister Mike");
52+
//SetCredit(rom, "Sprite by Knightcrawler");
53+
//SetCredit(rom, "Sprite by Mister Mike", "and VTSlacker");
54+
//SetCredit(rom, "Sprite by Lord Louie,", "Z-9 Lurker and Mister Mike");
55+
//SetCredit(rom, "Sprite by valence", "From Street Cleaner 3");
56+
SetCredit(rom, "Sprite by Varcal");
57+
58+
//FixGoomaArmPit(vanillaRomData, romData);
59+
60+
byte[] finalPatch = IpsPatcher.CreateIpsPatch(vanillaRomData, romData, false);
61+
62+
File.WriteAllBytes(outputFilename, finalPatch);
63+
}
64+
#pragma warning restore CS8321 // Local function is declared but never used
65+
66+
67+
68+
public static class IpsPatcher
69+
{
70+
// Create an IPS patch byte array from original and modified byte arrays.
71+
// If allowTruncate is true and modified is shorter than original, a 3-byte
72+
// truncate value will be appended after EOF (extension supported by many patchers).
73+
public static byte[] CreateIpsPatch(byte[] original, byte[] modified, bool allowTruncate = true)
74+
{
75+
if (original == null) original = Array.Empty<byte>();
76+
if (modified == null) modified = Array.Empty<byte>();
77+
78+
var outBuf = new MemoryStream();
79+
// Write header "PATCH"
80+
outBuf.Write(new byte[] { (byte)'P', (byte)'A', (byte)'T', (byte)'C', (byte)'H' }, 0, 5);
81+
82+
int maxIndex = Math.Max(original.Length, modified.Length);
83+
84+
int i = 0;
85+
while (i < maxIndex)
86+
{
87+
bool differs;
88+
if (i >= modified.Length)
89+
{
90+
// modified is shorter -> bytes beyond modified are implicitly different
91+
differs = true;
92+
}
93+
else if (i >= original.Length)
94+
{
95+
// original doesn't have this byte -> it's different
96+
differs = true;
97+
}
98+
else
99+
{
100+
differs = original[i] != modified[i];
101+
}
102+
103+
if (!differs)
104+
{
105+
i++;
106+
continue;
107+
}
108+
109+
// Find the end of this differing region in 'modified' terms (j is exclusive)
110+
int j = i;
111+
while (j < modified.Length)
112+
{
113+
bool diffAtJ = (j >= original.Length) || (original[j] != modified[j]);
114+
if (!diffAtJ) break;
115+
j++;
116+
}
117+
118+
// If modified ended (j == modified.Length) but original has extra bytes beyond
119+
// modified, we still process the differing region [i, j) ; truncation is handled separately.
120+
// Now encode the differing region [i, j) as one or more IPS records.
121+
// We'll walk k through [i,j) and for repeated-byte runs use RLE where beneficial.
122+
int k = i;
123+
while (k < j)
124+
{
125+
// Determine run length of same byte in modified starting at k
126+
byte runByte = modified[k];
127+
int runLen = 1;
128+
while (k + runLen < j && modified[k + runLen] == runByte && runLen < 0xFFFF) runLen++;
129+
130+
// Heuristic: use RLE if the run is at least 3 bytes long (saves space often).
131+
// You can adjust threshold as desired.
132+
const int RLE_THRESHOLD = 3;
133+
if (runLen >= RLE_THRESHOLD)
134+
{
135+
// If runLen may be larger than 0xFFFF, we already limited it above; if remaining
136+
// run > 0xFFFF we'll loop and emit additional RLE records.
137+
WriteRleRecord(outBuf, k, runLen, runByte);
138+
k += runLen;
139+
}
140+
else
141+
{
142+
// Emit a raw block. We should accumulate as large a contiguous non-RLE block
143+
// as possible up to 0xFFFF.
144+
int rawStart = k;
145+
int rawLen = 0;
146+
while (k < j && rawLen < 0xFFFF)
147+
{
148+
// if this next byte would start a sufficiently long RLE, break to emit RLE next
149+
if (k + RLE_THRESHOLD <= j)
150+
{
151+
byte candidate = modified[k];
152+
int candidateRun = 1;
153+
while (k + candidateRun < j && modified[k + candidateRun] == candidate && candidateRun < 0xFFFF)
154+
candidateRun++;
155+
if (candidateRun >= RLE_THRESHOLD && rawLen > 0)
156+
break; // let the next loop iteration handle the RLE
157+
}
158+
// consume one byte into the raw block
159+
k++;
160+
rawLen++;
161+
}
162+
163+
// Now write the raw record for [rawStart, rawStart+rawLen)
164+
WriteRawRecord(outBuf, rawStart, modified, rawStart, rawLen);
165+
}
166+
}
167+
168+
// move i past this differing region
169+
i = j;
170+
}
171+
172+
// Write EOF
173+
outBuf.Write(new byte[] { (byte)'E', (byte)'O', (byte)'F' }, 0, 3);
174+
175+
// Optional truncate extension: many IPS patchers support a 3-byte size after EOF
176+
if (allowTruncate && modified.Length < original.Length)
177+
{
178+
Write3ByteBigEndian(outBuf, modified.Length);
179+
}
180+
181+
return outBuf.ToArray();
182+
}
183+
184+
// Write a raw (non-RLE) IPS record:
185+
// [3-byte offset][2-byte size][data...]
186+
private static void WriteRawRecord(Stream s, int offset, byte[] src, int srcIndex, int length)
187+
{
188+
// Split records larger than 0xFFFF
189+
int remaining = length;
190+
int curSrcIdx = srcIndex;
191+
int curOffset = offset;
192+
while (remaining > 0)
193+
{
194+
int chunk = Math.Min(remaining, 0xFFFF);
195+
Write3ByteBigEndian(s, curOffset);
196+
Write2ByteBigEndian(s, chunk);
197+
s.Write(src, curSrcIdx, chunk);
198+
199+
remaining -= chunk;
200+
curSrcIdx += chunk;
201+
curOffset += chunk;
202+
}
203+
}
204+
205+
// Write an RLE record:
206+
// [3-byte offset][2-byte size==0][2-byte rleSize][1-byte value]
207+
private static void WriteRleRecord(Stream s, int offset, int rleLength, byte value)
208+
{
209+
int remaining = rleLength;
210+
int curOffset = offset;
211+
while (remaining > 0)
212+
{
213+
int chunk = Math.Min(remaining, 0xFFFF);
214+
Write3ByteBigEndian(s, curOffset);
215+
// size==0 indicates RLE record
216+
Write2ByteBigEndian(s, 0);
217+
Write2ByteBigEndian(s, chunk);
218+
s.WriteByte(value);
219+
220+
remaining -= chunk;
221+
curOffset += chunk;
222+
}
223+
}
224+
225+
private static void Write3ByteBigEndian(Stream s, int value)
226+
{
227+
if (value < 0 || value > 0xFFFFFF) throw new ArgumentOutOfRangeException(nameof(value));
228+
s.WriteByte((byte)((value >> 16) & 0xFF));
229+
s.WriteByte((byte)((value >> 8) & 0xFF));
230+
s.WriteByte((byte)(value & 0xFF));
231+
}
232+
233+
private static void Write2ByteBigEndian(Stream s, int value)
234+
{
235+
if (value < 0 || value > 0xFFFF) throw new ArgumentOutOfRangeException(nameof(value));
236+
s.WriteByte((byte)((value >> 8) & 0xFF));
237+
s.WriteByte((byte)(value & 0xFF));
238+
}
239+
}
240+
241+
public static class StringExtensions
242+
{
243+
public static string PadCenter(this string text, int width)
244+
{
245+
if (string.IsNullOrEmpty(text) || text.Length >= width)
246+
return text;
247+
248+
int spaces = width - text.Length;
249+
int padLeft = spaces / 2;
250+
251+
return $"{new string(' ', padLeft)}{text}".PadRight(width);
252+
}
253+
}

SpriteEdit/SpriteEdit.csproj

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<Platforms>AnyCPU;x64</Platforms>
8+
<RootNamespace>Z2Randomizer.SpriteEdit</RootNamespace>
9+
<LangVersion>default</LangVersion>
10+
<Configurations>Debug;Release;Unsafe Debug</Configurations>
11+
</PropertyGroup>
12+
13+
<PropertyGroup Condition=" '$(Configuration)' == 'Unsafe Debug' ">
14+
<DebugSymbols Condition=" '$(DebugSymbols)' == '' ">true</DebugSymbols>
15+
<Optimize Condition=" '$(Optimize)' == '' ">false</Optimize>
16+
</PropertyGroup>
17+
18+
<ItemGroup>
19+
<ProjectReference Include="..\RandomizerCore\RandomizerCore.csproj" />
20+
</ItemGroup>
21+
22+
</Project>

Z2Randomizer.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreSourceGenerator", "Core
4444
EndProject
4545
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintRooms", "PrintRooms\PrintRooms.csproj", "{C8742699-E989-142E-1D3A-B0DBA58DC2CF}"
4646
EndProject
47+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpriteEdit", "SpriteEdit\SpriteEdit.csproj", "{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}"
48+
EndProject
4749
Global
4850
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4951
Debug|Any CPU = Debug|Any CPU
@@ -222,6 +224,18 @@ Global
222224
{C8742699-E989-142E-1D3A-B0DBA58DC2CF}.Unsafe Debug|Any CPU.Build.0 = Release|Any CPU
223225
{C8742699-E989-142E-1D3A-B0DBA58DC2CF}.Unsafe Debug|x64.ActiveCfg = Release|x64
224226
{C8742699-E989-142E-1D3A-B0DBA58DC2CF}.Unsafe Debug|x64.Build.0 = Release|x64
227+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
228+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
229+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Debug|x64.ActiveCfg = Debug|x64
230+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Debug|x64.Build.0 = Debug|x64
231+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
232+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Release|Any CPU.Build.0 = Release|Any CPU
233+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Release|x64.ActiveCfg = Release|x64
234+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Release|x64.Build.0 = Release|x64
235+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Unsafe Debug|Any CPU.ActiveCfg = Unsafe Debug|Any CPU
236+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Unsafe Debug|Any CPU.Build.0 = Unsafe Debug|Any CPU
237+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Unsafe Debug|x64.ActiveCfg = Unsafe Debug|x64
238+
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Unsafe Debug|x64.Build.0 = Unsafe Debug|x64
225239
EndGlobalSection
226240
GlobalSection(SolutionProperties) = preSolution
227241
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)