It's gotten to the point where I think I need to actually make a new category for these posts. From now on, there's a tech category for posts that are primarily about tech-y stuff, and tags will be used to separate topics.
Replays
Making good progress on reading replay files. I've made Property representations of a lot of Fields, so now they're properly exported when the replay is serialized. Here's an example of a serialized replay, so far:
{
"IsValid": true,
"Date1": "2025-04-08T18:37:19",
"Date2": "2025-04-08T18:37:19",
"Winner": 0,
"P1": {
"Name": "TheZanderBug",
"SteamID": [...]
},
"P2": {
"Name": "Systemagical",
"SteamID": [...]
},
"Recorder": {
"Name": "Systemagical",
"SteamID": [...]
},
"P1CharID": 25,
"P2CharID": 25,
"Unknownx304": 1,
"Unknownx308": 6,
"P1Level": 33,
"P2Level": 24,
"Unknownx31c": 1,
"RoundsToWin": 2,
"SecondsPerRound": 99,
"StageOrMusic1": 86,
"StageOrMusic2": 52,
"P1Info": {
"Unknown1": 25604,
"CharacterID": 25,
"Unknown2": 0,
"Unknown3": 16
},
"P2Info": {
"Unknown1": 25604,
"CharacterID": 25,
"Unknown2": 1,
"Unknown3": 5
}
}
(If you find this because I put your full IGN: ... Hi Zander!)
I'm still trying to figure out what some of these unknowns are. My running theory is that Unknown3 under P1Info/P2Info is the palette choice (0-indexed), which checks out because I'm using Palette 6 in this replay and Zander was, in fact, using Palette 17. I'm not sure what unknown 1/2 are. Levels are definitely -1 off what they are in game (so again, 0 indexed.)
The good news is, it looks like I have free reign to actually modify some of these values straight in the file without having to update the checksum or the replay list. (My guess is, only the header is validated, but I won't make any concrete statements yet.) I've been able to confirm that Unknown3 is palette, and Unknown2 is weird. Changing it from 0 to 1 causes the entire match to go differently -- it seems like it causes P1's inputs to start before the round starts. Meanwhile, changing P2's from 1 to 0 had no difference on the start of the match. My guess is it has something to do with when inputs start processing, or something along those lines...?
Changing Unknown1 from 25604 to 25600 doesn't seem to have changed anything in the early match either. (I chose 25600 because I saw it in another replay somewhere.)
I haven't serialized it yet, but I've also found round-start information that looks kind of like below:
/* Example data block: (3th int on is consistent between replays 6,7,11 for the first round.)
*
* 00000480 92 79 f3 03 uint 66288018
* 00000484 66 dc cc 90 uint 2429344870
* 00000488 d9 11 00 00 uint 4569
* 0000048c 01 00 00 00 uint 1
* 00000490 01 00 00 00 uint 1 # p1 has burst
* 00000494 00 00 00 00 uint 0
* 00000498 01 00 00 00 uint 1 # p2 has burst
* 0000049c 00 00 00 00 uint 0
* 000004a0 a0 86 01 00 uint 100000
* burst meter?
* 000004a4 a0 86 01 00 uint 100000
* burst meter p2?
*/
While mucking about with the binary, I found that the 2nd uint represents the intro animation (despite the fact this is only shown in the first round.) Replacing it causes a different intro animation to play, which has a different length of time before the players can act, which causes inputs to desynchronize. (In my example case, the intro animation was longer, and therefore a bunch of inputs got ingored.) The third number appears to be related to which inputs to process, though it doesn't seem like it's just the offset to start reading from. (Otherwise, replacing the 2nd round's value with the 1st round's should cause it to replay the first round, but that's not the case.)
It seems like intro/outro skipping is based off of user inputs, so that's... frustrating. I'll need to look into how to dummy those inputs cleanly if I want to add the ability to add skips to a replay.
It also looks like my initial assumption that the replay list is only loaded on game start is true, which is rough. Either I'll need to figure out how to force the game to reload the replay list, or figure out how to modify the values in memory, .. or just make some UI that users reference instead of the in game replay list. I don't like any of these three options, but we'll see what happens. The third is probably the most straightforward since it doesn't require any injection or anything. Or I can just make users restart the game. (shrug emoji)
How-To: Command Line Arguments in C
Again, I'm not used to coding in C# so I'm having to re-learn a lot of things that were "easy" for me to do in python. An example is actually writing a CLI. For example, in my Replay code, I have a CLI that currently has two major modes of use:
- Translate into JSON so I can see what differences are between different replays.
- Modify a replay file with some adjustment.
Part 2 might need a UI, but for now I'm going to hard-code it. Eventually I'll make some complex opts where you can define what values to change, but for now I am going to hard hard code it where every time you run it it does a specific adjustment to a specific part of the replay.
First off, I learned that the different "actions" a CLI can do are called "verbs", which was way more accurate than I expected.
Second off, I was provided this example syntax on how to do it:
public class GlobalOptions
{
[Option('v', "verbose", Default = false, HelpText = "Output should be more verbose")]
public bool Verbose { get; set; }
}
[Verb("json")]
public class JsonOptions : GlobalOptions
{
}
[Verb("edit")]
public class EditOptions : GlobalOptions
{
}
public static int Main(string[] args)
{
// Allow the console to handle ANSI escape sequences.
VirtualTerminal.EnableAnsi();
var exitCode = Parser.Default.ParseArguments<EditOptions, JsonOptions>(args)
.MapResult(
(EditOptions opts) => RunEdit(opts),
(JsonOptions opts) => RunJson(opts),
HandleErrors);
return exitCode;
}
private static int RunEdit(EditOptions options)
{
return 0;
}
private static int RunJson(JsonOptions options)
{
return 0;
}
private static int HandleErrors(IEnumerable<Error> errors)
{
return 1;
}
Don't mind the EnableAnsi bit. I'll need to figure if I actually want to use that right now, because it seems to add a tiny bit of complexity. So this is neat.
This is where I admit that I made this blog post entirely just because I couldn't remember how to do this, and realized it was getting progressively more difficult to find this to reference it the more I talked with my friend about my project. So... here it is. I'm going back to actually implementing this now.