Ich habe vor kurzem angefangen zu lernen, wie man Scripte für Gothic (2) schreibt, da kam es gelegen, dass ich schon immer mal Manaregeneration in jeder möglichen Mod haben wollte und Ninja dies ermöglichen kann.
Gerne akzeptiere ich konstruktive Kritik, wenn vorhanden
Konfiguration des Patches
Um die Standardwerte (THRESHOLD/Schwellenwert: 50 Mana, PERMILLE: 10) zu überschreiben,
trägt man folgendes (z.B. ans Ende) in die System\Gothic.ini ein:
Erklärung
THRESHOLD: Der "Maximales Mana"-Wert, den der Held benötigt, damit die Manaregeneration freigeschaltet wird. (Standard: 50)
PERMILLE: Der Wert mithilfe dessen, die Regenerationsrate bestimmt wird. (Standard: 10 => 1% Pro Tick)
TICKRATE: Frequenz/Tickrate der Manaregeneration in Millisekunden (1000=1 Sekunde)
PER_TICK: Feste Menge Mana welche Pro Tick regeneriert wird. (Sinnvoll für G1, wo Manakosten gering sind)
Die If-Klausel kannst du dir mit FF_ApplyOnceExt sparen (die macht genau das).
Das setzen des Timers ist ein bisschen gefährlich, denn das ist eine globale Einstellung. Falls sich andere Teile der Mod darauf verlassen, dass die Timer im Menü weiterlaufen, machst du das damit kaputt. Ist allerdings nicht unbedingt deine Schuld: Man kann das in LeGo 2.5 nicht für einzelne FFs einstellen. Im nächsten Release gibt es eine Möglichkeit, FFs an den Gametimer zu hängen, dann brauchst du das nicht mehr. Insgesamt gibt es dann zwei getrennte Timer, anstatt einem, den man umstellen kann.
Dann in der Manareg_Init(): Das meiste kannst du dir da sparen, scheinst du ja von mud-freaks Vorlage kopiert zu haben. Die Kommentare sind ziemlich irreführend oder mud-freak und ich verwenden andere Begrifflichkeiten. Zumindest wird die ManaReg_InitOnce() einmal pro Spielstand aufgerufen, nicht einmal pro Session (das heißt für mich: Zwischen Starten und Beenden der Anwendung). Um es wirklich 1x pro Session zu machen, müsste die Variable Ninja_ManaReg_Initialized eine Konstante mit dem Wert 0 sein.
In der *_REGENERATION.d:
Die Funktion wird nur einmal alle zwei Sekunden aufgerufen, aber Float-Operationen sind dennoch nicht soo billig (zumindest im Vergleich zum Rechnen mit Integern). In deinem Fall lässt sich die Mana-Menge auch problemlos ohne Floats berechnen:
Code:
var int mreg_div; mreg_div = n.attribute[ATR_MANA_MAX]/50;
// Falls du korrekt runden möchtest:
var int mreg_div; mreg_div = (n.attribute[ATR_MANA_MAX]+25)/50;
Das ist zwar eher eine Mikro-Optimierung, aber der Code wird lesbarer und du kannst den Faktor jetzt (einfacher) als Konstante ausdrücken. Insgesamt solltest du deine Einstellungen als Konstanten deklarieren und nicht in einer Init-Funktion befüllen.
Das setzen des Timers ist ein bisschen gefährlich, denn das ist eine globale Einstellung. Falls sich andere Teile der Mod darauf verlassen, dass die Timer im Menü weiterlaufen, machst du das damit kaputt. Ist allerdings nicht unbedingt deine Schuld: Man kann das in LeGo 2.5 nicht für einzelne FFs einstellen. Im nächsten Release gibt es eine Möglichkeit, FFs an den Gametimer zu hängen, dann brauchst du das nicht mehr. Insgesamt gibt es dann zwei getrennte Timer, anstatt einem, den man umstellen kann.
D.h. um das sauberer zu implementieren sollte ich warten, verstehe ich das richtig? (Oder gibt es eine Variable/Methode mit der ich "pausieren/überspringen" kann)
Ich habe mal bisschen aufgeräumt, da war ja noch viel nicht verwendeter Code aus der inspirierenden Vorlage.
var int menge; menge = (n.attribute[ATR_MANA_MAX] + 25) / NINJA_MANAREG_MAX_MANA_DIVISOR;
// nicht eher
var int menge; menge = (n.attribute[ATR_MANA_MAX] + (NINJA_MANAREG_MAX_MANA_DIVISOR/2)) / NINJA_MANAREG_MAX_MANA_DIVISOR;
Wegen dem Timer-Problem kannst du einfach das in deine FF schreiben:
Code:
if (MEM_Game.timeStep) {
return;
};
Dann wird der Code nicht ausgeführt, falls das Spiel gerade pausiert ist. Ist dann ein bisschen ungenau wegen dem 2-Sekunden Timer, der ja trotzdem läuft, aber fürs erste funktioniert das.
var int menge; menge = (n.attribute[ATR_MANA_MAX] + 25) / NINJA_MANAREG_MAX_MANA_DIVISOR;
// nicht eher
var int menge; menge = (n.attribute[ATR_MANA_MAX] + (NINJA_MANAREG_MAX_MANA_DIVISOR/2)) / NINJA_MANAREG_MAX_MANA_DIVISOR;
Du hast natürlich recht - hatte schon viel zu lange nicht wirklich mit Mathe zu tun, sogar simple Mathematik kann man vergessen
Bezüglich des Timers, genau sowas hatte ich mir als workaround gedacht - aber kannte / kenne die ganzen variablen noch nicht wirklich. Kommt noch
Wegen dem Timer-Problem kannst du einfach das in deine FF schreiben:
Code:
if (MEM_Game.timeStep) {
return;
};
Dann wird der Code nicht ausgeführt, falls das Spiel gerade pausiert ist. Ist dann ein bisschen ungenau wegen dem 2-Sekunden Timer, der ja trotzdem läuft, aber fürs erste funktioniert das.
Das scheint leider nicht zu funktionieren, mit diesem Code wird die Methode nicht ausgeführt :/
Ich habe auch noch ein bisschen Feedback (für Übersichtlichkeit nummeriert).
Cool, dass du dich bei dem Patch so an die Vorlagen gehalten hast. Das macht alles recht übersichtlich!
Wünschenswert wäre sowohl eine Readme_Ninja_ManaReg.txt als auch eine Info-Beschreibung in der VDF-Datei (siehe GFA Patch). Da ist es auch wichtig zu erwähnen, dass es ich eben nicht um LeGo 2.5 handelt, sondern um eine angepasste Version (die du scheinbar aus dem GFA Patch übernommen hast):
Zitat von Readme vom GFA-Patch
LeGo 2.5.0-rev-170*
[...]
* For compatibility with LeGo 2.4, some functions are not included
(ViewPtr_AddText, View_AddText, View_AddTextColored, Button_SetCaption)
Die Dateinamen der angepassten LeGo-Version solltest du auch an deinen Patchnamen anpassen, diese heißen noch
Code:
__BUTTONS_NINJA_GFA.D
__VIEW_NINJA_GFA.D
(Entsprechende Referenzen in der HEADER.SRC auch anpassen.) Das ist wichtig, falls sich die Inhalte der beiden Patches mal unterscheiden sollten, denn jede Datei mit identischem Pfad wird nur einmal aus den VDF-Dateien geladen (daher müssen alle Patch-spezifischen Dateien einzigartige Namen tragen).
Zitat von Lehona
Dann in der Manareg_Init(): Das meiste kannst du dir da sparen, scheinst du ja von mud-freaks Vorlage kopiert zu haben. Die Kommentare sind ziemlich irreführend oder mud-freak und ich verwenden andere Begrifflichkeiten. Zumindest wird die ManaReg_InitOnce() einmal pro Spielstand aufgerufen, nicht einmal pro Session (das heißt für mich: Zwischen Starten und Beenden der Anwendung). Um es wirklich 1x pro Session zu machen, müsste die Variable Ninja_ManaReg_Initialized eine Konstante mit dem Wert 0 sein.
Damit die Mana-Regeneration auch nach Weltenwechsel problemlos läuft, solltest du hier lieber hero anstatt PC_Hero verwenden.
Zitat von johnnyboyy
Das scheint leider nicht zu funktionieren, mit diesem Code wird die Methode nicht ausgeführt :/
Du könntest auch folgendes probieren:
Code:
if (MEM_Game.pause_screen) {
return;
};
Ich habe den Patch nicht getestet. Hast du mal ein paar Tests mit einer handvoll verschiedener und verschieden-alter Mods gemacht? Auch in Verbindung mit anderen Patches? Hast du Randfälle überprüft (während Regeneration Spiel speichern/laden, Weltenwechsel, Spiel beenden und neu starten, neues Spiel, usw.).
Meiner Meinung nach ist das Testen von Patches besonders wichtig, denn sonst verursachst du Mod-Entwicklern eine Menge Ärger, wenn sie sich um Bugs kümmern sollen, die gar nicht durch ihre Mod entstanden sind (außerdem wird bei Problemen gern schnell pauschalisiert, z.B. "Patches sind alle von Vornherein verbuggt!").
Gute Arbeit!
EDIT: Ein Verbesserungsvorschlag wäre, dass sich der Schwellenwert von 50 Mana und die Regenerierungsgeschwindigkeit/-menge in der Gothic.ini einstellen ließe.
Vielleicht hab ich die Bedingung umgedreht - überprüf mal das Gegenteil.
Damit gehts
Zitat von mud-freak
Gute Arbeit!
EDIT: Ein Verbesserungsvorschlag wäre, dass sich der Schwellenwert von 50 Mana und die Regenerierungsgeschwindigkeit/-menge in der Gothic.ini einstellen ließe.
D.h. aus den Konstanten werden wieder Variablen, die ich im "initOnce" befülle, korrekt?
Sporadisch erhalte ich beim Speichern eine Access Violation, hier kann ja eigentlich nur "hero" der pointer sein, oder irre ich mich?
Gibt es beim Speichern/Levelwechsel etwas wichtiges zu beachten, was ich nicht tue?
EDIT: Kann es zurückführen auf den Marvin-Modus, sobald ich ihn Aktiviere und Deaktiviere (c42c) tritt das verhalten auf. Auch ohne mein Script. (Nur LeGo und Ikarus)
Reproduzierbar durch Marvin->"edit abilities" -> Attribute -> 2 = 80 -> c42c -> speichern. Zufällig (häufig) Fehler und crash
Code (Ninja_ManaReg_Regeneration)
Spoiler:(zum lesen bitte Text markieren)
Code:
func void Ninja_ManaReg_Regeneration() {
// HACK: Entfernen wenn es FF gibt, welche nur im nicht pausierten Modus feuern.
if (!mem_game.timestep) {
return;
};
//var c_npc n; n = Hlp_GetNpc(PC_Hero);
var c_npc n; n = Hlp_GetNpc(hero);
if (n.attribute[ATR_MANA_MAX] >= NINJA_MANAREG_MANA_THRESHOLD) {
if (n.attribute[ATR_MANA] < n.attribute[ATR_MANA_MAX]) {
var int menge; menge = (n.attribute[ATR_MANA_MAX] + (NINJA_MANAREG_MAX_MANA_DIVISOR/2)) / NINJA_MANAREG_MAX_MANA_DIVISOR;
Npc_ChangeAttribute(n, ATR_MANA, menge);
};
};
};
D.h. aus den Konstanten werden wieder Variablen, die ich im "initOnce" befülle, korrekt?
Das sieht im Code auf Github ganz gut aus. Sollte so klappen.
Zitat von johnnyboyy
Sporadisch erhalte ich beim Speichern eine Access Violation, hier kann ja eigentlich nur "hero" der pointer sein, oder irre ich mich?
Nein, der Pointer ist ein nicht vorhandenes Array (_PM_Head.content) beim Schreiben in PermMem in Zusammenhang mit deiner FrameFunction. Wenn das wie du schreibst auch ohne dein Skript vorkommt, stimmt da was nicht. Kann es sein, dass das mit einem geladenem Speicherstand passiert ist?
Speicherst du mit deinem Patch, wird die FrameFunction mit in den Speicherstand gelegt. Lädst du dann den Speicherstand ohne den Patch, kann PermMem den Funktionsnamen "Ninja_ManaReg_Regeneration" keinem Symbol zuordnen. Ich meine aber, das war kein Problem und LeGo würde in solchen Fällen lediglich eine Warnung auswerfen. Vor allem kann ich mir nicht vorstellen, dass es an einem nicht existierendem Array (_PM_Head.content) scheitert...
Kannst du versuchen, den Fehler etwas mehr einzugrenzen? (Mit/ohne Patch, neues Spiel/geladenes Spiel jeweils mit/ohne Patch, ...) Hast du noch andere Änderungen in Gothic, die die Ursache dafür sein könnten?
Vorweg: mein Gothic2 Verzeichnis für das Scripten ist ein frisch installiertes, nach empfohlener Reihenfolge + Mdk mit Rohdaten
Ich habe einen Speicherstand, welchen ich ohne Mod-Starter / Scripts & Ninja erstellt habe, "START1" diesen habe ich als Basis für alle meine Tests benutzt.
Diesen habe ich immer geladen (Modstarter + Scripte parsen) und anschließend auf "START2" gespeichert. Soweit so gut.
Wenn ich jedoch, im Marvin Modus meine Attribute ändere und den Marvin-Modus dann beende, anschließend versuche auf START3 zu speichern, bekomme ich teilweise eine Access Violation.
Diese tritt auch auch, wenn ich die FF_ApplyOnce-Zeile auskommentiere (d.h. nur der INIT-Code mit dem Mergen von Flags wird ausgeführt).
Nach der Arbeit versuche ich das noch genauer zu reproduzieren.
EDIT: Also sind Speicherstände kaputt, wenn man den Ninjapatch wieder entfernt? // derp
Versuch es am besten immer mit neuen Spielen als Basis anstatt einem geladenen Spielstand (ausgenommen du willst eben gerade das Speicher- und Ladeverhalten mit und ohne Patch testen).
Wenn du nur Änderungen an Ninja vornimmst, brauchst du übrigens nicht das Häkchen bei "Skripte parsen" im Mod-Starter, dann geht's schneller. Ninja parst immer seine Skripte.
Vielleicht finde ich am Wochenende Zeit zu probieren, ob ich den Fehler reproduzieren kann.
Zu deinem Edit:
Nein. Im Normalfall kann man sie problemlos entfernen und hinzufügen (solang sie keine Symbolindices abspeichern, was mir nur vom EventHandler in LeGo bekannt ist, den man aber auch ohne PermMem-Handle benutzen kann, um das Problem zu umgehen). Allerdings hatte ich in meinen Patches nie eine fortlaufende FrameFunction und kann mich nicht mehr erinnern, ob das ein Problem war.
Mein Ziel ist es einen Patch zu kreieren, welcher mit vorhandenen Spielständen verwendet werden kann, deswegen hatte ich das als Basis genommen.
Mein Script lasse ich aktuell noch über Gothic.src Parsen und erst nach dem testen landet das im separaten "ninjasrc" Verzeichnis, mit der Struktur vom Patch
Danke für den zSpy Log. Bis ich mir das genauer angucken kann, kannst du einmal unter deiner Funktion "Ninja_ManaReg_Init()" eine "leere" Funktion anfügen? Also in /NINJA/CONTENT/NINJA_ManaReg_INITCONTENT.D nach Zeile 57 etwas wie:
Code:
func void Ninja_ManaReg_Dummy() {
var int i; i = i;
return;
};
Ikarus weigert sich, negative Pointer zu dereferenzieren (MEM_PtrToInst), was dann später zu einem MEM_WriteInt(0, x) führt (und MEM_WriteInt hat aus Performance-Gründen keine Sanity-Checks) Wenn ich mich nicht irre ist es aber keinesfalls unmöglich, negative Pointer zu erzeugen. Eine kurze Google-Suche hat bestätigt, dass alle 32bit-Werte ungleich NULL gültige Pointer sind.
Die Frage ist natürlich, warum das jetzt mehr oder weniger zum ersten Mal aufgetreten ist (bzw. ich davon gehört habe). Benutzt das neuste SystemPack vielleicht das Flag, dass den Adressraum von 2 auf 4 GiB erweitert?
Du kannst mal versuchen, in MEM_PtrToInst nur bei ptr == 0 abzubrechen bzw. gar nicht abzubrechen und nur eine Warnung auszugeben.
Edit: Eh, da gehen unsere Lösungsansätze aber ganz schön auseinander
Da fällt mir ein: Ich hab ja das LAA-FLAG gesetzt auf meiner gothic.exe!
Hab mal ohne den FLAG getestet: Das Problem tritt nicht mehr auf (zumindest in meinen tests)
Hatte den mal auf meine "clean" installation gesetzt, weil das früher mal vor OutOfMemory geholfen hatte, meine ich mich zu erinnern
EDIT:
Zitat von mud-freak
Danke für den zSpy Log. Bis ich mir das genauer angucken kann, kannst du einmal unter deiner Funktion "Ninja_ManaReg_Init()" eine "leere" Funktion anfügen? Also in /NINJA/CONTENT/NINJA_ManaReg_INITCONTENT.D nach Zeile 57 etwas wie:
Code:
func void Ninja_ManaReg_Dummy() {
var int i; i = i;
return;
};
Warum zum Geier hilft das gegen das Problem?? *interessiert*
Habe das mal eingebaut, und die gothic2.exe mit LAA-Flag getestet und das Problem scheint nicht mehr aufzutreten
EDIT2:
Zitat von Lehona
Du kannst mal versuchen, in MEM_PtrToInst nur bei ptr == 0 abzubrechen bzw. gar nicht abzubrechen und nur eine Warnung auszugeben.
Ich habe mal Ikarus.d->MEM_PtrToInst geändert und damit getestet:
Code:
func MEMINT_HelperClass MEM_PtrToInst (var int ptr) {
var MEMINT_HelperClass hlp;
const int hlpOffsetPtr = 0;
if (!hlpOffsetPtr) {
hlpOffsetPtr = MEM_ReadIntArray (currSymbolTableAddress, hlp) + zCParSymbol_offset_offset;
};
if (ptr == 0) {
if (!MEM_AssignInstSuppressNullWarning) {
/* Instanzen die Null sind, will man eigentlich nicht, die machen nur Ärger. */
MEM_Warn ("MEM_PtrToInst: ptr is NULL. Use MEM_NullToInst if that's what you want.");
};
MEM_WriteInt(hlpOffsetPtr, 0);
} else {
MEM_WriteInt(hlpOffsetPtr, ptr);
};
MEMINT_StackPushInst (hlp);
};
Vorher noch die Frage: Bist du wirklich sicher, dass das Problem durch die leere Funktion behoben ist (inkl. gesetztem LAA-Flag) oder hast du es nur ein-zwei mal getestet?
Was FrameFunctions in Patches angeht: Entfernst du den Patch von einer Mod, die LeGo benutzt, stürzt das Spiel beim Laden der zuvor gespeicherten Spiele ab. Das liegt daran, dass die FrameFunctions mitgespeichert/-geladen werden, aber die Funktion ohne den Patch nicht mehr existiert. LeGo anzupassen, dass es erst schaut, ob eine entsprechende Funktion existiert, wird nichts bringen, weil sich das ja nicht rückwirkend auf ältere Mods auswirkt. Die Spieler müssten also die "SCRPTSAVE.SAV" editieren und die FrameFunction herauslöschen, was nicht besonders Benutzerfreundlich ist. Das ist aber nun ein spezieller Fall, denn die bisherigen Patches benutzen keine (fortlaufenden) FrameFunctions.
Mir fallen einige Alternativen ein (FrameFunction-Handle beim Speichern ausschließen, FrameFunction über Hooks emulieren, FrameFunctions Handle-los nachbauen), die aber alle mehr oder weniger große Probleme mit sich bringen.
Dass das nun nicht von Haus aus funktioniert, liegt nicht an Ninja, geschweige denn an LeGo. (Ninja öffnet ja nur die Tür für Skriptpatches, setzt aber nicht die Verwendung von LeGo voraus. Und niemand hätte bei der Entwicklung von LeGo so etwas ahnen können oder Rücksicht darauf nehmen müssen.) Es ist also beim Erstellen des Patches, wo man sich etwas einfallen lassen muss, wie man seine Ideen realisiert. Deshalb ist das Ganze nicht so ganz geradlinig wie normales Skripten und ein bisschen mehr herausfordernd. Ich hoffe, das ging schon aus der Dokumentation von Ninja etwas hervor.
Hier meine Empfehlung:
Anstatt einer FrameFunction erstellst du einen Hook (z.B. an selber Adresse), mit dem du die verstrichene Zeit mit der Verzögerung von 2 Sekunden abgleichst und entsprechend die Manapunkte erhöhst (wenn das Spiel gerade nicht pausiert ist). Das ist aber sehr speziell für diesen Fall und löst das Problem nicht in anderen Fällen. Denn durch den Hook läuft diese "Schleife" in jedem geladenen Spielstand und lässt sich nicht von Spielstand zu Spielstand de-/aktivieren, oder mitspeichern. Bei der Manaregeneration ist das ja kein Problem, weil sie immer stattfindet und nicht beispielsweise erst gelernt werden muss o.ä.
Vorher noch die Frage: Bist du wirklich sicher, dass das Problem durch die leere Funktion behoben ist (inkl. gesetztem LAA-Flag) oder hast du es nur ein-zwei mal getestet?
...
Hier meine Empfehlung:
Anstatt einer FrameFunction erstellst du einen Hook (z.B. an selber Adresse), mit dem du die verstrichene Zeit mit der Verzögerung von 2 Sekunden abgleichst und entsprechend die Manapunkte erhöhst (wenn das Spiel gerade nicht pausiert ist). Das ist aber sehr speziell für diesen Fall und löst das Problem nicht in anderen Fällen. Denn durch den Hook läuft diese "Schleife" in jedem geladenen Spielstand und lässt sich nicht von Spielstand zu Spielstand de-/aktivieren, oder mitspeichern. Bei der Manaregeneration ist das ja kein Problem, weil sie immer stattfindet und nicht beispielsweise erst gelernt werden muss o.ä.
Ich hatte es ca 5x probiert, jeweils Neigeladen und neuen Spielstand (10x Ges.)
Kann natürlich auch einfach sein, dass in den Speichervorgängen einfach kein negativer Pointer aufgetreten ist. Ich probiere das nach der Arbeit noch ein paar Mal.
Zum anderen Thema:
Gibt es da eine (verständliche) Anleitung wie man die Adresse herausbekommt und hooks in die gewünschte Adresse einbindet?
Ich probiere das nach der Arbeit noch ein paar Mal.
Das wäre gut zu wissen, denn ich glaube es liegt nicht zwingend am LAA-Flag, bzw. mich wundert dass beide Lösungsansätze funktionieren.
Zitat von johnnyboyy
Gibt es da eine (verständliche) Anleitung wie man die Adresse herausbekommt und hooks in die gewünschte Adresse einbindet?
Im LeGo-Thread hatte sich glaube ich Lehona mal die Mühe gemacht eine verständliche Anleitung zu Hooks zu schreiben, aber ich habe den Post jetzt auf die schnelle nicht gefunden. Hier ist es aber ziemlich einfach: mit "an selber Adresse" meinte ich die Adresse von FrameFunctions aus LeGo ("oCGame__Render"). Es ginge zwar auch eleganter (z.B. in oCAIHuman::PC_walkcycle oder so ähnlich, dann würde die Funktion direkt nur während des laufenden Spiels aufgerufen werden), aber das sollte so passen (code ungetestet):
Code:
func void Ninja_ManaReg_Regeneration() {
// Not during loading
if (!Hlp_IsValidNpc(hero)) {
return;
};
// Only in-game
if (!MEM_Game.timestep) {
return;
};
// Only in a certain interval
var int delayTimer; delayTimer += MEM_Timer.frameTime;
if (delayTimer < 2000) {
return;
};
delayTimer -= 2000;
// Increase mana
if (hero.attribute[ATR_MANA_MAX] >= Ninja_ManaReg_Mana_Threshold) {
if (hero.attribute[ATR_MANA] < hero.attribute[ATR_MANA_MAX]) {
var int menge; menge = (hero.attribute[ATR_MANA_MAX] + (Ninja_ManaReg_Max_Mana_Divisor/2)) / Ninja_ManaReg_Max_Mana_Divisor;
Npc_ChangeAttribute(hero, ATR_MANA, menge);
};
};
};
Die Initialisierung von LeGo kann man sich so komplett sparen, weil die HookEngine auf keinem Paket aufbaut (seit 2.4), d.h. die ganze "MergeLeGoFlags"-Geschichte kannst du dir so sogar sparen.