Warhammer Dark Omen Forum

Modifications => Campaigns => Topic started by: aqrit on November 29, 2013, 08:41:02 AM

Title: Cut-Scene Modding
Post by: aqrit on November 29, 2013, 08:41:02 AM
What is the current state of cut-scene modding?

Is the wiki up to date?
http://wiki.dark-omen.org/do/DO/WHMTG (http://wiki.dark-omen.org/do/DO/WHMTG)

Has anyone given thought to externalizing the .MTG file?

Title: Re: Cut-Scene Modding
Post by: Ghabry on November 29, 2013, 01:58:02 PM
I had some thoughts about this. But havnt started working on this yet ;)

My idea was to read some script.whmtg file using Mod Selector.
The file structure should look like this http://pastebin.com/9mf0NfwD (http://pastebin.com/9mf0NfwD) without the line count at the beginning.
That file is assembled on mod loading and then written in WHMTG-binary format to memory (the memory location must be the same everytime because savegames reference it, e.g. using VirtualAllocEx). And then the hardcoded stuff pointing to WHMTG (e.g. New Campaign, Tutorial, Jump to Chapter x cheat) should be repointed to the new one.

Title: Re: Cut-Scene Modding
Post by: aqrit on December 01, 2013, 08:51:54 AM
Looks pretty much all figured out.

I doubt that the save file would have an absolute address store in it.
my guess is that "current position" in the script would be relative to "script start"
so I think it will not be necessary to load at the same memory address every time.

since pastebin has apparently banned my IP address from posting...
here is what things look like to me at this point in time.

004C3C48 // start of script ( 4C3C20 thru 4C3C244 == dead code? )
004CCD28 // end of script
004C3D48 // tutorial
004C3D68 // new campaign
004C3DA8 // Start game at chapter 2
004C3E40 // Start game at chapter 3
004C3F28 // Start game at chapter 4
004C4060 // Start game at chapter 5

//VA func, name, argc, args w/fixups
0x0041DB60, "WH_GOTO", 1, { 1 } // label
0x0041DB80, "WH_IF", 2, { }
0x0041DC30, "WH_ENDIF", 0, { }
0x0041DC60, "WH_ELSE", 0, { }
0x0041DC80, "WH_GOSUB", 1, { 1 } // label
0x0041DCD0, "WH_RETURN", 0, { }
0x0041DDF0, "WH_PUSHLV", 0, { }
0x0041DE10, "WH_POPLV", 0, { }
0x0041DD00, "WH_REPEAT", 0, { }
0x0041DD20, "WH_UNTIL", 2, { }
0x0041DED0, "WH_END", 0, { }
0x0041C920, "WH_PlayMovie", 1, { 1 } // ".tgq"
0x0041C970, "WH_MeetingPoint", 2, { 1, 2 } // ".bmp", label
0x0041CA00, "WH_TravelMap", 10, { 1, 2, 3 } // ".bmp", ".spr", ".dot"
0x0041CB10, "WH_Deploy", 0, { }
0x0041CB20, "WH_Battle", 2, { 1 } // "B1_01"
0x0041CDC0, "WH_GetUnitStatus", 1, { }
0x0041CE20, "WH_GetUnitHireStatus", 1, { }
0x0041CE80, "WH_AddUnit", 1, { }
0x0041CED0, "WH_RemoveUnit", 1, { }
0x0041DEE0, "WH_SetVariable", 2, { 1 } // var
0x0041DF00, "WH_ReadVariable", 1, { 1 } // var
0x0041D620, "WH_AddCash", 1, { }
0x0041D970, "WH_GameOver", 0, { }
0x0041DF20, "WH_ClearVariables", 2, { 1 } // var
0x0041CFB0, "WH_ForceUnit", 1, { }
0x0041CFF0, "WH_UnForceUnit", 1, { }
0x0041D040, "WH_ExcludeUnit", 1, { }
0x0041D080, "WH_IncludeUnit", 1, { }
0x0041D0D0, "WH_TemporyUnitSet", 1, { }
0x0041D120, "WH_TemporyUnitClear", 1, { }
0x0041D170, "WH_UnitIsGoingSet", 1, { }
0x0041D1C0, "WH_UnitIsGoingClear", 1, { }
0x0041CAB0, "WH_Book", 0, { }
0x0041C9D0, "WH_MeetingWait", 0, { }
0x0041CB40, "WH_InitDebrief", 2, { }
0x0041CB70, "WH_Debrief", 0, { }
0x0041CAE0, "WH_SaveGame", 0, { }
0x0041CC00, "WH_Delay", 1, { }
0x0041CCE0, "WH_Pause", 1, { }
0x0041CBE0, "WH_SetDeafultSaveName", 1, { 1 } // "text"
0x0041D640, "WH_AddMagic", 1, { }
0x0041D870, "WHMTG_DisplayBitmap", 3, { 1 } // ".bmp"
0x0041D8A0, "WHMTG_RemoveBitmap", 0, { }
0x0041D210, "WH_CheckObjective", 1, { }
0x0041D550, "WHMTG_Voice", 2, { 1 } // "VC001"
0x0041D8B0, "WHMTG_SpotAnim", 4, { }
0x0041D950, "WHMTG_ChooseInit", 0, { }
0x0041D740, "WHMTG_PlaySFX", 6, { 1, 2, 5 } // "stay", "continue", "H_KZ001"
0x0041D720, "WHMTG_StopSFX", 1, { }
0x0041D980, "WHMTG_StopAllSFX", 2, { }
0x0041D9A0, "WHMTG_PlayMusic", 1, { }
0x0041D9C0, "WHMTG_SetMusic", 1, { 1 } // ".fsm"
0x0041D9E0, "WHMTG_SetBackground", 0, { }
0x0041D2A0, "WHMTG_Speak", * // var arg???
0x0041D360, "WHMTG_SpeakNoWait", 1, { }
0x0041D420, "WHMTG_Narrate", 5, { 3 } // ".mad"
0x0041D410, "WHMTG_Wait", 1, { }

// not used ( guessed at args # )
0x0041DE30, "WH_DO", 1, { unknown }
0x0041DE20, "WH_SETLV", 1, { unknown }
0x0041DE60, "WH_LOOP", 0, {}
0x0041DEC0, "WH_BREAK", 0, {}
0x0041C910, "WH_Test", 1, { unknown }
0x0041D9B0, "WHMTG_StopMusic", 0, {}
0x0041D280, "WHMTG_AddBitmap", 1, { unknown }
0x0041D250, "WH_SetObjective", 2, { unknown }
0x0041D690, "WH_RemoveMagic", 1, { unknown }
0x0041CDA0, "WH_ShowMouse", 1, { unknown }
0x0041CD90, "WH_HideMouse", 0, {}
0x0041CBA0, "WH_Picture", 1, { unknown }

// not implemented
0x0041DA00, "WH_Narration"
0x0041DA10, "WH_WriteTextToFile"
0x0041DA20, "WH_SetUnitVar"
0x0041DA30, "WH_ReadUnitVar"
0x0041DA40, "WH_DisableAutosave"
0x0041DA50, "WHMTG_StartAnimAsync"
0x0041DA60, "WHMTG_StopAnim"
0x0041DA70, "WHMTG_PlayAnim"
0x0041DA80, "WHMTG_LoadHeads"
0x0041DA90, "WHMTG_ShowHead"
0x0041DAA0, "WHMTG_AddOption"
0x0041DAB0, "WHMTG_ChooseOption"
0x0041DAC0, "WHMTG_HideHead"
0x0041D9F0, "WHMTG_PlaySample"
0x0041DAD0, "WHMTG_PlaySampleNoWait"
0x0041DAE0, "WHMTG_LoadDots"
0x0041DB10, "WHMTG_PlayDots"
0x0041DB20, "WHMTG_WaitForDots"
0x0041DB30, "WHMTG_FinishDots"
0x0041DB40, "WHMTG_ContinuePrompt"
0x0041DAF0, "WHMTG_SetResult"
0x0041DB00, "WHMTG_RemoveBitmap"
0x0041DB50, "WHMTG_ResetDotList"


int WHMTG_Narrate(         
   DWORD head_id
   DWORD head_animiation_id
   CHAR* psz_mad_name
   DWORD unused // always 0 in script and ignored in func
   DWORD keep_highlight

Title: Re: Cut-Scene Modding
Post by: Ghabry on December 01, 2013, 01:36:57 PM
Most of them is already at http://wiki.dark-omen.org/do/DO/WHMTG/OpCodes (http://wiki.dark-omen.org/do/DO/WHMTG/OpCodes)

The WHMTG struct starts in the savegame header at 0xBC and has a size of 0xDC
Some parts of the struct can be easily figured out by looking at a debug print function at 0x40ED80. Any idea what LV means?

The program counter is at 0x00 of the struct, the script entry point at 0x08. (my data could be wrong)
For my first savegame (saved before border counties mission) the entry point is
0x4C3D90 and PC 0x3A. But by looking at other savegames that data is always the same so I lack some information there :/

But you are right, it can't be hardcoded, you can e.g. load English savegames in a German Dark Omen version.

Title: Re: Cut-Scene Modding
Post by: aqrit on December 01, 2013, 06:48:57 PM
I saw that wiki page, I posted this here to show the arg count and arg types for the functions

the LV is "the" location that WH_IF, WH_UNTIL, and WH_ReadVariable, access... (as you know) ... state machine... Local Value maybe?

thanks for the info about the save file.
0xBC + 0x00 is the program counter
0xBC + 0x08 is the start script
0xBC + 0x0C is the end script

however the start script and end script are ignored. ( hex-edited them to 0xFFFFFFF )
so the exe must use its own values ( or possibly difference the two then align 4 and adjust :p )

004C3C48 + 3A * 4 = 004C3D30 ( aka right after the WH_SaveGame ln the english version )

004C3D90 is the script start in the german version?

Title: Re: Cut-Scene Modding
Post by: Ghabry on December 01, 2013, 10:40:13 PM
Okay I had something else for 0x0C, but yours looks more correct.

I compared 10 different savegames and excluding the stack (20 ints starting from 0x1C) they are all the same. (I dont think that the stack is used for loading) So the actual location is stored somewhere else :(

No idea where the script starts in the German version, never checked.

Title: Re: Cut-Scene Modding
Post by: aqrit on December 02, 2013, 08:35:07 AM
I checked, 004C3D90 is start script in the German version.

The save script is a sub-function so the relative return address [program counter]  ( marking where we really are in the campaign ) is on the stack. The stack IS restored from the save file. The script entry points are hardcoded in the executable and need to be hooked.


Title: Re: Cut-Scene Modding
Post by: Ghabry on December 02, 2013, 01:50:25 PM
Can you extend this further? How is the address of that WHMTG "save script" sub-function?

Title: Re: Cut-Scene Modding
Post by: aqrit on December 03, 2013, 12:56:13 AM
I think we lost track of what each other are talking about...

I assume...
two sub-scripts can save the game one is 0x4C3C50 and the other is 0x4C3CF0
in a sav file the program counter will always point to after the call of WH_SaveGame in one of those two functions
when the script resumes at the program counter it will run until it hits the RETURN statement
in which case it pops the return address from the stack and continues from there.
just like a real function call

here is a "compilable" dump of the default script
http://bitpatch.com/downloads/default_WHMGT.7z (http://bitpatch.com/downloads/default_WHMGT.7z)
now all we need is a compiler :p

edit: already found a bug: Voice has two args
though it seems the second one is ignored.

Title: Re: Cut-Scene Modding
Post by: Ghabry on December 03, 2013, 03:21:31 AM
I know how a stack works ;). But thanks for the additional notes, now it makes more sense how the program counter gets the correct value.

The script dump looks great. Have to write an assembler for the mod selector now...

Minor nit-picking:
Can you replace the first argument of IF/UNTIL with !=, ==, <, >?
1: LV == val, 2: LV != val, 3: LV < val, 4: LV > val

And maybe a feature suggestion:
You probably noticed that ClearVariables overwrites 96 bytes at 0x00537FF0. [That is in the savegame header and I called that "Savegame progress struct".
You can view that one in the Wh32Edit header editor (also see savegame header base (http://Savegame_Header_base) source)]

And all SetVariable/ReadVariable calls are (4 byte aligned) in that 96 bytes (SetVariable doesnt check where it writes *uugh*). Maybe use numbers from 0-23 instead of the hex values when a write is in that range. Makes editing the script easier.

Title: Re: Cut-Scene Modding
Post by: aqrit on December 08, 2013, 03:27:59 AM
https://github.com/Ghabry/Dark-Omen-Mod-Selector/pull/1 (https://github.com/Ghabry/Dark-Omen-Mod-Selector/pull/1)

Script Compiler
Completely untested beyond the first mission.

I couldn't build the mod-selector with my C++ compiler,
so I'll leave it to Ghabry to hook it into that.
ImportScript( "whmtg.txt" );

also updated:
http://bitpatch.com/downloads/default_WHMGT.7z (http://bitpatch.com/downloads/default_WHMGT.7z)
with a new script and my quickly-made default script disassembler.

I'm sure some people are less than amused by the way I write C++...
but oh well. 8)

If it is to be used "on-the-fly" then something needs to be changed about the VirtualProtect call in ImportScript()
where it installs its hooks

Title: Re: Cut-Scene Modding
Post by: Ghabry on December 08, 2013, 12:34:47 PM
My program should compile with Visual Studio 2010 (or newer).

I don't care much about code quality in that project, I'm just happy that it works ;)

// so fun fact: OutputDebugString() isn't suitable
// because that gets spammed with complaints of heap corruption by dark omen / the kernel
Yes, the corruption spamming after battle end is great... I wonder why this doesnt crash

The compiler works for me, too. At least in the first mission, not tested further. Great work aqrit.

Title: Re: Cut-Scene Modding
Post by: olly on December 08, 2013, 09:23:33 PM
Amazing Script Compiler, many Thanks Aqrit! It will really open all sorts of possibilities for Dark Omen. So far, I've been shown how to mod the Tutorial map loading sequence, so it opens the Trading Post level instead.

    ForceUnit 1
    Battle "B1_01" 21



Title: Re: Cut-Scene Modding
Post by: Ghabry on December 08, 2013, 09:41:23 PM
Dread King and his friends having a quick chat before attempting to conquer the world.

Title: Re: Cut-Scene Modding
Post by: Jeronimo on December 09, 2013, 04:40:38 PM
Good one Ghabry. :)

Title: Re: Cut-Scene Modding
Post by: Ghabry on December 10, 2013, 01:11:48 AM
How to do a conversation:
Look at the whmtg script and try to alter it ;)

The needed commands are Speak, SpeakNoWait and Narrate

Speak is used for the initialization.
First argument is how many faces you want (4 seems to be the maximum)
Second argument is the background graphic behind the portraits (too lazy to dump the strings)
Now variable length: First_arg * 4 arguments follow
These are always: head_id, unk_id (just use 0, 1, 2 and 3), x-pos, y-pos
No idea how the head map to ids, 0 is Morgan, 1 is Klaus, 59 is Dread King. You can use the <head> section in Wh32Edit database.xml as a reference, but the id mapping is not correct.


Speak 4 2 59 0 176 78 60 1 346 122 61 2 28 204 62 3 496 182

SpeakNoWait is second initialisation step
Call this for any head_id used in Speak.


SpeakNoWait 59
SpeakNoWait 60
SpeakNoWait 61
SpeakNoWait 5062

When you add 5000 to the argument a injured head is rendered (used for Volkmar after consulting the libar mortis or how that was called). An injured head is rendered, too, when the unit with that head is in the party and is heavily injured (you probably noticed this feature already).

Now the talking begins.

First arg is the head_id.
Second arg controls what kind of animation is played by the head (like look right/left, move forward/back)
Third arg is the file to play.
Forth arg is unused.
Fifth can be 1 or 0, when 1 the head is not disabled after finished talking. Use this when the next Narrate is done by the same head.


Narrate 59 2 "KF036" 0 0

Title: Re: Cut-Scene Modding
Post by: Ghabry on December 10, 2013, 01:41:50 AM
And that one is funny:
You can detect via WHMTG if the Save or the Unit Book button were clicked.

This way you can add e.g. the following functionality:
When you click the Book a MeetingPoint is spawned, and Klaus says "Morgan! We can't recruit units here, we must be in a city!".

Or for saving:
Klaus: "Morgan! Sleeping in the tavern costs 1000 gold coins for all our regiments. Do you want to take a break?" [Yes (Save the game)] [No]

Ohhhh, so many evilness possible now.