@Milky:
Meine Meinung: In den früheren Testversionen gab es nicht Abstürze in solchem Ausmaß. Deswegen kann es durchaus möglich sein, dass es an der HookEngine lag/liegt.
Um sicher zu gehen, würde ich die Hooks HOOK_AI_Drop, Hook_EV_DrawWeapon, CloseDiary und ColorItemName erst einmal deaktivieren, da sie nicht grundlegend wichtig für die Mod sind. Ein paar Features funktionieren dann zwar nicht, aber das ist nicht wichtig um die Abstürze zu testen.
REMOVE_SHARP ist wichtig, weil der in der Story (Sumpftempel) gebraucht wird. Das dürfte aber mit ein paar Anpassungen in den Dialogen trotzdem relativ leicht zu deaktivieren sein.
Es stürzt nicht bei hooktest2() ab sondern an anderer Stelle. Weiß nicht genau wo, ich habe nur alles auskommentiert, was mit Hooks zu tun hatte. [..] An sich ist der Absturz aber nicht ganz unlogisch, da ja der Stack geleert wird.
Ja, da hast du wohl recht. In meiner leeren Testwelt wird Hlp_GetInstanceID wohl einfach nicht (so häufig) aus den Skripten aufgerufen, deshalb hatte ich keine Probleme.
Zitat von Lehona
Zur Sicherheit könnte man den kompletten Datenstack sichern? Müsste man aber wohl in Assembly machen. Solange das nicht der Auslöser ist, also den Aufwand erstmal nicht wert (auch performancetechnisch). Könnte maner aber als zusätzliche Funktion in HookEngine anbieten.
Ich denke auch, dass wir erst einmal den Auslöser festmachen sollten. So groß sollte der Aufwandt aber nicht sein. Ich habe das gerade mal grob zusammen geschrieben (ich habe gerade kein IDA hier, deshalb stimmen die Offsets und Adressen nicht ganz). Was Performance angeht, werden halt vor und nach jedem Daedalus-Aufruf 8000 Bytes hin und her kopiert. Ich lasse das mal hier, falls wir darauf noch einmal zurückkommen wollen.
Spoiler:(zum lesen bitte Text markieren)
Code:
; Secure data stack in zCParser::CallFunc before calling the Daedalus function
;
; TODO
; - Find second address to hook "XY", plus the length "Z", where the stack offset is the same as in the first hook
; - Replace "XXX XXX, XXX" with that very instruction mentioned above
; - Find the stack offset "AAA" (or register) that holds the data stack during the second hook
; - Allocate memory for injecting the code and adjust the addresses "HERE1" and "HERE2" in the jumps
; - Decrease all stack offsets by 4 in zCParser::CallFunc in-between the two hooks
; From 0x792A76 jump to:
push 0x2004 ; sizeof(zCPar_DataStack)
call operator new(uint)
push eax ; Secure pointer to the data stack backup on the stack
push ecx ; Backup registers
push edi
push esi
mov edi, eax ; Setup src, dst and counter for movs
mov esi, ecx
mov ecx, 0x801 ; sizeof(zCPar_DataStack)/4
rep
movs
pop esi ; Restore registers
pop edi
pop ecx
call zCPar_DataStack::Clear(void) ; Rewrite what has been overwritten with jump
jmp 0x792A7B - HERE1 - 1 ; Jump back where left off
; Continue with CallFunc...
; Decrease all following stack offsets by 4
; After function call at address XY jump to
push ecx ; Backup registers
push edi
push esi
mov edi, [esp+AAA] ; Setup src, dst and counter for movs
mov esi, [esp+0xC] ; AAA is the offset to the data stack
mov ecx, 0x801 ; sizeof(zCPar_DataStack)/4
rep
movs
mov ecx, esi ; Free memory of data stack backup
call operator free(uint)
pop esi ; Restore registers
pop edi
pop ecx
add esp, 0x4 ; Remove the data stack backup from stack (given it is on top!)
XXX XXX, XXX ; Rewrite what has been overwritten with jump
jmp XY+Z - HERE2 - 1 ; Jump back where left off (XY+Z)
Vorher wären "ein paar Tests" mit der Änderung in HookEngine.d vielleicht gar nicht schlecht, bis jemand eine neue Idee hat, wo das Problem liegen könnte.
Vorher wären "ein paar Tests" mit der Änderung in HookEngine.d vielleicht gar nicht schlecht, bis jemand eine neue Idee hat, wo das Problem liegen könnte.
Ich habe mal eine aktualisierte Version an unseren Tester gegeben.
Es gab bereits einen Absturz beim Laden eines Spielstands: [...]
Der Stacktrace enthält hier Fehlinformationen. Zwei der durchlaufenen Funktionen haben keinen "Namen" und es werden deshalb die nächst-vorherigen Funktionen aufgeführt. oCMsgState::operator delete()+23393 ist in echt in - nennen wir es - Npc_CreateInvItems und zSTRING::Init()+5521 befindet sich in - nennen wir es - zCParser::PopNpc.
Das Problem liegt in einem Aufruf von CreateInvItems, der (über zwei Ecken) aus einer NPC-Instanz aufgerufen wird. So kann man sich das Vorstellen:
Code:
instance XXXXX(Npc_default)
|
V
Daedalus function
|
V
Daedalus function
|
V
CreateInvItems(INVALID, X, X);
Das erste Argument von CreateInvItems ist nicht etwa eine leere, sondern eine ungültige, NPC-Instanz.
Eine leere Instanz (z.B. hero aus PC_Hero) würde nur folgende Warnung erzeugen:
Code:
SCRIPT: Npc_CreateInvItems(): illegal param: "HERO" is NULL. .... <oGameExternal.cpp,#252>
Ähnlich, wenn wir stattdessen ein Item oder eine andere Instanz übergeben.
Hier scheitert der Cast zu einem NPC so grundlegend, weil die übergebene Instanz gar keine Instanz ist. Um die gleiche AV mit exakt dem gleichen Stacktrace zu reproduzieren, muss man nur triggerMe() aus einer NPC-Instanz ausführen:
Hier ist das übergebene Symbol tatsächlich eine NPC-Instanz (der Parser ist zufrieden), die aber gar keine ist (der Cast in zCParser::PopNpc scheitert).
Wonach ihr suchen müsst, ist also eine NPC-Instanz aus der eine Daedalusfunktion aufgerufen wird, die eine zweite Daedalusfunktion aufruft, aus der CreateInvItems aufgerufen wird. Dort solltet ihr das erste Argument überprüfen.
Eventuell könnte man die External hooken und sich den NPC ausgeben lassen. Der letzt-ausgegebene vor dem Crash ist dann der Übeltäter.
Die External würde ich allerdings nicht hooken. Das Spiel würde eher abstürzen, weil der Hook den Stack leeren würde. Damit würden wir keine bracuhbaren Informationen bekommen.
Hat jemand eine einfach-umsetzbare Idee, alle in Frage kommenden Skripte aufzulisten?
Worauf würde es denn hinweisen, wenn dieser Absturz nicht reproduzierbar ist? Sprich, gleicher Spielstand kann sonst geladen werden? Wie würden wir dann weiter nach dem Fehler suchen?
(Der Absturz trat beim Laden eines Spielstands auf. Der Npc war also vorher schon in der Welt, als gespeichert wurde, wurde also zumindest einmal ohne Absturz initialisiert.)
Theoretisch zutreffen würde die Beschreibung auf unsere Funktion, Ambiente-Inventare zu verteilen. Da wird zunächst aus der NPC-Instanz heraus aufgerufen:
Code:
B_CreateAmbientInv (self);
Diese Funktion sieht dann so aus:
Code:
func void B_CreateAmbientInv(var C_Npc slf)
{
var int zufall;
zufall = Hlp_Random (7);
if (slf.guild == GIL_VLK)
{
B_CreateAmbientInv_VLK (zufall);
}
else if (slf.guild == GIL_BAU)
{
B_CreateAmbientInv_BAU (zufall);
}
//....
else
{
B_CreateAmbientInv_BAU (zufall);
};
};
und die dort aufgerufenen Funktionen machen alle mehr oder weniger dasselbe:
Frage: (wie) könnte self zwischen Instanz und dem Funktionsaufruf kaputt gehen?
Zwischen Beginn der Instanz und Aufruf des Ambiente-Inventars passieren meist solche Aufrufe:
Problemkind ist der NPC mit Symbolindex 34167. Falls ihr noch die Gothic.dat habt, mit der die AV aufgetreten ist, könntet ihr mal mit DecDat schauen welcher NPC das ist. Da es nicht reproduzierbar ist, bezweifle ich aber, dass ihr in der NPC-Instanz etwas brauchbares finden werdet. Nachschauen schadet aber nicht.
Unter welchen Umständen bist du denn gestorben (Magie, Nahkampf, Bogen mit Pfeil in der Hand während Tod, war der NPC mit der obigen SymID involviert, usw.) und hast du mit Quickload oder aus dem Menu geladen?
Problemkind ist der NPC mit Symbolindex 34167. Falls ihr noch die Gothic.dat habt, mit der die AV aufgetreten ist, könntet ihr mal mit DecDat schauen welcher NPC das ist. Da es nicht reproduzierbar ist, bezweifle ich aber, dass ihr in der NPC-Instanz etwas brauchbares finden werdet. Nachschauen schadet aber nicht.
Unter welchen Umständen bist du denn gestorben (Magie, Nahkampf, Bogen mit Pfeil in der Hand während Tod, war der NPC mit der obigen SymID involviert, usw.) und hast du mit Quickload oder aus dem Menu geladen?
Ich habe es gerade in DecDat gesucht. 34167 ist aber leider eine Routine, die absichtlich leer gehalten wurde:
Unter welchen Umständen bist du denn gestorben (Magie, Nahkampf, Bogen mit Pfeil in der Hand während Tod, war der NPC mit der obigen SymID involviert, usw.) und hast du mit Quickload oder aus dem Menu geladen?
Ich habe mit einem Wolfsmesser gegen einen Wolf gekämpft und aus dem Menu geladen. Was der NPC "Sontril" damit zu tun haben kann weiss ich nicht der war ja meilenweit entfernt.
Ich habe mir mal die Funktionen angeschaut, die von Sontrils Instanz aus ausgerufen werden.
Meiner Ansicht nach in Frage kommen könnten da lediglich:
1)
Code:
// const int EquipItem_TogglesEquip = 1;
func void Equip_Item (var c_npc slf, var int ItemInst)
{
if (!Npc_HasItems (slf, ItemInst)){
CreateInvItems (slf, ItemInst, 1);
};
if (!Npc_GetInvItem(slf, ItemInst)) {
MEM_AssertFail("Unexpected behaviour in EquipItem.");
return;
};
// if ((item.mainflag == ITEM_KAT_NF) && (Npc_HasReadiedMeleeWeapon(slf)))
// || ((item.mainflag == ITEM_KAT_FF) && (Npc_HasReadiedRangedWeapon(slf))) {
// MEM_Warn ("EquipItem: Caller wants to equip an item while item of the same type is readied. Ignoring request.");
// return;
// };
// if (item.flags & ITEM_ACTIVE)
// && (!EquipItem_TogglesEquip) {
// /* calling EquipItem would unequip the item. */
// MEM_Info ("EquipItem: This item is already equipped. Ignoring request.");
// return;
// };
CALL_PtrParam (MEM_InstToPtr (ItemInst));
CALL__thiscall (MEM_InstToPtr (slf), 7545792); // oCNpc__EquipItem
};
(allerdings ist das hier nur eine Daedalus-Funktion entfernt -- können wir uns hiermit versehentlich den Stack kaputt machen, so dass etwas in der nächsten Funktion nicht klappt?)
2)
B_CreateAmbientInv, darin wird eine Gilden-spezifische Funktion aufgerufen, die dann Items erstellt. Code habe ich in meinem vorherigen Beitrag gezeigt. Passt von der 2-Daedalus-Aufrufe Idee her. Allerdings scheint mir die Funktion nicht unbedingt selbst der Übeltäter zu sein, sofern "self" nicht bereits zuvor kaputt geht.
ein Item erstellt? Aber als External würde das im stacktrace anders aussehen?
---------------------------------
Symbol-id 34167 ist, wie Neconspictor geschrieben hat, die Routine rtn_empty_20002 zu finden. Der String "empty" (nicht case-sensitive) kommt in den Skripten nicht vor, daher gehe ich davon aus, dass diese Routine aus Gothic-Sicht nur eine beliebige (leere) Funktion ist, die nie aufgerufen wird.
8577h = 34167d. Das sollte esp+4 an dieser Adresse sein, also das erste Argument an zCParser::CreateInstance. Dass dort der Symbolindex des NPC steht, habe ich bei mir mit dem Hero ausprobiert und in der getriggerten AV überprüft.
Nur zur Sicherheit die Gegenfrage: Seid ihr sicher, dass es sich wirklich um die (unveränderte, nicht neu-kompilierte) Gothic.dat handelt?
Zitat von Milky-Way
Ich habe mir mal die Funktionen angeschaut, die von Sontrils Instanz aus ausgerufen werden.
Ich kann nur spekulieren (weil ich im Moment kein Gothic hier habe), aber ich könnte mir vorstellen, dass innerhalb/mit oCNpc::EquipItem die aktuelle Instanz (sprich self) überschrieben wird. Das würde aber nicht die Nicht-Reproduzierbarkeit erklären.
Apropos Reproduzierbarkeit: Diese Kette der beteiligten Enginefunktion wird nur dann so aufgerufen, wenn man aus dem laufenden Spiel heraus ein Spielstand lädt.
@Milky: Es wird nicht "Equip_Item" sondern das External "EquipItem" aufgerufen. Auch wird durch Equip_Item nicht der Stack geleert. Das passiert nämlich nur dann, wenn man von der Engine eine Daedalus Funktion aufruft (über zCParser::CallFunc()). Die HookEngine macht dasselbe, wodurch man keine Externals hooken sollte. Umgekehrt, von Daedalus eine Engine-Funktion aufzurufen ist dagegen kein Problem.
Btw. frage ich mich schon länger, warum es "Equip_Item" eigtl. gibt. Prinzipiell macht es nichts anderes wie das External "EquipItem".
Nur zur Sicherheit die Gegenfrage: Seid ihr sicher, dass es sich wirklich um die (unveränderte, nicht neu-kompilierte) Gothic.dat handelt?
Milky hat extra eine neue Demo mit der Veränderung in der HookEngine.d erstellt.
Also ja, es müsste die gleiche sein (sofern alles richtig installiert worden ist). Aber ich kann zur Sicherheit mal noch die vorherige Version überprüfen.
Zitat von mud-freak
Ich kann nur spekulieren (weil ich im Moment kein Gothic hier habe), aber ich könnte mir vorstellen, dass innerhalb/mit oCNpc::EquipItem die aktuelle Instanz (sprich self) überschrieben wird. Das würde aber nicht die Nicht-Reproduzierbarkeit erklären.
Apropos Reproduzierbarkeit: Diese Kette der beteiligten Enginefunktion wird nur dann so aufgerufen, wenn man aus dem laufenden Spiel heraus ein Spielstand lädt.
Aber oCNpc::EquipItem wird doch auch im External aufgerufen. Ich sehe da ehrlich gesagt keinen Kausalzusammenhang.
Es wird nicht "Equip_Item" sondern das External "EquipItem" aufgerufen.
Oh guter Punkt, rudern wir hier lieber wieder zurück.
Zitat von Neconspictor
Auch wird durch Equip_Item nicht der Stack geleert.
Achtung, das Problem dieser AV hier ist nicht ein leerer/korrupter Stack, sondern ein ungültiges self.
Ein bisschen ungünstig, dass wir hier verschiedene Crashs gleichzeitig diskutieren. Dass die beiden Crashes zusammenhängen halte ich aber nicht für ausgeschlossen.
Zitat von Neconspictor
Btw. frage ich mich schon länger, warum es "Equip_Item" eigtl. gibt. Prinzipiell macht es nichts anderes wie das External "EquipItem".
Aber oCNpc::EquipItem wird doch auch im External aufgerufen. Ich sehe da ehrlich gesagt keinen Kausalzusammenhang.
Stimmt, höchstwahrscheinlich ruft die externe Funktion EquipItem intern oCNpc::EquipItem auf. Ich dachte eher an die Benutzung von CALL_PtrParam und CALL__thicall. Aber das scheint ja nun irrelevant, da EquipItem und nicht Equip_Item aufgerufen wird.
Milky hat extra eine neue Demo mit der Veränderung in der HookEngine.d erstellt.
Also ja, es müsste die gleiche sein (sofern alles richtig installiert worden ist). Aber ich kann zur Sicherheit mal noch die vorherige Version überprüfen.
Ich habe es überprüft und da zeigt Symbolindex 34167 auf unseren Sontril. Wenn ich mich also nicht ganz täusche, muss irgendetwas bei der Installation schief gelaufen sein und unser Tester hat mit der alten Version gespielt
@Milky: Ich glaube wir sollten eine Art Versionsanzeige einbauen (über eine LeGo-View vielleicht?).
EDIT:
Zitat von mud-freak
Achtung, das Problem dieser AV hier ist nicht ein leerer/korrupter Stack, sondern ein ungültiges self.
Ein bisschen ungünstig, dass wir hier verschiedene Crashs gleichzeitig diskutieren. Dass die beiden Crashes zusammenhängen halte ich aber nicht für ausgeschlossen.
Das stimmt. Aber "self" muss davor schon ungültig sein. Und das meinte ich mit "Auch wird durch Equip_Item nicht der Stack geleert". Also dass die Funktion an sich "self" nicht überschreibt (hätte ich klarer formulieren sollen...). Oder habe ich da was übersehen?
Vielleicht könnte man noch mit MEM_GetFuncIDByOffset und co. aus der AV herauskitzeln welche Daedalusfunktionen beteiligt waren, aber vielleicht wäre es noch hilfreich zu wissen, ob dieser Crash auch mit der neusten Gothic.dat auftritt.
Vielleicht könnte man noch mit MEM_GetFuncIDByOffset und co. aus der AV herauskitzeln welche Daedalusfunktionen beteiligt waren, aber vielleicht wäre es noch hilfreich zu wissen, ob dieser Crash auch mit der neusten Gothic.dat auftritt.
Selbst mit exakt der Gothic.dat, mit der das Problem aufgetreten ist, kann man auf die oben beschriebene Weise (fast) unmöglich an die Funktionen heran kommen. Denn sobald man neuen Code schreibt und hinzufügt (z.B. MEM_GetFuncIDByOffset), verschiebt sich alles im Codestack und die Offsets aus der AV sind nutzlos.
Ich habe aber gerade mit erstaunen festgestellt, dass Ninja in solchen Fällen super Abhilfe schafft. Ninja fügt ja jeglichen neuen Code am Ende des Codestack dran, womit dann die Funktionsoffsets intakt bleiben.
Obwohl es nun schon ziemlich offensichtlich ist, dass die beiden Daedalusfunktionen B_CreateAmbientInv und B_CreateAmbientInv_XXX sind und wir nichts aus dieser Information gewinnen (der Ursprung des Problems wird wo anders sein), habe ich hier eine Möglichkeit mit der ihr die beiden Funktionsnamen bestätigen könnt - vorausgesetzt ihr habt noch exakt die Gothic.dat mit der die AV auftrat.
Alles was ihr machen müsst, ist diese VDF-Datei nach [Gothic]\Data\ kopieren und das Spiel starten. Dann werden zwei InfoBoxen erscheinen mit den beiden Funktionsnamen.
Wie gesagt, ich halte das für recht zwecklos, aber es würde mich interessieren, ob es klappt.
Selbst mit exakt der Gothic.dat, mit der das Problem aufgetreten ist, kann man auf die oben beschriebene Weise (fast) unmöglich an die Funktionen heran kommen. Denn sobald man neuen Code schreibt und hinzufügt (z.B. MEM_GetFuncIDByOffset), verschiebt sich alles im Codestack und die Offsets aus der AV sind nutzlos.
Ich habe aber gerade mit erstaunen festgestellt, dass Ninja in solchen Fällen super Abhilfe schafft. Ninja fügt ja jeglichen neuen Code am Ende des Codestack dran, womit dann die Funktionsoffsets intakt bleiben.
Obwohl es nun schon ziemlich offensichtlich ist, dass die beiden Daedalusfunktionen B_CreateAmbientInv und B_CreateAmbientInv_XXX sind und wir nichts aus dieser Information gewinnen (der Ursprung des Problems wird wo anders sein), habe ich hier eine Möglichkeit mit der ihr die beiden Funktionsnamen bestätigen könnt - vorausgesetzt ihr habt noch exakt die Gothic.dat mit der die AV auftrat.
Alles was ihr machen müsst, ist diese VDF-Datei nach [Gothic]\Data\ kopieren und das Spiel starten. Dann werden zwei InfoBoxen erscheinen mit den beiden Funktionsnamen.
Wie gesagt, ich halte das für recht zwecklos, aber es würde mich interessieren, ob es klappt.
Die Funktionsnamen sind B_CreateAmbientInv und B_CreateAmbientInv_Bau, also genauso wie du es vermutet hast. Könntest du den Source code zu deinem Ninja-Code posten? Könnte in der Zukunft vielleicht noch nützlich sein