Did you ever take a look at that project I had that hooked the game loop right where the network packets were exchanged? Basically the last place possible where you could create a command packet and have it sent to other players to be executed. I placed the hook there so there'd be no trouble with overwriting player issued commands if both a player and an AI were controlling the same colony. Heh, AI assisted play. I figured it might make developing an AI easier if I could fill in the gaps meanwhile while parts of the AI were still being developed. I never really got to the AI stuff though. I think the code location was at a deeper nesting that TApp:PlaybackCommand.
That's where I had originally hooked at, right before Network.GetGamePlayPacket was called (in fact using your code you sent me, the fact that I had that code was one of the reasons I was interested in writing this). Basically the first 'real' thing that happens in Dans_RULE_UIFrame.OnIdle (well of course, they call QueryPerformanceCounter to time the game loop).
The only problem with that was the random freeze-ups. I put a trace on several functions (Network.GetGamePlayPacket, TethysGame.ProcessGameCycle, some other places in there as well). What was happening, as soon as network lag occurred, (resent packet or a wait) the game would basically freeze.. my code would continue to be called, Network.GetGamePlayPacket would continue to be called, but the rest of the stuff like TethysGame.ProcessGameCycle would get skipped. The same packet would keep getting recorded for thousands of game cycles in the recording file.
This was a nasty problem to track down since it doesn't happen in single player or even when playing multiplayer with two sessions on localhost (or on the LAN for that matter). The lag was a hunch, so I wrote a wsock32.dll 'wrapper' that would allow me to create lag on one session. (It would randomly drop data given to sendto() but still return a success value).
After this I figured I would stay away from the network code and insert my hook in TethysGame.ProcessGameCycle.
--
Hmm, however, now that I think of it, I was modifying the packets inside the game engine. I was setting the timestamp to TethysGame.tick which would obviously cause problems if a packet was resent - it could have a different timestamp on it.
I noticed this after I switched where I was hooking the game loop, since by then I had started to use the .unknown member of the CommandPacket struct to store the player ID. What I figured was happening then, was the network layer was getting confused since it stores the net ID there.
Right now, I don't use Player.GetNextCommandPacketAddress at all, I just push the buffer that the game is about to give to TApp.PlaybackCommand as well as the player ID.
The other thing is, if I hooked before the network code as I had before, and stopped modifying the packets (which would probably prevent the game from freezing / blocking for more network data) couldn't I still drop data in the first tick or so? For example, if a player happened to send commands right at the beginning of the game.
Oh, and you shouldn't record and playback every CP the game executes. Remember that some will be auto generated and executed independent of the game cycle. These are not controlled by player issued commands, but rather by DLL or game code. If you saved and played these back, they'd probably be executed twice since the game would autogenerate them again.
I skip recording if it's a ctNop or ctMachineSettings packet. However, this was mostly to keep the file size down (recording either of these in addition to the rest of the commands had a tendency to double or triple the file size).
I figured ctNop = useless, since the game ignores it anyway, ctMachineSettings = useless as well since that's how the machine information is transmitted (for the net stats page, I think) but you can't access the net stats in the single player playback.
The playback system plays back any packet in the file.
As for the corruption, the CommandPackets are of variable size. You might want to use the size field in the header to save them properly. I'm pretty sure I've seen code create unusually large packets. Larger than the stack space typically reserved in functions that create and issue CPs. I'm not sure if it's an issue in network code though. Although, the network buffers do allow for CPs to be up to 512 bytes in length. Who knows, with any luck maybe the game designers remembered to properly set the size field for every CP they create in the game. (Actually, don't worry too much. The network code only copies the number of bytes specified in the size field).
I noticed you had a note saying that command packets have a fixed maximum of 0x74 bytes. Is that necessarily true? It's obviously a lot less than 512 bytes.
Btw, nice work with all the game vitals stuff and getting the correct DLL loaded for playback and all. That actually sounds like quite a hassle.
Thanks
Actually, it wasn't that bad. What I did was load the DLL and store the addresses of the major entry points (InitProc, AIProc, GetSaveRegions, etc..) In InitProc I load the recording file and validate some headers (there's a chunk of text that I do a string compare on so I know that it's a recorded game file). Next I pull out the mission specifics and checksum all the files in the game set (DLL, map, techtree, the spreadsheets in sheets.vol) For the checksum in both recording and playback I just use ResManager.ChecksumStream. If the checksum fails on any of the files I put up a warning and give the user an option to continue.
Next I load the game settings like day and night, etc and put them into the game engine. (I just overwrite the locations where functions such as TethysGame.UsesMorale, etc read from) I also read the random number seed into the random number generator class. (8 bytes here although the seed is only four bytes. The SetSeed function they use does some math on the number and gets 8 bytes. I figured this would be easier than trying to catch the 4 byte seed they use since the seed is set far before InitProc is called {even before the player objects are initialized}).
I then load the player information. Most of these settings are set the same way, I just write into the player classes (or use the exported function to set it if possible so I can avoid messy hacks as much as possible). As for the player name, that's easy enough too. It turns out that there are functions to get and set the player name, which are both part of the player class.
At this point I check that all the entry points are valid for the DLL, and then load the map and techtree. Reloading the map / techtree seems odd but it's actually very straightforward to do. I just simply call Map.LoadMap again with the map name I want to load, same with Research.LoadTechtree.
Next I patch GetProcAddress like I mentioned before so the game will get the correct address of trigger exports, unit block exports etc.
Finally I call the mission InitProc. In the rest of the functions (AIProc etc) I just call the mission AIProc and such.
---
I wonder... maybe Sirbomber had multiple structure kits in the factory's storage (Tokamak and Advanced Lab), and the recorder doesn't record which structure kit is pulled out correctly?
It should pull out the correct kit provided they were in the correct bays. The command that handles putting a kit into the convec (ctMoTransferCargo) takes the unit ID of the factory which holds the kit, and the bay ID. (The vehicle ID isn't needed since when this command is issued the vehicle will be on the dock).
The only thing I can think of, if something finished building sooner or later than it was supposed to. The playback system uses a DescBlock with MultiLastOneStanding for the mission type so the times shouldn't be different. (Things take longer in single player than in multiplayer).
I plan to look through the commands by hand today, and see if there are any anomalies.
(and now, this very long post comes to an end
)