Another question, not related to the topic but related to my current project.
I want to know if there is a way to check for a specific unit's death without doing something like:
void AIProc()
{
if (someUnit.IsLive() == 0)
{
...
}
}
Is there some trigger in Functions.h I've overlooked?
The original question about pausing music still needs to be answered, too.
Also, is there a way to add a delay between doing things without using time triggers?
While I can do it with time triggers, something like this would be much easier:
SCRIPT_API void PlayerDies()
{
TethysGame::AddMessage(-1, -1, "Three...", -1, sndBeep2);
(wait a few ticks)
TethysGame::AddMessage(-1, -1, "Two...", -1, sndBeep2);
(wait a few ticks)
TethysGame::AddMessage(-1, -1, "One...", -1, sndBeep2);
(wait a few ticks)
TethysGame::AddMessage(-1, -1, "Zero!", -1, sndBeep5);
}
Check MusicManager.h from the ForcedExports project.
class MusicManager
{
...
void Pause(); // 0x450BB0
void Unpause(); // 0x450C20
...
};
// Globals
extern MusicManager musicManager; // 0x565390
Probably the simplest way to use this information in your own projects without importing things from elsewhere would be something like this:
// Untested code
void __declspec(naked) Pause()
{
__asm
{
MOV ECX, 0x565390 // Address of MusicManager object
MOV EAX, 0x450BB0 // Address of Pause function
JMP EAX
}
}
That "(wait a few ticks)" idea is *beep* doomed to fail. That trigger callback must return before the game can continue processing and actually advance the game time. If you wanted to dump all the code in one function, then consider using AIProc. The other obvious choice is to have a sequence of TimeTrigger callbacks, maybe reuse the TimeTrigger. Or, you might also keep a global counter (in the struct saved by GetSaveRegions), and print the subsequent message each time it's called. (I would normally suggest a static local variable for this, but that's a bad idea with Outpost 2 level DLL programming).
Export void Callback()
{
scriptGlobal.counter++;
switch (scriptGlobal.counter)
{
case 1:
// etc.
break;
case 2:
// etc.
break;
}
}
Make sure the time trigger is repeatable, so it will keep calling the callback function.
I'm not seeing any exported way to detect a unit's death, which is actually quite annoying. I found that to be one of the serious limitations of the API. If you don't update cached unit references every so often, it's possible they can get stale, and even eventually end up point to a new unit that got placed in the old unit record slot after the old one dies off and a new one is created.
I believe there was an internally trigger, that could potentially be hooked, but I don't have the details, and I suspect you're not looking for something that complicated at the moment.
Edit: Added some comments to the assembly code.
Excellent! It works!
Well, sorta...
There is a delay between when the music should stop/resume and when it actually does. For example:
// Detects unit death
SCRIPT_API void CargoTruckDead()
{
Pause();
(lots code to determine whether to pass to the next player, kill the current player, or restart the turn)
GD.userGP.DoDeath();
}
However, the music doesn't pause until a mark or two after the Guard Post is killed. Why?
In another example, the music doesn't appear to pause; however as removing the unpause code makes it work I'm guessing that the game pauses the music and then immediately unpauses it. Am I doing something wrong, or is this what happens when we mess with OP2 too much?
Edit: Also, it sometimes randomly doesn't feel like unpausing. Other times, it works fine (other than that delay I was talking about).
I've tried it two ways so far:
1) Calling it from a trigger, like so:
// If the current player doesn't set off the bomb, go to the next player.
SCRIPT_API void NextPlayer()
{
GD.curPlayer++;
if (GD.curPlayer == GD.plAI)
{
GD.curPlayer = 0;
}
GD.stopGame = 0;
Unpause();
StartNewTurn();
} // end NextPlayer
2) Setting a boolean value to true:
// Find out the current phase.
SCRIPT_API void DoPhaseCheck()
{
GD.pauseMusic = 0; **I unpause the music here, but I pause it elsewhere.
if (GD.curPhase == 0)
{
DoPhase1();
} // end phase 1 check
else if (GD.curPhase == 1)
{
DoPhase2();
} // end phase 2 check
else
{
DoPhase3();
} // end phase 3 check
GD.stopGame = 0;
StartNewTurn();
} // end phase check
Then, in AIProc:
if (GD.pauseMusic == 1)
{
__asm
{
MOV ECX, 0x565390 // Address of MusicManager object
MOV EAX, 0x450BB0 // Address of Pause function
JMP EAX
}
}
else
{
__asm
{
MOV ECX, 0x565390 // Address of MusicManager object
MOV EAX, 0x450C20 // Address of Unpause function
JMP EAX
}
}
How did you do it?
Edit: I put it up on the SVN, so look there if you need more context.
I only tested the pausing. There was no delay at all.
// Holder for global script variables (for Saved game files)
struct ScriptGlobal
{
Unit scout;
};
ScriptGlobal scriptGlobal;
// /*Un*/tested code :p
void __declspec(naked) Pause()
{
__asm
{
MOV ECX, 0x565390 // Address of MusicManager object
MOV EAX, 0x450BB0 // Address of Pause function
JMP EAX
}
}
Export int InitProc()
{
TethysGame::CreateUnit(scriptGlobal.scout, mapScout, LOCATION(35, 5), Player0, mapNone, 0);
return true; // Level loaded successfully
}
Export void AIProc()
{
if ((scriptGlobal.scout.unitID != 0) && (!scriptGlobal.scout.IsLive()))
{
TethysGame::AddMessage(-1, -1, "Scout destroyed!", -1, 0);
Pause();
}
}
I'll take a peek at the code in the SVN. I'm particularly interested in what triggers you used and how they were setup.
A few problems:
1) You spelled my name wrong in your comments, fool. - FIXED
2) You got rid of my ownage nightmare-inducing comments. - FIXED
3) The music is supposed to pause when any of the vehicles are destroyed (to add to the suspense); it doesn't pause until the bomb countdown starts. I'll see if I can handle this on my own. - FIXED (Not to be rude, but did it ever occur to you that I paused/unpaused the music in certain places for a reason?)
4) About the music pausing/unpausing counting thing, I think we can add a "TimesPaused" counter of some sort, like this: - FIXED
// Functions for pausing and unpausing the in-game music.
void __declspec(naked) PauseMusic()
{
__asm
{
MOV ECX, 0x565390 // Address of MusicManager object
MOV EAX, 0x450BB0 // Address of Pause function
JMP EAX
}
pauseCount++;
}
void __declspec(naked) UnpauseMusic()
{
__asm
{
MOV ECX, 0x565390 // Address of MusicManager object
MOV EAX, 0x450C20 // Address of Unpause function
JMP EAX
}
pauseCount--;
}
// End music-controlling functions.
And then, up above:
BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
if (fdwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hinstDLL);
}
else if(fdwReason == DLL_PROCESS_DETACH)
{
while (pauseCount > 0 ) // If the music is paused at the end of the game, unpause it.
{
__asm
{
MOV ECX, 0x565390 // Address of MusicManager object
MOV EAX, 0x450C20 // Address of Unpause function
JMP EAX
}
pauseCount--;
}
}
return TRUE;
}
Of course, since I suck at stuff like this it probably won't work and I'll need you to make some correction that should have been blindingly obvious in the first place.
Edit: Yep; didn't work.
Edit 2: Fixed! I realized I should have done > -1, rather than > 0.
Guess what time it is everyone! http://forum.outpostuniverse.net/index.php?showtopic=4463 (http://forum.outpostuniverse.net/index.php?showtopic=4463)
Next up: a racing game!
Like I said, I didn't bother to read into your round starting/ending logic too much. There was a lot of code, and it looked somewhat twisted. I just figured the point where everything is created, and where everything dies was a safe point for demonstration purposes. I wasn't trying to finish your level for you.
Hmm, you seem to have missed one of my SVN log comments. That's not a good way to keep track of the paused count.
Consider how that assembly block works. It jumps (JMP) to the target function. Which means it doesn't push a return address (i.e. CALL). When that jumped to function finishes, it executes a return (RET) instruction. That instruction will look at whatever is on the top of the stack, and jump to it (and remove it from the stack). Now, if that assembly block was placed in it's own little function, with no local variables (like the example function I wrote), then the value on the top of the stack will be the return address from that function. But if you place it in a larger function, or try to put code after the jump, guess what. It's not returning there. It's returning to the parent function, and skipping any code beyond that point (or crashing because it read a local variable instead of a return address). I.e., it uses the fact that the "CALL" was the last instruction in a function with an equivalent parameter list, and so it was optimized to a JMP to avoid the extra step while returning.
If you want that to work, then remove those ASM blocks, and just call the function I provided. It's important that it's actually a call.
Attempted simple explaination:
f():
1: ... f()'s code
2: CALL g(1, 2)
3: ... rest of f()'s code
g(int a, int b):
4: ... g()'s code
5: PUSH b
6: PUSH a
7: CALL h(a, b)
8: RET 8
h():
9: ... h()'s code
10: RET 8 // Remomve 2 paraters (8 bytes) from the stack while returning
Stack contents after line X:
2: <2> <1> <Address of 3>
7: <2> <1> <Address of 3> <b=2> <a=1> <Address of 8>
10: <2> <1> <Address of 3>
8: [empty]
Notice the similarity in the first 3 values and the last 3 values on the stack after executing line 7. This duplication can be optimized away, by noting that in g, there is no real code after the function call to h, and the parameter list of the two functions are equal. Instead of duplicating the parameter lists, and then removing them twice, we can just feed through the first set.
f():
1: ... f()'s code
2: CALL g(1, 2)
3: ... rest of f()'s code
g(int a, int b):
4: ... g()'s code
7: JMP h(a, b)
h():
9: ... h()'s code
10: RET 8 // Remomve 2 paraters (8 bytes) from the stack while returning
Stack contents after line X:
2: <2> <1> <Address of 3>
7: <2> <1> <Address of 3>
10: [empty]
Note that this removed the PUSH instructions for the parameter passing, and the RET instruction after the original CALL. The CALL was then replaced by a JMP.
Now, what you've done, is you made the code after the replaced CALL non-empty. But since at line 10, when you try to return from h(), it see's the return address pointing into f(), and will skip completely over any remaining code in g(). This optimization is not safe in that case.
This is almost like appending h onto the end of g, and to make it work, the call, where the two functions are joined, must be the last thing in g.