func void B_ValidateOther(){// Original
if (Hlp_IsValidNpc(other)){Npc_GetTarget(self);
self.aivar[AIV_LASTTARGET] = Hlp_GetInstanceID(other);
self.aivar[AIV_LOADGAME] = FALSE;
return;
};
// Else: !Hlp_IsValidNpc(other)
var int symbPtr; symbPtr = MEM_GetSymbolByIndex(self.aivar[AIV_LASTTARGET]);
if (symbPtr){
var zCPar_Symbol symb; symb = _^(symbPtr);
if ((symb.bitfield & zCPar_Symbol_bitfield_type) == zPAR_TYPE_INSTANCE){
symb = _PM_ToClass(self.aivar[AIV_LASTTARGET]);
if (Hlp_StrCmp(symb.name, "C_NPC")){// Original
other = Hlp_GetNpc(self.aivar[AIV_LASTTARGET]);
Npc_SetTarget(self, other);
self.aivar[AIV_LOADGAME] = TRUE;
return;
};
};
};
// Else
// Npc_SetTarget(self, NULL); // ?
self.aivar[AIV_LOADGAME] = TRUE;
};
Da weiß ich aber nicht, was man am besten als Target von Self speichert, wenn etwas in der AI Variable ein ungültiger NPC gespeichert war (vor letzte Zeile). Müsste man mal austesten. Vielleicht etwas wie:
Code:
other = MEM_NullToInst();
Npc_SetTarget(self, other);
Damit stürzt das Spiel immerhin nicht mehr ab. Ich weiß nicht genau, was das Spiel macht, wenn Npc_GetTarget (self); kein brauchbares target findet?
Gibt es Bedenken, dass obiger Code zu umständlich, nicht performant genug sein könnte? Ich weiß nicht, wie aufwändig _PM_ToClass bzw. _^(MEM_ReadIntArray(currSymbolTableAddress, id)); sind, sowie Hlp_StrCmp.
Also ReadIntArray(...) ist super billig. Da Hlp_StrCmp() eine External ist, wird die vermutlich auch nicht allzu teuer sein. _PM_ToClass() verfolgt bloß ein paar Pointer, das kannst du denke ich auch bedenkenlos verwenden. Insgesamt: Wenn es euren Bug/Crash behebt, würde ich es einfach so lassen.
func void B_ValidateOther(){// Original
if (Hlp_IsValidNpc(other)){Npc_GetTarget(self);
self.aivar[AIV_LASTTARGET] = Hlp_GetInstanceID(other);
self.aivar[AIV_LOADGAME] = FALSE;
return;
};
// Else: !Hlp_IsValidNpc(other)
var int symbPtr; symbPtr = MEM_GetSymbolByIndex(self.aivar[AIV_LASTTARGET]);
if (symbPtr){
var zCPar_Symbol symb; symb = _^(symbPtr);
if ((symb.bitfield & zCPar_Symbol_bitfield_type) == zPAR_TYPE_INSTANCE){
symb = _PM_ToClass(self.aivar[AIV_LASTTARGET]);
if (Hlp_StrCmp(symb.name, "C_NPC")){// Original
other = Hlp_GetNpc(self.aivar[AIV_LASTTARGET]);
Npc_SetTarget(self, other);
self.aivar[AIV_LOADGAME] = TRUE;
return;
};
};
};
// Else
// Npc_SetTarget(self, NULL); // ?
self.aivar[AIV_LOADGAME] = TRUE;
};
Da weiß ich aber nicht, was man am besten als Target von Self speichert, wenn etwas in der AI Variable ein ungültiger NPC gespeichert war (vor letzte Zeile). Müsste man mal austesten. Vielleicht etwas wie:
Code:
other = MEM_NullToInst();
Npc_SetTarget(self, other);
Danke, damit stürzt es nun nicht mehr ab. Ob ein null target stört, weiß ich nicht. Da dieses Problem ja aber auch nur nach einem Patch auftritt, ist es mir auch nicht so wichtig, solange das Spiel nicht abstürzt.
Zitat von Lehona
Also ReadIntArray(...) ist super billig. Da Hlp_StrCmp() eine External ist, wird die vermutlich auch nicht allzu teuer sein. _PM_ToClass() verfolgt bloß ein paar Pointer, das kannst du denke ich auch bedenkenlos verwenden. Insgesamt: Wenn es euren Bug/Crash behebt, würde ich es einfach so lassen.
Mir ist dazu noch ein Problem zu dem Code aufgefallen. Kurz nach Starten von Gothic kann es sein, dass das Hilfssymbol yInstance_Help, das den Index null trägt, noch nie befüllt war. Wenn nun ein NPC in der AI-Variable null stehen hat (weil er z.B. noch nie einen Gegner hatte), wird das Parent (also Klasse oder Prototyp) vom Hilfssymbol nachgeschlagen, welches aber null ist, weil es noch nie gesetzt wurde. In dem Fall würde der Aufruf von _PM_ToClass einen Crash verursachen.
Abhilfe sollte stattdessen dieser Code schaffen (ungetestet):
Code:
func void B_ValidateOther() {
// Original
if (Hlp_IsValidNpc(other)) {
Npc_GetTarget(self);
self.aivar[AIV_LASTTARGET] = Hlp_GetInstanceID(other);
self.aivar[AIV_LOADGAME] = FALSE;
return;
};
// Else: !Hlp_IsValidNpc(other)
var int symbPtr; symbPtr = MEM_GetSymbolByIndex(self.aivar[AIV_LASTTARGET]);
if (symbPtr) {
var zCPar_Symbol symb; symb = _^(symbPtr);
if ((symb.bitfield & zCPar_Symbol_bitfield_type) == zPAR_TYPE_INSTANCE) {
symb = _PM_ToClass(self.aivar[AIV_LASTTARGET]);if (Hlp_StrCmp(symb.name, "C_NPC")) {
if (Hlp_Is_oCNpc(symb.offset)) {
// Original
other = Hlp_GetNpc(self.aivar[AIV_LASTTARGET]);
Npc_SetTarget(self, other);
self.aivar[AIV_LOADGAME] = TRUE;
return;
};
};
};
// Else
Npc_SetTarget(self, NULL);
self.aivar[AIV_LOADGAME] = TRUE;
};
Mir ist dazu noch ein Problem zu dem Code aufgefallen. Kurz nach Starten von Gothic kann es sein, dass das Hilfssymbol yInstance_Help, das den Index null trägt, noch nie befüllt war. Wenn nun ein NPC in der AI-Variable null stehen hat (weil er z.B. noch nie einen Gegner hatte), wird das Parent (also Klasse oder Prototyp) vom Hilfssymbol nachgeschlagen, welches aber null ist, weil es noch nie gesetzt wurde. In dem Fall würde der Aufruf von _PM_ToClass einen Crash verursachen.
Abhilfe sollte stattdessen dieser Code schaffen (ungetestet):
Code:
func void B_ValidateOther() {
// Original
if (Hlp_IsValidNpc(other)) {
Npc_GetTarget(self);
self.aivar[AIV_LASTTARGET] = Hlp_GetInstanceID(other);
self.aivar[AIV_LOADGAME] = FALSE;
return;
};
// Else: !Hlp_IsValidNpc(other)
var int symbPtr; symbPtr = MEM_GetSymbolByIndex(self.aivar[AIV_LASTTARGET]);
if (symbPtr) {
var zCPar_Symbol symb; symb = _^(symbPtr);
if ((symb.bitfield & zCPar_Symbol_bitfield_type) == zPAR_TYPE_INSTANCE) {
symb = _PM_ToClass(self.aivar[AIV_LASTTARGET]);if (Hlp_StrCmp(symb.name, "C_NPC")) {
if (Hlp_Is_oCNpc(symb.offset)) {
// Original
other = Hlp_GetNpc(self.aivar[AIV_LASTTARGET]);
Npc_SetTarget(self, other);
self.aivar[AIV_LOADGAME] = TRUE;
return;
};
};
};
// Else
Npc_SetTarget(self, NULL);
self.aivar[AIV_LOADGAME] = TRUE;
};
Auch mit dieser Version stürzt das Spiel nicht mehr ab beim Laden. Das von dir beschriebene Problem der vorherigen Version trat bei mir (noch) nicht auf, daher kann ich nicht testen, ob es in der Situation nun besser funktionieren würde. Aber danke, für deine Arbeit, ich nehme gerne die aktualisierte Version!
Einer unserer Tester hat momentan oefter mal Access Violations beim Laden eines Spielstands (aus dem Spiel heraus).
Die Fehlermeldungen, die dabei produziert wurden, sagen mir erst mal gar nichts (siehe unten) -- gibt es da einen Ansatz, dem Problem auf die Schliche zu kommen?
Es scheint so, als gaebe es zwei unterschiedliche Fehlermeldungen -- ob beide letztlich auf das selbe Problem hinweisen, weiss ich natuerlich noch nicht.
Ueber jegliche Ideen, woran es liegen koennte, wuerde ich mich sehr freuen. Wir verwenden Ikarus + LeGo jeweils auf dem aktuellsten Stand, inklusive noch nicht "veroeffentlichten" Aenderungen, die bereits auf Github zu finden sind. (die erschienen mir alle tatsaechliche Bugfixes zu sein, die mehr Problem beheben als ausloesen sollten)
Falls ich vom Spieler noch andere Infos erfragen sollte, lasst es mich einfach wissen
Einer unserer Tester hat momentan oefter mal Access Violations beim Laden eines Spielstands (aus dem Spiel heraus).
Die Fehlermeldungen, die dabei produziert wurden, sagen mir erst mal gar nichts (siehe unten) -- gibt es da einen Ansatz, dem Problem auf die Schliche zu kommen?
Es scheint so, als gaebe es zwei unterschiedliche Fehlermeldungen -- ob beide letztlich auf das selbe Problem hinweisen, weiss ich natuerlich noch nicht.
Ueber jegliche Ideen, woran es liegen koennte, wuerde ich mich sehr freuen. Wir verwenden Ikarus + LeGo jeweils auf dem aktuellsten Stand, inklusive noch nicht "veroeffentlichten" Aenderungen, die bereits auf Github zu finden sind. (die erschienen mir alle tatsaechliche Bugfixes zu sein, die mehr Problem beheben als ausloesen sollten)
Falls ich vom Spieler noch andere Infos erfragen sollte, lasst es mich einfach wissen
Hello. It looks like an error using forbidden memory. Let the tester try to disable DEP in the OS and see if it helps.
General recommendation for mods with Ikarus + Lego is to disable DEP, SP does not do this, Union always turns it off when installing.
Auch wenn der Stacktrace ähnlich aussieht, wie wenn DEP nicht ausgeschaltet ist, glaube ich nicht dass das die Ursache ist. Sonst sollte der Absturz schon viel früher passieren.
Zitat von Milky-Way
gibt es da einen Ansatz, dem Problem auf die Schliche zu kommen?
Ja, hier ein paar (dabei beziehe ich mich nur auf das ersten AC):
Nachschlagen der Adresse in IDA, an der die AC auftritt
Das ist hier 0079249D (oberste Zeile der AC nach dem "0023:"). Unter normalen Umständen wird neben "Gothic2.exe" noch das Debugsymbol aufgeführt aus dem die Exception geworfen wird. Merkwürdig, dass sie hier nicht steht. Das deutet eigentlich darauf hin, dass es sich nicht um die Reportversion handelt.
Angenommen es ist die Reportversion, dann ist die Adresse 0x79249D in zCParser::DoStack. Das macht es noch seltsamer, denn in dem Fall würde ich einen aussagekräftigeren Stacktrace erwarten (mehr Zeilen mit Ursprung in Daedalus, usw.).
An der Adresse wird die Variable nach einem "zPAR_OP_UN_PLUS" verarbeitet, also +var. Das Poppen der Variable scheitert, der Stack scheint korrupt.
Ob diese Diagnose so korrekt ist, bezweifele ich aber, denn der Stack zu dem Zeitpunkt sieht auch komisch aus, d.h. erster Wert (0x00000051) entspräche einem Codeoffset (vgl. hier) innerhalb von mem_checkversion, in dem kein "zPAR_OP_UN_PLUS" Token vorkommt (angenommen es handelt sich um die Content-Skripte und nicht Menu-Skripte o.Ä. oder der Stack hat sich in zCParser::DoStack schon geändert)
Systematisches Ausklammern von benutzten Patchen/Plugins
Das SystemPack, Ninja, aber vor allem Union(-Plugins) biegen viel in der Engine um. Ich kann mir vorstellen, dass der untypisch unvollständige Stacktrace damit zusammenhängt. Ninja kann ich an der bestimmten Adresse ausschließen und das SystemPack ändert m.W. nach auch nichts im Parser. Wilde Spekulationen, die von Spielern häufig als Absolut genommen werden, will ich hier aber vermeiden und warte desshalb mal auf eine Liste der tatsächlich verwendeten Patchtes/Plugins.
Untersuchen des Logs vom zSpy
Ninja erlaubt das loggen vom zSpy ohne Mod-Starter o.Ä. Voraussetzung dafür ist, dass der zSpy vor Spielstart gestartet ist und Ninja verwendet wird. Nötige Informationen finden sich dazu hier. Da der Absturz scheinbar häufiger vorkommt, ließe er sich evtl. noch einmal mit zSpy vom Tester reproduzieren.
Überprüfen, ob Speicherstand einer älteren Mod-Version
Es kann natürlich immer sein, dass etwas in einem Speicherstand nicht mehr mit der aktuellen Version der Mod übereinstimmt. Es wäre interessant zu wissen, ob der Speicherstand von einer früheren Version der Mod stammt oder mit der selben begonnen wurde.
Vielen Dank fuer die Anregungen -- bezueglich Patches etc. habe ich den Tester mal gefragt.
Ich habe auch mal nach dem genauen Zeitpunkt des Absturzes gefragt -- wuerde es eher auf die Menu.dat hinweisen, wenn der Absturz direkt beim Klick auf den zu ladenen Spielstand kommt?
Es sollte sich definitiv um einen Spielstand handeln, der mit derselben Version erstellt wurde.
Mir faellt gerade noch ein, dass wir folgenden Hook drin haben, weil es Probleme gab beim Laden von Spielstaenden, dass Gothic sich ueber korrupte Items beschwert hat. Die Funktion initSavegameWatcher(); wird einmalig pro Session aufgerufen zum Zeitpunkt zu dem wir auch alle unsere anderen Hooks eintragen. Falls es zu unuebersichtlich ist, kann ich auch den Code etwas mehr sortieren, bzw. rausloeschen, was gar nicht aufgerufen wird. Ich habe erst mal alles drin gelassen, damit ggf. Gedankengaenge ersichtlich werden / was / wieso hier im Code probiert wird.
Code:
//========================================
// [intern] Hook controller
//========================================
func int _HookWithOptionalSkip(var int funcID, // ESP-36
var int _edi, // ESP-32 // Function parameters in order of popad (reverse order of pushad)
var int _esi, // ESP-28
var int _ebp, // ESP-24
var int _esp, // ESP-20
var int _ebx, // ESP-16
var int _edx, // ESP-12
var int _ecx, // ESP-8
var int _eax) { // ESP-4
// Backup use-instance before anything else. Temporary variable for now, because it's done before locals()
var int _instBak_temp; _instBak_temp = MEM_GetUseInstance();
// Local backup for recursive hooks
locals();
var int result;
// Secure register variables locally for recursive hooks
var int eaxBak; eaxBak = EAX;
var int ecxBak; ecxBak = ECX;
var int edxBak; edxBak = EDX;
var int ebxBak; ebxBak = EBX;
var int espBak; espBak = ESP;
var int ebpBak; ebpBak = EBP;
var int esiBak; esiBak = ESI;
var int ediBak; ediBak = EDI;
// Get address of yINSTANCE_HELP by symbol index 0
const int instHlpAddr = 0;
if (!instHlpAddr) {
instHlpAddr = MEM_GetSymbolByIndex(0)+zCParSymbol_offset_offset;
};
// Also secure global instances
var int selfBak; selfBak = _@(self);
var int otherBak; otherBak = _@(other);
var int itemBak; itemBak = _@(item);
var int iHlpBak; iHlpBak = MEM_ReadInt(instHlpAddr);
var int instBak; instBak = _instBak_temp;
// Update register variables
EAX = _eax;
ECX = _ecx;
EDX = _edx;
EBX = _ebx;
ESP = _esp;
EBP = _ebp;
ESI = _esi;
EDI = _edi;
// Check whether Ikarus is initialized for hooks that happen during level change
if (!_@(MEM_Parser)) {
MEM_InitLabels();
MEM_InitGlobalInst();
};
// Do not overwrite the global instances by default
HookOverwriteInstances = FALSE;
// Clear data stack in-between function calls
MEM_Parser.datastack_sptr = 0;
// Call the function
MEM_CallByID(funcID);
result = MEMINT_StackPopInt();
// Restore global instances in between function calls
if (!HookOverwriteInstances) {
MEM_AssignInstSuppressNullWarning = TRUE;
self = _^(selfBak);
other = _^(otherBak);
item = _^(itemBak);
MEM_AssignInstSuppressNullWarning = FALSE;
};
MEM_WriteInt(instHlpAddr, iHlpBak);
MEM_SetUseInstance(instBak);
// Stack registers should be kept read-only in between function calls
ESP = _esp; // Stack pointer is read-only
EBP = _ebp; // Base pointer is read-only
// Update modifiable registers on stack (ESP points to the position before pushad)
MEM_WriteInt(ESP-32, EDI);
MEM_WriteInt(ESP-28, ESI);
MEM_WriteInt(ESP-16, EBX);
MEM_WriteInt(ESP-12, EDX);
MEM_WriteInt(ESP-8, ECX);
MEM_WriteInt(ESP-4, EAX);
// Restore register variables for recursive hooks
EDI = ediBak;
ESI = esiBak;
EBP = ebpBak;
ESP = espBak;
EBX = ebxBak;
EDX = edxBak;
ECX = ecxBak;
EAX = eaxBak;
// Just to be safe: restore again at the very end of the function
MEM_SetUseInstance(instBak);
return result;
};
func void HookWithOptionalSkip_I(var int address, var int oldInstr, var int function) {
var int SymbID; // Symbol index of 'function'
var int ptr; // Pointer to temporary memory of the old instruction
var int relAdr; // Relative address from 'address' to new assembly code
// ----- Safety checks -----
if (oldInstr < 5) {
PrintDebug("HookWithOptionalSkip_I: oldInstr is too small. The minimum required length is 5 bytes.");
return;
};
SymbID = function;
if (SymbID == -1) {
PrintDebug("HookWithOptionalSkip_I: The provided deadalus function was not found.");
return;
};
// ----- Backup old instruction -----
ptr = MEM_Alloc(oldInstr);
MEM_CopyBytes(address, ptr, oldInstr);
// ----- Allocate new stream for assembly code -----
ASM_Open(100 + oldInstr + 6 + 1); // Asm code + oldInstr + retn + 1 49
// ----- Treat possibly protected memory -----
MemoryProtectionOverride(address, oldInstr+3);
// ----- Add jump from engine function to new code -----
relAdr = ASMINT_CurrRun-address-5;
MEM_WriteByte(address + 0, ASMINT_OP_jmp);
MEM_WriteInt (address + 1, relAdr);
// ----- Write new assembly code -----
// Call daedalus hook function
ASM_1(ASMINT_OP_pusha); // ESP -= 32 (8*4)
ASM_1(ASMINT_OP_pushIm);
ASM_4(SymbID);
ASM_1(ASMINT_OP_pushIm);
ASM_4(MEM_GetFuncID(_HookWithOptionalSkip));
ASM_1(ASMINT_OP_pushIm);
ASM_4(parser);
ASM_1(ASMINT_OP_call);
ASM_4(zParser__CallFunc-ASM_Here()-4);
ASM_2(ASMINT_OP_addImToESP);
ASM_1(12); // 3*4: parser, _HookWithOptionalSkip, SymbID
//const int zCParser__GetParameterI = 7997280;//.text:007A0760 ?GetParameter@zCParser@@QAEXAAH@Z proc near
const int zCParser__PopDataValue = 7936176; //.text:007918B0 ; int __thiscall
// parser has to be in ecx
const int MOV_ECX_BASE = 185;// //0xb9
// mov eax, [eax]
const int GET_RETURN_VALUE = 139; //mov eax, [eax] // 8b 00 -> 008b
ASM_2(GET_RETURN_VALUE);
//ASM_1(ASMINT_OP_pushIm);
//ASM_4(parser);
//ASM_1(ASMINT_OP_call);
//ASM_4(zCParser__PopDataValue-ASM_Here()-4);
const int TEST_EAX_ZERO = 49285;//test eax, eax //85 C0 => 0xC085 (important: reversed order!)
ASM_2(TEST_EAX_ZERO);
// skip hooked code if eax is true
const int SKIP_OPCODE = 1908;// 7 bytes to jump: jz 07 // 74 05 => 0x0774 (important: reversed order!)
ASM_2(SKIP_OPCODE);
//ASM_1(oldInstr + 7); // ASMINT_OP_popa, oldInstr, ASMINT_OP_pushIm, address+oldInstr, ASMINT_OP_retn
// if (eax != 0)
ASM_1(ASMINT_OP_popa); // Pop altered registers
// Return to engine function
ASM_1(ASMINT_OP_pushIm);
ASM_4(address + oldInstr);
ASM_1(ASMINT_OP_retn);
// else
ASM_1(ASMINT_OP_popa); // Pop altered registers
// Append original instruction
MEM_CopyBytes(ptr, ASMINT_Cursor, oldInstr);
MEM_Free(ptr);
ASMINT_Cursor += oldInstr;
// Return to engine function
ASM_1(ASMINT_OP_pushIm);
ASM_4(address + oldInstr);
ASM_1(ASMINT_OP_retn);
var int i; i = ASM_Close();
};
func void HookWithOptionalSkip_F(var int address, var int oldInstr, var func function) {
HookWithOptionalSkip_I(address, oldInstr, MEM_GetFuncID(function));
};
class SimpleStack {
var int list;
var int numElements;
};
instance SimpleStack@(SimpleStack) {};
func int SimpleStack_isEmpty(var int stackPtr) {
var SimpleStack stack; stack = _^(stackPtr);
return stack.numElements == 0;
};
func void SimpleStack_Clear(var int stackPtr) {
if (SimpleStack_isEmpty(stackPtr)) {return;};
var SimpleStack stack; stack = _^(stackPtr);
List_Destroy(stack.list);
stack.list = 0;
stack.numElements = 0;
};
func int SimpleStack_Create() {
var int ptr; ptr = create(SimpleStack@);
var SimpleStack stack; stack = _^(ptr);
stack.list = 0;
stack.numElements = 0;
return ptr;
};
func void SimpleStack_Destroy(var int stackPtr) {
SimpleStack_Clear(stackPtr);
MEM_Free(stackPtr);
};
func void SimpleStack_Push(var int stackPtr, var int data) {
var SimpleStack stack; stack = _^(stackPtr);
if (SimpleStack_isEmpty(stackPtr)) {
stack.list = List_Create(data);
} else {
List_AddFront(stack.list, data);
};
stack.numElements += 1;
};
func int SimpleStack_Pop(var int stackPtr) {
if (SimpleStack_isEmpty(stackPtr)) {return 0; };
var SimpleStack stack; stack = _^(stackPtr);
var zCList current; current = _^(stack.list);
var int result; result = current.data;
var int next; next = current.next;
MEM_Free(_@(current));
stack.list = next;
stack.numElements -= 1;
if (SimpleStack_isEmpty(stackPtr)) {
stack.list = 0;
};
return result;
};
func void SavegameWatcher__printVTable(var int vTable) {
const int zCVob_vtbl = 8624508; //.rdata:0083997C ; const zCVob::`vftable'
const int zCObject_vtbl = 8579228; //.rdata:0082E89C ; const zCObject::`vftable'
var string msg;
msg = "SavegameWatcher__printVTable: vTable = ";
if (vTable == oCItem_vtbl) {
msg = ConcatStrings(msg, "oCItem_vtbl");
} else if (vTable == oCNpc_vtbl) {
msg = ConcatStrings(msg, "oCNpc_vtbl");
} else if (vTable == zCVob_vtbl) {
msg = ConcatStrings(msg, "zCVob_vtbl");
} else if (vTable == zCObject_vtbl) {
msg = ConcatStrings(msg, "zCObject_vtbl");
} else {
msg = ConcatStrings(msg, "other/unknown");
};
MEM_Info(msg);
msg = "SavegameWatcher__printVTable: vtable address = ";
msg = ConcatStrings(msg, IntToString(vTable));
MEM_Info(msg);
};
func void SavegameWatcher__logInstanceName(var int instanceID) {
MEM_Info("SavegameWatcher__logInstanceName");
var string msg; msg = "SavegameWatcher__logInstanceName:: log instance id: ";
var string instanceName;
if (instanceID < 0 || instanceID >= MEM_Parser.symtab_table_numInArray) {
MEM_Info("SavegameWatcher__logInstanceName:: INVALID");
instanceName = "INVALID INSTANCE";
} else {
MEM_Info("Before sym");
//var int i; i = MEM_ReadIntArray(currSymbolTableAddress, instanceID);
var zCPar_Symbol sym; sym = MEM_PtrToInst(MEM_ReadIntArray(currSymbolTableAddress, instanceID));
instanceName = sym.name;
MEM_Info("After sym");
};
msg = ConcatStrings(msg, instanceName);
MEM_Info(msg);
};
func void SavegameWatcher__logVob(var int vobPtr) {
if (vobPtr == 0) {
MEM_Warn("SavegameWatcher__logVob:: vob is null");
return;
};
var zCVob vob; vob = _^(vobPtr);
if (vob._vtbl == oCItem_vtbl) {
var oCItem itm; itm = _^(vobPtr);
MEM_Info("SavegameWatcher__logVob:: Log item");
SavegameWatcher__logInstanceName(itm.instanz);
MEM_Info("SavegameWatcher__logVob:: Finished logging item");
} else if (vob._vtbl == oCNpc_vtbl) {
var oCNpc onpc; onpc = _^(vobPtr);
MEM_Info("SavegameWatcher__logVob:: Log npc");
SavegameWatcher__logInstanceName(onpc.instanz);
MEM_Info("SavegameWatcher__logVob:: Finished logging npc");
};
var string msg; msg = "SavegameWatcher__logVob:: ref-Ctr = ";
var string refCtr; refCtr = IntToString(vob._zCObject_refCtr);
msg = ConcatStrings(msg, refCtr);
MEM_Info(msg);
msg = "SavegameWatcher__logVob:: address (decimal): ";
var string address; address = IntToString(vobPtr);
msg = ConcatStrings(msg, address);
MEM_Info(msg);
SavegameWatcher__printVTable(vob._vtbl);
};
/**
* Checks a zCVob if it is valid for the world-vob-list.
* Note: Although zCWorld manages a list of zCVobs, it only stores oCItems and oCNpcs.
* Thus, other zCVobs are treated as invalid.
*/
func int SavegameWatcher__isValidVob(var int vobPtr, var int ignoreHighRefs) {
var zCVob vob; vob = _^(vobPtr);
var int isValid; isValid = 1;
//if (vob._vtbl == oCItem_vtbl
//|| vob._vtbl == oCNpc_vtbl) {
// isValid = 1;
//};
if (vob._zCObject_refCtr < 0) {
isValid = 0;
};
if (vob._zCObject_refCtr >= 900 && !ignoreHighRefs) {
isValid = 0;
};
return isValid;
};
func void SavegameWatcher__voblistOnDestruct() {
if (EDI == 0) {
MEM_Warn("SavegameWatcher__voblistOnDestruct:: A null-pointer is supposed to be deleted!");
return;
};
var zCObject vob; vob = _^(EDI);
if (vob.refCtr < 0) {
MEM_Warn("SavegameWatcher__voblistOnDestruct:: Detected corrupted vob!");
SavegameWatcher__printVTable(vob._vtbl);
};
};
func void SavegameWatcher__voblistOnCheckRef() {
if (EDI == 0) {
MEM_Warn("SavegameWatcher__voblistOnCheckRef:: A null-pointer is supposed to be checked for ref counting!");
return;
};
if (ECX < 0) {
var zCObject vob; vob = _^(EDI);
var string msg;
msg = "SavegameWatcher__voblistOnCheckRef:: refCtr = ";
msg = ConcatStrings(msg, IntToString(ECX)); // Decreased(!) reference count
msg = ConcatStrings(msg, " :: ");
msg = ConcatStrings(msg, vob.objectName);
// Reference counter <= 0: Display message as warning
MEM_Warn(msg);
};
};
func int checkAndCleanup(var int vobList) {
var int vobPtr; vobPtr = 0;
var int listPtr; listPtr = vobList;
//var int corruptedVobsPtr; corruptedVobsPtr = SimpleStack_Create();
var int prevPtr; prevPtr = listPtr;
while(listPtr != 0);
vobPtr = MEM_ReadInt(listPtr + 4);
if (vobPtr != 0) {
if (!SavegameWatcher__isValidVob(vobPtr, true)) {
// log vob to file
SavegameWatcher__logVob(vobPtr);
var zCListSort list;
var zCListSort prev;
// skip data-elem
if (listPtr == vobList) {
list = _^(vobList);
//list.data = 0; // Note: Can produce nullptr read
if (list.next != 0) {
vobList = list.next;
};
} else {
list = _^(listPtr);
prev = _^(prevPtr);
prev.next = list.next;
//list.data = 0; // Note: Can produce nullptr read
};
};
};
prevPtr = listPtr;
listPtr = MEM_ReadInt(listPtr + 8);
end;
return vobList;
};
func void SavegameWatcher__checkVobList() {
MEM_Info("SavegameWatcher__checkVobList:: Check voblist");
MEM_World.voblist = checkAndCleanup(MEM_World.voblist);
MEM_World.voblist_items = checkAndCleanup(MEM_World.voblist_items);
MEM_World.voblist_npcs = checkAndCleanup(MEM_World.voblist_npcs);
MEM_Info("SavegameWatcher__checkVobList:: Finished");
};
//.text:00781480 ; public: void __thiscall zCListSort<class zCVob>::InsertSort(class zCVob *)
func void SavegameWatcher__zCListZCVobInsertSortHook() {
var int vobPtr; vobPtr = MEM_ReadInt(ESP + 4);
if (!SavegameWatcher__isValidVob(vobPtr, true)) {
//AI_PrintScreen ("Detected corrupted vob to be inserted!", -1, YPOS_Global+20, FONT_Screen, 15);
MEM_Warn("SavegameWatcher__zCListZCVobInsertSortHook:: Detected corrupted vob to be inserted!");
SavegameWatcher__logVob(vobPtr);
};
};
/**
* @return true if the replaced code for the jump instruction should be skipped;
* false otherwise
*/
func int oCWorldDisposeVobsIterVobListDestructReplaceHook() {
// edi contains the vob to be destructed
if (EDI == 0) {
MEM_Warn("SavegameWatcher__voblistOnDestruct:: A null-pointer is supposed to be deleted!");
return true;
};
var zCObject vob; vob = _^(EDI);
if (vob.refCtr < 0) {
MEM_Warn("SavegameWatcher__voblistOnDestruct:: Detected corrupted vob!");
SavegameWatcher__printVTable(vob._vtbl);
MEM_AssignInstNull(vob);
return true;
};
return false;
};
func void initSavegameWatcher() {
//MEM_InitAll();
const int oCWorld__DisposeVobs_iterVoblist_checkRef = 7867681; //0x780D21
const int oCWorld__DisposeVobs_iterVoblist_destruct = 7867688; //0x780D28
const int oCWorld__DisposeVobs = 7867344; //.text:00780BD0 ?DisposeVobs@oCWorld@@UAEXXZ proc near ; CODE XREF: oCWorld::~oCWorld(void)+35↑p
const int zCListSort__InsertSort = 7869568; //.text:00781480 ; public: void __thiscall zCListSort<class zCVob>::InsertSort(class zCVob *)
//HookEngineF(oCWorld__DisposeVobs_iterVoblist_checkRef, 5, SavegameWatcher__voblistOnCheckRef);
//HookEngineF(oCWorld__DisposeVobs_iterVoblist_destruct, 6, SavegameWatcher__voblistOnDestruct);
//HookEngineF(oCWorld__DisposeVobs, 10, SavegameWatcher__checkVobList);
//HookEngineF(zCListSort__InsertSort, 5, SavegameWatcher__zCListZCVobInsertSortHook);
const int oCWorld__DisposeVobs_iterVoblist_destruct_replace = 7867688; //00780D28
HookWithOptionalSkip_F(oCWorld__DisposeVobs_iterVoblist_destruct_replace, 9, oCWorldDisposeVobsIterVobListDestructReplaceHook);
};
Danke für den Code. Ein zSpy Log wäre tatsächlich im Moment notwendig, um mehr zum Ursprung sagen zu können. Da der Absturz, wenn ich das richtig verstehe, unmittelbar zu Beginn des Ladevorgangs auftritt, ist ein Zusammenhang mit dem oCWorld::DisposeVobs-Hook nicht unwahrscheinlich. Aus dem Skript heraus konnte ich da aber so ohne Weiteres nicht erkennen, wo es zu einem Problem kommen könnte, weil das doch recht sehr verkapselt ist.
Beiläufig könntet ihr mit dieser etwas "leichteren" (und performanteren) Lösung etwas aufräumen, wenn ihr auf die Debugausgaben verzichten könnt. Vielleicht behebt es das Problem schon. Mit dem Skript unten (nur mäßig getestet) fällt im Grunde der gesamte gepostete Code weg. Anstatt den Referenzzähler zu überprüfen, wird hier ein Check eingeschoben der prüft, ob die Adresse der virtuellen Tabelle sich in einem sinnvollen Bereich befindet, um zu bestimmen ob es sich um einen gültigen zCVob-Pointer handelt. Andere, besserdefinierte Ansätze (z.B. über __RTDynamicCast, um Objektklassen/-vererbungen festzustellen) verkraften ungültige Pointer nicht.
Spoiler:(zum lesen bitte Text markieren)
Code:
func void hookDisposeVobs(){// In assembly for faster execution
const int oCWorld__DisposeVobs_destroyVob = 7867690; //0x780D2A
// Only write it once
if (MEM_ReadByte(oCWorld__DisposeVobs_destroyVob) == /*6A*/106){// Overwrite old code (modified from LeGo_HookEngine)
ASM_Open(/*new*/14 + /*old*/7 + /*ret*/6);
var int relAdr; relAdr = ASMINT_CurrRun-oCWorld__DisposeVobs_destroyVob-5;
MemoryProtectionOverride(oCWorld__DisposeVobs_destroyVob, 5);
MEM_WriteByte(oCWorld__DisposeVobs_destroyVob+0, ASMINT_OP_jmp);
MEM_WriteInt( oCWorld__DisposeVobs_destroyVob+1, relAdr);
// Check if address lies between smallest and largest known vtbl addresses
ASM_1(/*3D*/ 61); ASM_4(/*0x82E6F0*/8578800); //cmp eax, zSTRING__vftable
ASM_1(/*7C*/124); ASM_1(14); //jl .done
ASM_1(/*3D*/ 61); ASM_4(/*0x843DB4*/8666548); //cmp eax, exception__vftable
ASM_1(/*7F*/127); ASM_1(7); //jg .done
// Rewrite what has been overwritten
ASM_1(/*6A*/106); ASM_1(/*01*/ 1); //push 0x1
ASM_1(/*8B*/139); ASM_1(/*CF*/207); //mov ecx, edi
ASM_1(/*FF*/255); ASM_1(/*50*/ 80); ASM_1(/*0C*/ 12); //call DWORD PTR [eax+0xC]
// .done:
ASM_1(/*68*/104); ASM_4(oCWorld__DisposeVobs_destroyVob+7); //push address
var int i; i = ASM_Close(); //ret
};
};
Ist das Überprüfen der Vobliste aktuell denn noch notwending? Wenn ja, wäre es anstatt dessen vielleicht mal an der Zeit dem tatsächlichen Ursprung der ungültigen Vobs in der Vobliste auf die Schliche zu kommen als nur Symptome zu bekämpfen (die, wie möglicherweise hier, noch mehr Probleme erzeugen).
Bevor wir hier irgendetwas überstürzen wäre der zSpy log sinnvoll, gerade weil der Stacktrace der AV so unvollständig ist.
Ist das Überprüfen der Vobliste aktuell denn noch notwending? Wenn ja, wäre es anstatt dessen vielleicht mal an der Zeit dem tatsächlichen Ursprung der ungültigen Vobs in der Vobliste auf die Schliche zu kommen als nur Symptome zu bekämpfen (die, wie möglicherweise hier, noch mehr Probleme erzeugen).
Diese unschöne Lösung haben wir eingesetzt, weil wir dem eigentlichen Problem nicht auf die Schliche gekommen sind. Müsste man also durch reines Testen ausprobieren, ob's jetzt auch ohne geht. Problem ist, dass damals die Abstürze sehr unregelmäßig und manchmal erst nach Stunden aufgetreten sind. Da ist es extrem schwer, das eigentliche Problem zu lokalisieren.
Danke für den Code. Ein zSpy Log wäre tatsächlich im Moment notwendig, um mehr zum Ursprung sagen zu können. Da der Absturz, wenn ich das richtig verstehe, unmittelbar zu Beginn des Ladevorgangs auftritt, ist ein Zusammenhang mit dem oCWorld::DisposeVobs-Hook nicht unwahrscheinlich. Aus dem Skript heraus konnte ich da aber so ohne Weiteres nicht erkennen, wo es zu einem Problem kommen könnte, weil das doch recht sehr verkapselt ist.
Aktuell tritt der Fehler (leider?) nicht mehr auf beim Spieler, daher kann es noch etwas dauern, bis wir da mal ein zSpy-log zu bekommen. Eventuell deutet das daraufhin, dass der Fehler mit etwas zusammenhängt, was es vor allem zu Beginn der Mod gibt, aber das ist letztlich nur wilde Spekulation.
Die Funktion hookDisposeVobs würde ich im nächsten Durchlauf ausprobieren lassen.
Hello. Nobody figured out why a dat file sometimes cannot be unpacked via DecDat? It displays an error like this
Spoiler:(zum lesen bitte Text markieren)
[23:08:39] Inf: export symbol dia_cassia_magicbook_justint(:69009)[23:08:39] Inf: export symbol dia_cassia_magicbook_kill(:69010)
[23:08:39] Inf: export symbol dia_cassia_magicbook_stolen(:69011)
[23:08:39] Inf: export symbol dia_cassia_magicbook_rumors(:69012)
[23:08:39] Inf: export symbol dia_cassia_richstones(:69013)
[23:08:39] Inf: export symbol dia_cassia_richstones_condition(:69014)
[23:08:39] ERR: unhandled exception occured: java.lang.ArrayIndexOutOfBoundsException: -1
java.lang.ArrayIndexOutOfBoundsException: -1
at de.lego.gottfried.decdat.decompiler.Function.decompileParameter(Function.java:71 )
at de.lego.gottfried.decdat.decompiler.Function.decompileOperation(Function.java:14 1)
at de.lego.gottfried.decdat.decompiler.Function.decompile(Function.java:239)
at de.lego.gottfried.decdat.decompiler.Function.<init>(Function.java:261)
at de.lego.gottfried.decdat.decompiler.Decompiler.addBytecode(Decompiler.java:105)
at de.lego.gottfried.decdat.decompiler.DecompilerFunction.toString(DecompilerFuncti on.java:38)
at de.lego.gottfried.decdat.exporter.Exporter.toFile(Exporter.java:25)
at de.lego.gottfried.decdat.exporter.Exporter.ToFile(Exporter.java:125)
at de.lego.gottfried.decdat.MainForm.actionPerformed(MainForm.java:264)
at javax.swing.AbstractButton.fireActionPerformed(Unknown Source)
at javax.swing.AbstractButton$Handler.actionPerformed(Unknown Source)
at javax.swing.DefaultButtonModel.fireActionPerformed(Unknown Source)
at javax.swing.DefaultButtonModel.setPressed(Unknown Source)
at javax.swing.AbstractButton.doClick(Unknown Source)
at javax.swing.plaf.basic.BasicMenuItemUI.doClick(Unknown Source)
at javax.swing.plaf.basic.BasicMenuItemUI$Handler.mouseReleased(Unknown Source)
at java.awt.Component.processMouseEvent(Unknown Source)
at javax.swing.JComponent.processMouseEvent(Unknown Source)
at java.awt.Component.processEvent(Unknown Source)
at java.awt.Container.processEvent(Unknown Source)
at java.awt.Component.dispatchEventImpl(Unknown Source)
at java.awt.Container.dispatchEventImpl(Unknown Source)
at java.awt.Component.dispatchEvent(Unknown Source)
at java.awt.LightweightDispatcher.retargetMouseEvent(Unknown Source)
at java.awt.LightweightDispatcher.processMouseEvent(Unknown Source)
at java.awt.LightweightDispatcher.dispatchEvent(Unknown Source)
at java.awt.Container.dispatchEventImpl(Unknown Source)
at java.awt.Window.dispatchEventImpl(Unknown Source)
at java.awt.Component.dispatchEvent(Unknown Source)
at java.awt.EventQueue.dispatchEventImpl(Unknown Source)
at java.awt.EventQueue.access$500(Unknown Source)
at java.awt.EventQueue$3.run(Unknown Source)
at java.awt.EventQueue$3.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Un known Source)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Un known Source)
at java.awt.EventQueue$4.run(Unknown Source)
at java.awt.EventQueue$4.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Un known Source)
at java.awt.EventQueue.dispatchEvent(Unknown Source)
at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
at java.awt.EventDispatchThread.run(Unknown Source)
It looks like DecDat has any bugs because it mangles functions.
For example