Die Erkenntnisse bzgl. des Auslesens beliebiger Speicherstellen schrien geradezu danach, dass darauf aufbauend ein Skriptpaket entsteht, dass den Zugriff auf verschiedene Engineobjekte ermöglicht.
Dieses Paket macht viele für Modder interessante Engineklassen in Daedalus verfügbar und bietet grundlegende Funktionalität zum Arbeiten mit Engineobjekten.
Desweiteren sind nebem diesem allgemeinen Rahmenwerk auch einige sehr spezielle und unmittelbar nützliche Funktionen Teil dieses Pakets.
Ich möchte hier zunächst eine kurze, unvollständige Übersicht darüber geben, was mit dem Paket möglich ist. Für eine umfassendere Einschätzung möchte ich auf die Doku verweisen.
Grundfunktionalität
Lesen und schreiben von Integern und Strings an beliebigen Speicherstellen.
aus Speicheradresse einen Objektzeiger gewinnen
aus einem Objektzeiger seine Speicheradresse gewinnen
Unmittelbar nützlich (nur Beispiele!):
Marvinmodus an und ausschalten
Spiel pausieren
Regen kontrollieren
Truhen oder Türen auf oder abschließen
Menüelemente bearbeiten (zum Beispiel "Speichern"-Menüpunkt deaktivieren)
SpawnManager Konstanten bearbeiten
Informationsbeschaffung:
Das vom Spieler fokussierte Objekt finden
Vobs anhand ihres Namens finden
Position der Kamera ermitteln
Waynet analysieren
herausfinden ob eine Taste gedrückt ist
Kommandozeilenparameter auslesen
Vollständig neue Möglichkeiten:
Gothic.ini lesen und schreiben, .ini Datei der Mod lesen
Daedaluscode zur Laufzeit bearbeiten
Speicher allozieren und freigeben (flüchtig bzgl. Laden und Speichern!)
Sonstiges / "Nice to have":
Choiceliste einer oCInfo direkt bearbeiten
Suchen von Parsersymbolen anhand ihres Namens
Funktionen anhand ihres Namens oder Symbolindex aufrufen
Im Code Labels initialisieren und zu ihnen springen (für elegante Schleifen)
Stringfunktionen: Zugriff auf einzelne Zeichen, Länge, Vergleich von Strings, String -> Integer Konvertierung
Nicht für jedes der oben genannte Dinge, gibt es eine für sich stehende vorgefertigte Funktion. Was genau und mit welchen Parametern zur Verfügung steht, ist der Doku zu entnehmen. Grundsätzlich gilt: Dieses Paket ist eher als Türöffner zu verstehen, der das nötige Wissen über die Objekte und nur grundlegende Funktionen zur Verfügung stellt. Spezielle Anwendungen müssen selbst daraus abgeleitet werden.
Ich biete zum Beispiel keine Funktionen an, die direkt den Marvin Modus abschaltet. Aber durch dieses Paket ist das Abschalten mit nur zwei Zeilen Code möglich, wenn man weiß wie es geht (in MEM_Game die Eigenschaft game_testmode überschreiben).
Es sind weit mehr als die genannten Dinge möglich. Wenn man sich etwas einarbeitet, kann man mit diesem Paket eine große Menge an neuen Features implementieren. Was dieses Skriptpaket aber ausdrücklich nicht kann, ist Enginefunktionen aufzurufen, zu verändern oder gar zu ersetzen. Dafür gibt es G2Ext. Als Faustregel: Alle Features, die nur darauf angewiesen sind, Lese- und/oder Schreibzugriff auf die richtigen Daten zu haben, sind umsetzbar. Features, die ein vollständig neues Verhalten der Engine benötigen, sind dagegen mit großer Wahrscheinlichkeit nicht umsetzbar.
Es ist aber nicht immer offensichtlich, ob ein konkretes Vorhaben mit Ikarus umgesetzt werden kann oder nicht. Im Zweifelsfall: Fragen! Einschätzungen, ob bestimmte, hier nicht genante Features, möglich sind und wenn ja, welche Klassen und Klasseneigenschaften nützlich sein könnten, kann ich auf Anfrage geben.
Ansonsten hilft das Stöbern in den Klasseneingeschaften vielleicht weiter, um eine Idee davon zu bekommen, wo man rankommt und wo man vermutlich nicht so einfach rankommt.
Damit die Scripte laufen, muss eine Gothic 2 Reportversion zur Verfügung stehen, auch bei den Spielern! Mit anderen Versionen ist dieses Paket inkompatibel, die Offsets stimmen nicht und Klassen sind möglicherweise verschieden.
Doku:
Hier eine kurze Übersicht, was das Paket beinhaltet. Wer weiß, was Klassen und Zeiger sind, den wird Abschnitt III langweilen, indem ich ein paar Grundbegriffe kläre. Abschnitt IV verliert ein paar Worte zu allen Klassen, die ich herausgeschrieben habe, Abschnitt V eine Übersicht über die Funktionen, die ich zur Verfügung stelle.
Man beachte insbesondere auch Abschnitt VII, die Beispiele helfen vielleicht zu verstehen, worum es hier geht.
Code:
//######################################################
//
// Skriptpaket "Ikarus"
// Author: Sektenspinner
//
//######################################################
/*
Inhalt
I. ) Abgrenzung
II. ) Voraussetzungen
III.) Zu Grunde liegende Konzepte
IV. ) Klassen
V. ) Funktionen
VI. ) Gefahren
VII.) Beispiele
*///######################################
// I. Abgrenzung
//######################################
"Ikarus" ist eine Sammlung von Engine Klassen (bzw. ihrem Speicherbild)
und einigen Funktionen, die helfen mit diesen Klassen umzugehen. Ikarus
ist nützlich um jenseits der Grenzen von Daedalus aber innerhalb der
Grenzen der zEngine zu arbeiten. Dadurch können einige sonst
unerreichbaren Daten und Objekte ausgewertet und verändert werden,
darunter einige, die für Modder durchaus interessant sind.
Ikarus eröffnet KEINE Möglichkeiten die zEngine selbst zu verändern. Wer
ein anderes Verhalten implementieren möchte, das nicht mit bestehenden
Strukturen auskommt, ist hier falsch und mit G2Ext besser beraten.
//######################################
// II. Technische Voraussetzungen
//######################################
Dieses Scriptpaket ist nur auf einer Gothic 2 Reportversion lauffähig.
Auf anderen Gothic Versionen wird die Nutzung dieser Scripte zu
Abstürzen führen. Wer sich also dazu entscheidet, dieses Scriptpaket zu
verwenden, muss dafür sorgen, dass die Spieler der Mod ebenfalls die
Reportversion nutzen.
Neuere Reportversionen als 2.6.0.0 (zur Zeit bei Nico in Planung) werden
KEINE Probleme bereiten und werden voll mit diesem Paket kompatibel sein.
Dieses Scriptpaket setzt ein Verständnis grundlegender
Programmierkonzepte und eine gute Portion Ausdauer und Forschungsgeist
voraus. Die meisten Klasseneigenschaften sind nicht dokumentiert, das
heißt oft muss man ausprobieren ob sie das tun, was man erwartet. Im
Forum kann ich versuchen Fragen zu bestimmten Eigenschaften zu
beantworten und kann ebenfalls versuchen zu gegebenen Problemstellungen
passende Klassen und Eigenschaften zu benennen, die das gewünschte
leisten.
Unterstützung von Gothic 1: Auch wenn andere Gothic Versionen und damit
auch Gothic 1 nicht offziell unterstützt werden ist Ikarus relativ gut
mit Gothic 1 lauffähig, wenn die Konstanten (größtenteils
Speicheroffsets) in Ikarus_Const.d angepasst werden. Da sich in Gothic 1
aber auch einige Klassen unterscheiden und bislang (August 2010) keine
an Gothic 1 angepassten Klassen verfügbar sind, kann es sein, dass
bestimmte Klassen nicht funktionieren.
//######################################
// III. Zu Grunde liegende Konzepte
//######################################
Ich habe versucht den folgenden Text so zu schreiben, dass auch
programmiertechnisch Unbedarfte sich ein Bild davon machen können, worum
es hier geht. Das soll nicht heißen, dass programmiertechnisch
Unbedarfte das Paket nach lesen dieses Textes sofort effektiv nutzen
können. Aber ich wollte lieber etwas zu viel schreiben, als zu wenig.
Wer sich mit Programmieren auskennt sollte mindestens Punkt 1 und 2
problemlos überspringen können.
//--------------------------------------
// 1.) Der Speicher, Adressen und Zeiger
Der Speicher ist eine große Tabelle von Daten. Die Positionen in der
Tabelle werden durch Adressen benannt, also positive ganze Zahlen. An
jeder so identifizierten Speicherzelle steht ein Byte mit Daten (also
ein Wort, das aus 8 binären Ziffern (0 oder 1) besteht). Meistens
erstreckt sich ein Datum (also eine Dateneinheit) über mehrere
zusammenhängende Speicherzellen, oft 4 Stück.
Wenn Gothic ausgeführt wird, liegt Gothic im Speicher herum. "Gothic"
ist im dem Fall sowohl das Programm selbst (der Assemblercode) als auch
die Daten auf denen Gothic arbeitet. Programm und Daten sind in
getrennten Bereichen (Segmenten), für die getrennte
Zugriffsberechtigungen gelten. Auf das Datensegment darf lesend und
schreibend zugegriffen werden und das ist auch das Segment mit dem sich
dieses Skriptpaket beschäftigt.
Während Gothic arbeitet, werden immer wieder neue Objekte angelegt und
nicht mehr benötigte Objekte zerstört, was es schwierig machen kann, ein
bestimmtes Objekt, sagen wir eine bestimmte Truhe zu finden, da man
nicht von vorneherein wissen kann, wo die Daten zu dieser Truhe im
Speicher aufbewahrt werden.
Ganz bestimmte Objekte kann man aber sehr leicht finden. Das sind
meistens sehr wichtige Objekte, die es nur einmal gibt (und irgendetwas
muss ja leicht zu finden sein, sonst könnte die Engine selbst ja gar
nicht arbeiten). Oft ist vereinbahrt, dass ein solches wichtiges Objekt
immer an einer ganz bestimmten Speicherstelle zu finden ist (man weiß
schon, wo man suchen muss, bevor man das Programm ausführt). In diesem
Fall, muss man nur auf die passende Speicherstelle zugreifen und hat die
Daten gefunden, die man will.
Wenn die Position des Objekts im Speicher nicht bekannt ist, gibt es
manchmal eine Stelle im Speicher, an der man die aktuelle Addresse des
Objekts nachschauen kann. Dann kann man das Objekt indirekt finden, in
dem man zunächst an einem wohlbekannten Ort nachschlägt, wo das Objekt
liegt und mit diesem Wissen anschließend darauf zugreift.
Vergleichbar ist das mit einem Buch: Angenommen wir suchen Kapitel 5.
Wenn wir wissen, dass Kapitel 5 auf Seite 42 anfängt, können wir das
Buch direkt richtig aufschlagen.
Im Allgemein wird es aber so sein, dass wir das nicht wissen (denn bei
verschiedenen Büchern sind die Kapitelanfänge im Allgemeinen auf
verschiedenen Seiten). Zum Glück gibt es die Vereinbahrung, dass das
Inhaltsverzeichnis immer am Anfang des Buches ist, das heißt wir können
das Inhaltsverzeichnis finden und dort nachschauen wo Kapitel 5 anfängt.
Mit diesem Wissen ist dann Kapitel 5 leicht zu finden.
Navigation im Speicher ist im Grunde genau dies: Von wenigen Objekten
ist bekannt, wo sie herumliegen. Sie dienen als eine Art
Inhaltsverzeichnis und beinhalten Verweise auf andere Objekte. Diese
Verweise nennt man auch Zeiger oder Pointer.
Beispiel:
An Speicherstelle 0xAB0884 (das ist hexadezimal für 11208836) steht
immer die Addresse das aktuellen oGame (das ist eine Datensammlung, die
wichtige Information zur aktuellen Spielsitzung enthält). Wenn man
diesem Verweis folgt, findet man also ein Objekt vom Typ oGame. Dieses
Objekt beinhaltet verschiedene Eigenschaften, unter anderem die
Eigenschaft "world". Hier findet sich abermals eine Adresse, die diesmal
auf ein Objekt vom Typ oWorld zeigt. Hier findet sich widerum ein
Verweis auf einen zCSkyController_Outdoor, der hat unter anderem eine
Textur, die widerum Daten hat... So kann man sich nach und nach vom
großen Ganzen auf einen einzelnen Pixel am Himmel vorhangeln.
//--------------------------------------
// 2.) Klassen
Ich habe bereits von Objekten gesprochen, die Eigenschaften haben. Es
ist aber so, dass Speicher im Computer etwas unstrukturiertes,
"untypisiertes" ist. Man sieht einem Speicherbereich nicht an, ob es
sich bei seinem Inhalt zum Beispiel um eine Kommazahl, eine Zeichenkette
oder um Farbwerte handelt. Das heißt, selbst wenn man sich überlegt, was
man unter einem Baum, einer Truhe oder einem Npc versteht, muss man
genau festlegen in welcher Reihenfolge welche Daten abgelegt werden. Das
nennt man auch das Speicherlayout eines Objekts und wird in Daedalus als
Klasse notiert. Beispiel:
class cSandwich {
var cToast oben;
var cKäse käse;
var cToast unten;
};
Dies beschreibt eine Klasse, die drei Unterobjekte beinhaltet. Direkt am
Anfang der Klasse steht ein Objekt vom Typ cToast, dann kommt ein Objekt
vom Typ cKäse und dann noch ein Objekt vom Typ cToast. Leider kann man
das in Daedalus nicht so hinschreiben, sondern die Unterobjekte müssen
in primitive Typen heruntergebrochen werden. Wenn man verfolgt, was ein
cToast und cKäse ist, könnte man das Speicherbild von cSandwich
vielleicht so beschreiben:
class cSandwich {//var cToast oben;
var int oben_braeunungsgrad;
var int oben_Gebuttert;
//var cKäse käse;
var string käse_name;
var int käse_fettgehalt;
//var cToast unten;
var int unten_braeunungsgrad;
var int unten_Gebuttert;
};
Wenn man ein konkretes Sandwich sw vorliegen hätte, könnte sw.käse_name
zum Beispiel "Edammer" sein und sw.unten_gebuttert könnte 1 sein, das
heißt der untere Toast wäre mit Butter bestrichen.
Mit dem Wissen, dass eine Ganzzahl (int) immer 4 Byte groß ist und ein
string immer 20 Byte, weiß man schon viel über die Klasse.
Angenommen, ein cSandwich stünde an Speicherposition 123452 (das heißt,
das Objekt beginnt dort), dann findet man an Position 123452 den Wert
"unten_braeunungsgrad", an Position 123456 den wert "unten_Gebuttert",
an Position 123460 die Zeichenkette "käse_name", an Position 123480 den
Wert "käse_fettgehalt" usw..
Auch wenn dieser Hintergrund nicht unbedingt notwendig ist, um dieses
Paket zu benutzen, halte ich es für nützlich dies zu verstehen.
//--------------------------------------
// 3.) Nicht darstellbare primitive Datentypen
Nicht alle primitiven Datentypen ("primitiv" heißt nicht weiter sinnvoll
zerteilbar) die die zEngine kennt, sind in Daedalus auch verfügbar.
Deadalus kennt nur Ganzzahlen (int) und Zeichenketten (string). Das
heißt aber nicht, dass man mit anderen Datentypen nicht auch arbeiten
könnte, aber man muss darauf achten die Datentypen korrekt zu behandeln.
Ein bisschen ist das, als hätte man einen Chemieschrank und auf jeder
Flasche steht "Destilliertes Wasser", obwohl sich in bei weitem nicht
allen Flaschen genau das verbirgt. Die Chemikalien funktionieren
natürlich noch genauso gut, solange man weiß, was wirklich hinter den
Ettiketten steckt. Wenn man aber eine Säure behandelt, als wäre sie
Wasser, wird einen das Ettikett nicht eines besseren belehren und es
knallt (unter Umständen).
Das "Destillierte Wasser" ist in unserem Fall eine Ganzzahl, also ein
integer der Größe 32 bit. Alles was nicht gerade Zeichenkette ist, ist
in den Klassen dieses Pakets als solche Ganzzahl deklariert. Was sich
wirklich dahinter verbirgt, geht aus den Kommentaren hervor.
Einige wichtige Datentypen sind:
//##### int ######
Wenn es nicht nur als int deklariert ist, sondern auch im Kommentar
steht, dass es ein int ist, dann ist es ein int! Also die altbekannte 4
Byte große Ganzzahl mit Vorzeichen.
//##### zREAL ####
Ein zREAL ist ein 4 Byte IEEE Float, oft mit "single" bezeichnet. Das
ist eine Gleitkommazahl mit der man in Daedalus normalerweise nicht
rechnen kann. Ich habe aber mal Funktionen geschrieben, die solche
Zahlen verarbeiten können:
http://forum.worldofplayers.de/forum/showthread.php?t=500080
Zum Beispiel bietet diese Scriptsammlung eine Funktion roundf, die einen
solchen zREAL in eine Ganzzahl umwandelt (rundet).
//##### zBOOL ####
(Sinnloserweise) auch 4 Byte groß. Ein zBOOL ist wie eine Ganzzahl, aber
mit der Vereinbahrung, dass nur zwischen "der Wert ist 0" und "der Wert
ist nicht 0" unterschieden wird. Ein zBOOL hat also die Bedeutung eines
Wahrheitswerts.
//##### zDWORD ####
Eine 4 Byte große Zahl ohne Vorzeichen. Das heißt die
Vergleichsoperationen <, <=, >, >= liefern mitunter falsche Ergebnisse,
da ein sehr großes zDWORD als negative Zahl interpretiert wird, obwohl
eine positive Zahl gemeint ist. Das sollte aber nur in Außnahmefällen
von Bedeutung sein, im Regelfall kann ein zDWORD behandelt werden wie
ein Integer, solange man nicht versucht negative Werte hineinzuschreiben.
//#### zCOLOUR ####
Ein 4 Byte großes Speicherwort, bei dem jedes Byte für einen Farbkanal
steht: rot, grün, blau und alpha. Jeder Kanal ist also mit 8 bit
aufgelöst. Die Farbe "orange" würde als Hexadezimalzahl interpretiert so
aussehen:
0xFFFF7F00
blauer Kanal: 00, es ist kein Blauanteil in der Farbe.
grüner Kanal: 7F, also mittelstark vertreten
roter Kanal: FF, also so rot als möglich.
alpha Kanal: FF, also volle Opazität.
Die scheinbar umgekehrte Reihenfolge kommt daher, dass die
niederwertigen Bytes auch an den kleineren Adressen steht. Im Speicher
sieht die Farbe so aus:
Byte0: 00
Byte1: 7F
Byte2: FF
Byte3: FF
Siehe dazu auch "little Endian" z.B. in der Wikipedia.
//##### zVEC3 #####
Dies ist kein primitiver Datentyp, wird aber oft verwendet: Es ist ein
Trippel aus drei zREALs und stellt einen Vektor im dreidimensionalen
Raum dar. Deklariert habe ich solche zVEC3 in der Regel als integer
Arrays der Länge 3.
//## Zeigertypen ###
Ein Zeiger ist ein vier Byte großes Speicherwort und enthält die Adresse
eines anderen Objekts. In aller Regel weiß man von welchem Typ das
Objekt ist, auf das der Zeiger zeigt. Als Datentyp des Zeigers gibt man
den Datentyp des Objekts an, auf das gezeigt wird und versieht diesen
mit einem *. Nehmen wir mal an, wir treffen auf folgendes:
var int ptr; //cSandwich*
Dass ptr als Integer deklariert ist, soll uns nicht weiter stören, der
wahre Datentyp steht im Kommentar: Es ist (kein cSandwich aber) ein
Zeiger auf ein cSandwich. Das heißt, nimmt man den Wert von ptr her und
interpretiert ihn als Adresse, wird man an dieser Adresse ein cSandwich
im Speicher vorfinden. An folgenden Adressen ist also folgendes zu
finden:
ptr + 0 : int oben_braeunungsgrad
ptr + 4 : int oben_Gebuttert
ptr + 8 : string käse_name
ptr + 28 : int käse_fettgehalt
ptr + 32 : int unten_braeunungsgrad
ptr + 36 : int unten_Gebuttert
(man beachte, dass ein Integer 4 Byte und ein String 20 Byte groß ist)
Es kann auch sein, dass ein Zeiger auf gar kein Objekt zeigt. Dann ist
sein Wert (als Zahl interpretiert) 0. Man spricht von einem Null-Zeiger.
Zum Beispiel könnte ein Zeiger auf die aktuelle Spielsitzung Null sein,
solange der Spieler noch im Hauptmenü ist und gar keine Sitzung
existiert.
Natürlich gibt es auch Zeiger auf Zeiger. Diese sind dann entsprechend
mit zusätzlichen Sternen gekennzeichnet.
//######################################
// IV. Die Klassen
//######################################
Die Klassen, die ich herausgesucht habe, sind bei weitem nicht alle
Klassen der Engine (es gibt viel mehr). Aber es sind die Klassen, von
denen ich glaube, dass sie für Modder am interessantesten sein können.
Ich habe versucht zu jeder Klasse unter dem Punkt "nützlich für" ein
Beispiel zu nennen, wofür man das Wissen über und den Zugriff auf diese
Klassen nutzen könnte. Vielleicht sind manche der genannten Sachen
schwieriger als ich vermute. Ich habe das meiste nämlich nicht
ausprobiert.
//########### oCGame #############
Hält Eigenschaften der Session und Referenzen auf zahlreiche globalen
Objekte, zum Beispiel die Welt oder den InfoManager. Einige
Einstellungen sind auch interessant.
Nützlich für:
-Marvin Modus an und ausschalten (game_testmode)
-Spiel pausieren (singleStep)
-Interface ausschalten wie mit toggle Desktop (game_drawall)
-uvm.
//######## oCInfoManager #########
Das Objekt, dass die Anzeige von Dialogen übernimmt. Kümmert sich zum
Beispiel darum, dass in den Views die passenden Dinge angezeigt werden.
Nützlich für:
-Diry Hacks mit isDone, z.B. um während eines Dialogs ein Dokument
anzuzeigen
-herausfinden, was im Dialog gerade passiert, z.B ob der Spieler gerade
eine Auswahl treffen muss.
-?
//######## oCInfoManager #########
Hält eine Liste aller Script-Infos (oCInfo).
Nützlich für:
-?
//########## oCInfo ##############
In weiten Teilen schon in Daedalus bekannt. Zusätzlich ist eine Liste
von Choices erreichbar sowieso die Eigenschaft "told", die in Daedalus
über Npc_KnowsInfo gelesen werden kann.
Nützlich für:
-Choiceliste bearbeiten. Man könnte eine Funktion implementieren, die
nur selektiv eine Choice aus der Liste entfernt.
-Nach Choicewahl durch den Nutzer entscheiden, welche Choice gewählt
wurde, selbst wenn alle Choices die selbe Funktion benutzen.
//######## oCInfoChoice ##########
Hält einen Beschreibungstext und den Symbolindex der Funktion, die
aufgerufen werden soll.
//#### oCMob und Unterklassen ####
Die verschiedenen Mobs sind aus dem Spacer bekannt. Besonders die
verschließbaren Mobs sind interessant.
Nützlich für:
-Das vom Spieler fokussierte Mob (oCNpc.focus_vob) bearbeiten, zum
Beispiel Truhen durch Zauber öffnen.
-Von Npc geöffnete Tür wieder verschließen.
//######## zCVob und oCNpc #######
Sind vollständig von Nico übernommen. Einen oCNpc vollständig zu kennen
hat dieses Scriptpaket erst möglich gemacht!
Nützlich für:
-Positionsdaten auslesen und verändern.
-Zugriff auf einen ganzen Haufen anderer Dinge
//############ oCItem ############
Vobs mit bekannten Scripteigenschaften. Zusätzlich lässt die Eigenschaft
"instanz" auf die Scriptinstanz des Items schließen. "amount"(für
fallengelassenen Itemstapel) sollte nicht vergessen werden zu
berücksichtigen, wo nötig.
Nützlich für:
-Heldenfokus analysieren
-Items für den Helden beim Tauchen aufheben
-Telekinese?
//######### zCCamera #############
Die Kamera eben. :-)
Aber Vorsicht: Die zCCamera ist kein zCVob. Sie hält aber eine Referenz
auf ein Hilfsvob (connectedVob).
Nützlich für:
-Positionsdaten ermitteln
-Screenblende einbauen (i.d.R. aber über VFX leichter möglich).
//##### zCMenu / zCMenuItem ######
Das Hauptmenü ist ein Menü. Der "Spiel starten" Button ist ein
Menüelement. Es gibt aber noch mehr Menüs (zum Beispiel das Statusmenü)
die ihrerseits Menüitems beinhalten.
Vorsicht: Zum Teil werden diese Objekte nur beim ersten mal erstellt und
dann im Speicher behalten (auch beim Laden eines anderen Savegames!) zum
Teil werden sie bei jeder Benutzung neu erzeugt.
Nützlich für:
-Charaktermenü überarbeiten (ziemlich fummlig)
-Speichern Menü-Item unter bestimmten Umständen deaktivieren
(Speicherpunkte / Speicherzonen).
//########## zCOption ############
Kapselt die Informationen der Gothic.ini und der [ModName].ini.
Funktionen um die Daten zu lesen und zu verändern stehen in Ikarus
bereit.
Nützlich für:
-Daten Sessionübergreifend festhalten (Lade / Speicherverhalten,
Schwierigkeitsgrad...)
-Einstellungen des Spielers lesen (zum Beispiel Sichtweite)//########## zCParser ############
Der Parser ist nicht nur die Klasse, die beim kompilieren der Scripte
meckert sondern auch eine virtuelle Maschine, die den kompilierten Code
ausführt. Der Parser hält zudem die Symboltabelle.
Nützlich für:
-Daedalus Code zur Laufzeit bearbeiten (Parser Stack).
//######## zCPar_Symbol ##########
Jedes logische Konstrukt (Variable, Instanz, Funktion, Klasse) wird über
ein Symbol identifiziert. Im Grunde sind alle diese Objekte der Reihe
nach durchnummeriert. Mit dieser Nummer erreicht man über die
Symboltabelle des Parsers diese Symbole. Ein Symbol hat einen Namen und
seinen Inhalt, der verschiedenen Typs sein kann.
Nützlich für:
-"call by reference"
-Instance-Offsets bearbeiten.
//#### zCSkyController_Outdoor ####
Beinhaltet alles, was mit dem Himmel zu tun hat. Insbesondere die
Planeten (Sonne und Mond), eine Farbtabelle zur Beleuchtung (je nach
Tageszeit verschieden)(nicht aber die Lightmap selbst, die ist auf die
Polys verteilt) sowie aktuelle Regeneinstellungen.
Nützlich für:
-Regen starten
-Das Sonnenlicht umfärben
-Aussehen der Planeten (Sonne / Mond) ändern
//## zCTrigger und Unterklassen ###
zCTrigger, oCTriggerScript, oCTriggerChangeLevel und oCMover sind aus
dem Spacer bekannt. Jetzt kann man auch auf sie zugreifen.
Nützlich für:
-Ziel eines oCTriggerScripts verändern
-Triggerschleife bauen, die jeden Frame feuert, indem nach einem
Wld_SendTrigger die Eigenschaft _zCVob_nextOnTimer auf die aktuelle Zeit
gesetzt wird (siehe zTimer).
//####### zCWaynet und Co #########
Das zCWaynet beinhaltet eine Liste von allen zCWaypoints und allen
oCWays (das sind die roten Linien zwischen den Waypoints). zCWaypoints
sind keine Vobs, aber jeder zCWaypoint hat ein Hilfsvob in der Welt
(einen zCVobWaypoint), der auch wirklich ein Vob ist, aber sonst nichts
kann.
Jeder oCWay kennt die beiden beteiligten zCWaypoints zwischen denen er
verläuft und jeder zCWaypoints kennt alle oCWays, die von ihm ausgehen.
Nützlich für:
-?
//########### oWorld ##############
Hält neben dem Vobtree auch Dinge wie SkyController und Waynet. Außerdem
gibt es neben dem sehr technischen Bsp-Tree noch die activeVobList, die
alle Vobs enthält die auf irgendeine Art selbst aktiv sind. Das sind
Npcs, Trigger usw. In jedem Frame wird die activeVobList in die walkList
kopiert. Die walkList wird dann sequenziell durchlaufen.
Nützlich für:
-Objekte in der Welt suchen, zum Beispiel Npcs (voblist_npcs), items
(voblist_items), alle Vobs mit AI, z.B. auch Trigger
(activeVobList_array)
-Mir ist es mit rumgehacke an activeVobList und walklist gelungen alles
außer den Helden einzufrieren und später zu reaktivieren
//######## zCZoneZFog #############
Fogzones sind Gebiete mit (möglicherweise farbigem) Nebel, der die
Sichtweite beeinflusst.
Nützlich für:
-evtl. mysteriöse Orte mit obskuren Farbwechseln
-möglicherweise automatische Sichtweiten Korrektur abhängig von der
Framerate
//####### VERSCHIEDENES ###########
//zCTree
Ein Knoten in einem Baum. Das kann ein innerer Knoten oder ein
Blattknoten sein.
Es gibt Zeiger auf das erste Kind, den linken und rechten
Geschwisterknoten sowie den Elternknoten.
Natürlich gibt es auch einen Zeiger auf die Daten.
Der Vobtree ist aus zCTrees aufgebaut.
//zCArray
Ein Array hat einen Zeiger auf einen Speicherbereich in dem die Daten
stehen.
Die Daten sind einfach aneinandergereit. Das Array kennt die Anzahl der
Objekte in dem Speicherbereich. Unter Umständen ist mehr Speicher
alloziert als nötig.
//zCArraySort
Wie ein Array, nur dass die Objekte immer auf bestimmte Art und Weise
geordnet sind (es sei denn du machst diese Ordnung kaputt).
//zList
Sehr gewöhnungsbedürftige generische Liste, die aber mit der Eigenschaft
"next" des generischen Typs arbeitet. Kaum verwendet.
//zCList
Ein Listenelement. Beinhaltet den Zeiger auf genau ein Objekt (die
Daten) und einen Zeiger auf das nächste Listenelement.
//zCListSort
Wie ein Listenelement, nur dass die Gesamtheit der Listenelemente auf
bestimmte Art und Weise geordnet ist (es sei denn du machst diese
Ordnung kaputt).
//zCTimer
Man kann ablesen wie lange ein Frame braucht und es gibt Tuning
Parameter für minimale und maximale Framerate. Vorsicht. Hieran drehen,
kann dafür sorgen, dass das Spiel einfriert!
//oCWorldTimer
Enthält die Weltzeit (0 = 0:00, 6000000 ~=~ 23:59) und den aktuellen Tag.
//oCSpawnManager
Enthält ein Array aller im Moment gespawnter Npcs. Das Spawnen kann
ausgeschaltet werden (spawningEnabled). Die eigentlich interessanten
Werte, die insert und remove Reichweite sind statisch, ich habe die
Adressen dieser Werte über der Klasse angegeben.
//oCPortalRoom und oCPortalRoomManager
Der einzige oCPortalRoomManager der Welt kennt alle oCPortalRooms. Zudem
wird festgehalten in welchem Raum der Spieler gerade ist (curPlayerRoom)
und wo er vorher war (oldPlayerRoom).
Jeder oCPortalRoom hat einen Besitzer, eine Besitzergilde und einen
Namen.
//zCVobLight
Ein Licht. Ich habe nicht getestet, inwiefern man Lichter wirklich
bearbeiten kann. Es könnte sein, dass der Bsp-Tree wissen muss, welche
Lichter welches Weltsegment betreffen (im Spacer muss man ja auch am
Licht wackeln, damit es gerendert wird).
Vielleicht werden Vergrößerungen des Lichtradius erst nach neuem Laden
wirksam. Licht ausschalten sollte aber auch so gehen. Kurz: Selbst
ausprobieren.
//oCMag_Book
Wird genutzt um den Kreis über dem Spieler bei der Zauberauswahl
anzuzeigen. Außerdem enthält diese Klasse Zuordnungen von Zaubern <->
Items <-> Tasten.
//zString
Ein String. Da diese Klasse ein Daedalus-Primitiv ist, ist es in aller
Regel nicht nötig, auf die einzelnen Eigenschaften zuzugreifen. Ich habe
diese Eigenschaften allerdings gebraucht um Speicherallokation zu
implementieren.
//######################################
// V. Die Funktionen
//######################################
//######################################
// 1.) Low-Level Speicherzugriff
Lesen und schreiben von strings und integern ist mit folgenden
Funktionen möglich:
func int MEM_ReadInt (var int address)
func void MEM_WriteInt (var int address, var int val)
func string MEM_ReadString (var int address)
func void MEM_WriteString (var int address, var string val)
Wenn address <= 0 ist, wird ein Fehler ausgegeben. Andernfalls wird
versucht an dieser Adresse zu lesen bzw. zu schreiben.
Liegt die Adresse nicht im Datensegment, gibt es eine Zugriffsverletzung
(Gothic stürzt ab).
Bei Stringoperationen ist es zudem nötig, dass an der angegebene Stelle
bereits ein gültiger zString steht. Wenn dort Unsinn steht, kann lesen
und schreiben gleichmaßen Gothic zum Absturz bringen.
Zudem sind zwei reine Bequemlichkeitsfunktionen implementiert:
func int MEM_ReadIntArray (var int arrayAddress, var int offset)
func int MEM_WriteIntArray (var int arrayAddress, var int offset, var
int value)
Diese Funktionen lesen / schreiben den Wert an Stelle arrayAddress + 4 *
offset.
//######################################
// 2.) Parser Zeug
Mit der Adresse eines Objekts allein kann man nur sehr unbequem auf die
Objekteigenschaften zugreifen.
Besser ist es, wenn eine Instanz auf das Objekt zeigt, und dann mit
"instanz.eigenschaft" auf eine beliebige Eigenschaft zugegriffen werden
kann.
Dazu gibt es folgende Funktion:
func void MEM_AssignInst (var int inst, var int ptr)
Sie nimmt eine Instanz (eigentlich den Symbolindex) und eine Adresse
entgegen und sorgt dafür, dass die Instanz auf ptr zeigt.
Beispiel zur Benutzung:
//******************
func void somefunc(){//Hole Helden
var oCNpc her;
her = Hlp_GetNpc(PC_HERO);
//Hat der Held ein Vob im Fokus?
if (!her.focus_vob){ return; };
//Lasse meinFocusVob auf her.focus_vob zeigen
var zCVob meinFocusVob;
MEM_AssignInst (meinFocusVob, her.focus_vob);
//Nutze meinFocusVob, z.B. um Vobnamen auszugeben
Print(meinFocusVob._zCObject_objectName);
};
//******************
Die umgekehrte Funktion zu MEM_AssignInst ist MEM_InstGetOffset. Sie
liest den Offset aus einer Instanz aus.
func int MEM_InstGetOffset (var int inst)
Zum Beispiel liefert MEM_InstGetOffset (hero) die Adresse des Helden im
Speicher. Diese Funktion wird seltener gebraucht als MEM_AssignInst.
Anmerkung: Instanzen eines Typs und "variablen" eines Typs sind
gleichwertig. Im obigen Beispiel wäre es möglich gewesen "meinFocusVob"
außerhalb der Funktion als "instance meinFocusVob (zCVob);" zu
deklarieren.
Anmerkung: MEM_AssignInst gibt eine Warnung aus, falls ptr == 0, weil
eine Zuweisung eines Nullzeigers in vielen Fällen nicht absichtlich
passieren wird. Um ganz bewusst 0 zu zuweisen gibt es MEM_AssignInstNull.
//******************
Nicht immer weiß man zur Kompilierzeit wann man welche Funktion aufrufen
will. Wenn man zum Beispiel die Condition-Funktion eines Mobs aufrufen
will, das der Spieler im Fokus hat, ist man zur Kompilierzeit ratlos,
weil man nicht ahnen kann welches Mob sich der Spieler aussuchen wird.
Ikarus stellt eine Möglichkeit zur Verfügung mit deren Hilfe Funktionen
anhand ihres Namens oder ihres Symbolindex aufgerufen werden können. Im
Beispiel des Mobs, kann der Name der Condition-Funktion einfach im Mob
nachgeschaut werden.
Die Funktionen sind leicht zu benutzen und leicht zu erklären:
func void MEM_CallByString (var string fnc)
func void MEM_CallByID (var int ID)
MEM_CallByString ruft die Funktion mit Namen fnc auf, hierbei muss der
Name GROSSGESCHRIEBEN werden, wenn es sich um ein Konstante handelt.
MEM_CallByID ruft die Funktion mit Symbolindex ID. An den Symbolindex
einer Funktion kommt man mit zum Beispiel mit MEM_FindParserSymbol.
MEM_CallByID ist schneller als MEM_CallByString und sollte bevorzugt
werden, wenn die selbe Funktion sehr oft aufgerufen werden muss.
Für Funktionen ohne Parameter und Rückgabewerte ist dies das ganze
Geheimnis.
Wenn die aufzurufende Funktion Parameter hat, müssen diese zuvor auf den
Datenstack gelegt werden. Das geht mit den Funktionen:
func void MEM_PushIntParam (var int param)
func void MEM_PushStringParam (var string strParam)
Die Parameter müssen in der richtigen Reihenfolge gepusht werden, von
links nach rechts.
Hat eine Funktion einen Rückgabewert sollte dieser nach Aufruf vom
Datenstack heruntergeholt werden, sonst können unter ungünstigen
Umständen Stacküberläufe entstehen (abgesehen will man den Rückgabewert
vielleicht einfach haben).
Dies geht mit den Funktionen:
func int MEM_PopIntResult()
func string MEM_PopStringResult()
Siehe auch Beispiel 5.
//******************
func int MEM_FindParserSymbol (var string inst)
Kleines Nebenprodukt: Liefert das zCPar_Symbol mit Namen inst zurück,
falls ein solches Symbol existiert. So kann man Variablen anhand ihres
Namens finden und bearbeiten. Es ließe sich eine Art CallByReference
bauen.
//######################################
// 3.) Sprünge
Rücksprünge sind sehr elegant möglich. Mithilfe zweier einfacher Zeilen
kann die aktuelle Position im Parserstack (darunter versteht man einen
assemblerartigen Code der beim Kompilieren der Scripte erzeugt wird)
abgefragt und gesetzt werden. Wird die Position im Parserstack
verändert, so wird die Ausführung an dieser neuen Stelle fortgesetzt.
Beispiel: Folgender Code gibt die Zahlen von 0 bis 42 aus:
//******************
func void foo(){/* Initialisierung */
MEM_InitLabels();
var int count; count = 0;
/* In label die Ausführungsposition festhalten. */
var int label;
label = MEM_StackPos.position;
/* <---- label zeigt jetzt hierhin,
* also auf die Stelle NACH der Zuweisung von label. */Print(ConcatStrings("COUNT: ", IntToString(count)));
count += 1;
if (count <= 42){/* Die Ausführungsposition ersetzen,
* bei dem "<-----" wird dann weitergemacht */
MEM_StackPos.position = label;
};
/* Ist 43 erreicht, wird die "Schleife" verlassen. */};
//******************
Wichtig: MEM_InitLabels() muss nach dem Laden eines Spielstandes einmal
ausgeführt werden. Am einfachsten ist es, diese Funktion aus INIT_GLOBAL
aufzurufen. Erst nachdem diese Funktion aufgerufen wurde, kann korrekt
auf MEM_StackPos.position zugegriffen werden!
Wichtig: Eigentlich selbstverständlich: Ein Label muss initialisiert
sein, bevor dorthin gesprungen werden kann! Vorwärtssprünge sind daher
im Allgemeinen schwierig, weil die Sprungstelle passiert wird, bevor das
Sprungziel passiert wird.
Wichtig: Labels sind nach Speichern und Laden ungültig. Das heißt ein
Label muss "sofort" verwendet werden. Ich wüsste aber sowieso keinen
Grund, weshalb man sich Labels für längere Zeit aufheben sollte.
Wichtig: Wer zwischen verschiedenen Funktionen hin und herspringt und
nicht weiß, was er tut, wird auf die Nase fallen. Wer mit Labels rechnet
und nicht weiß was er tut ebenfalls. Allgemeiner: Wer etwas anderes
macht als Zuweisungen der obigen Art, könnte auf die Nase fallen.
//######################################
// 4.) String Funktionen
//******************
func int STR_GetCharAt (var string str, var int pos)
Liefert das Zeichen am offset pos im String str zurück (das heißt den
Zahlwert dieses Zeichens, wie man ihn in der ASCII-Tabelle findet). Die
Zählung beginnt bei 0.
//******************
func int STR_Len (var string str)
Liefert die Länge des Strings in Zeichen.
//******************
const int STR_GREATER = 1;
const int STR_EQUAL = 0;
const int STR_SMALLER = -1;
func int STR_Compare (var string str1, var string str2){
Liefert STR_GREATER, wenn str1 lexikographisch nach str2 kommt und
entsprechend STR_SMALLER oder STR_EQUAL in den anderen Fällen. Beispiele:
STR_Compare ("A", "B") -> STR_SMALLER
STR_Compare ("ABC", "ABC") -> STR_EQUAL
STR_Compare ("AA","A") -> STR_GREATER
STR_Compare ("BA", "BB") -> STR_SMALLER
STR_Compare ("B", "a") -> STR_SMALLER
STR_Compare ("A", "") -> STR_GREATER
(zum vorletzen Beispiel ist zu bemerken, dass Großbuchstaben
ironischerweise "kleiner" als Kleinbuchstaben sind)//******************
func int STR_ToInt (var string str)
Konvertiert die String-Repräsentation einer Ganzzahl in einen Integer.
Etwa wird "42" in den Integer 42 umgewandelt.
Beispiele für gültige Strings:
1.) 42
2.) +42
3.) -42
Beispiele für ungültige Strings:
1.) ++42
2.) 42+
3.) 4.2
4.) HelloWorld
Im Fehlerfall wird eine Warnung ausgegeben und 0 zurückgegeben.
//******************
func string STR_SubStr (var string str, var int start, var int count)
Gibt den Teilstring von str zurück, der an Index start beginnt und count
Zeichen lang ist.
Beispiele:
STR_SubStr ("Hello World!", 0, 5): "Hello"
STR_SubStr ("Hello World!", 6, 5): "World"
Als Spezialfall davon ist folgende Funktion implementiert:
func string STR_Prefix (var string str, var int count)
STR_Prefix gibt den String zurück, der aus den ersten count Zeichen von
str besteht.
Dies entspricht dem Verhalten von STR_SubStr, mit start == 0.
//######################################
// 5.) Menü-Funktionen
Diese Funktionen sollen den Zugriff auf Menüelemente (zum Beispiel im
Charaktermenü) vereinfachen. Leider werden manche Menüs jedesmal neu
erzeugt (vom Script aus), andere dagegen werden einmal erzeugt und dann
behalten. Problem: Ein Charaktermenü gibt es zum Beispiel erst, nachdem
es das erste mal geöffnet wurde, danach liegt es im Speicher.
Abhängig davon und von dem, was man eigentlich tun will, kann es nötig
sein in den Menüscripten Änderungen einzubringen oder es ist nötig, sich
das Menü als Objekt zu holen und in dem fertigen Objekt selbst
herumzuschmieren. Für letzteres gibt es hier eine Hilfestellung:
func int MEM_GetMenuByString (var string menu)
func int MEM_GetMenuItemByString (var string menuItem)
Liefert die Adressen der des Menüs bzw. des Menüitems falls ein Menü
bzw. Menüitem mit diesem Namen existiert.
//######################################
// 6.) Globale Instanzen initialisieren
Das Scriptpaket führt folgende Instanzen ein:
instance MEM_Game (oCGame);
instance MEM_World (oWorld);
instance MEM_Timer (zCTimer);
instance MEM_WorldTimer (oCWorldTimer);
instance MEM_Vobtree (zCTree);
instance MEM_InfoMan (oCInfoManager);
instance MEM_InformationMan (oCInformationManager);
instance MEM_Waynet (zCWaynet);
instance MEM_Camera (zCCamera);
instance MEM_SkyController (zCSkyController_Outdoor);
instance MEM_SpawnManager (oCSpawnManager);
Die hier benutzten Klassen haben alle eines gemeinsam: Es gibt stehts
maximal ein Objekt von ihnen zur gleichen Zeit (es gibt z.B. nicht
gleichzeitig zwei Welten oder zwei Himmel).
Ich stelle eine Funktion zur Verfügung, die die Offsets dieser Instanzen
auf das entsprechende eindeutige Objekt setzt.
func void MEM_InitGlobalInst()
Nachdem MEM_InitGlobalInst aufgerufen wurde, können alle oben genannten
Instanzen genutzt werden. Nach dem Laden eines neue Spielstandes muss
MEM_InitGlobalInst() erneut aufgerufen werden!
//######################################
// 7.) Ini Zugriff
Ini Dateien haben folgende Form:
//**************************
[mySection1]
myOption1=myValue1
myOption2=myValue2
[mySection2]
myOption1=myValue1
myOption2=myValue2
//**************************
Literale in eckigen Klammern identifizieren Sektionen eindeutig.
Innerhalb von Sektionen werden Optionen mit eindeutigen Namen
identifiziert. Jede Option nimmt einen Wert an, der nach dem "="-Zeichen
steht. Da .ini Dateien keine Binärdateien sind, sind Sektionsnamen,
Optionsnamen und Werte alle vom Typ string.
In diesem Skriptpaket gibt es Funktionen, um lesend und schreibend auf
die Gothic.ini zuzugreifen, sowie um lesend auf die .ini Datei der Mod
zuzugreifen. Zum Lesen gibt es:
func string MEM_GetGothOpt (var string sectionname,
var string optionname)
func string MEM_GetModOpt (var string sectionname,
var string optionname)
MEM_GetGothOpt durchsucht die Gothic.ini, MEM_GetModOpt die .ini Datei
der Mod. Gesucht wird nach der Option optionname in der Sektion
sectionname. Falls eine solche Sektion mit einer solchen Option
existiert, wird der Wert dieser Option zurückgegeben. Ein leerer String
sonst.
Zudem habe ich Funktionen geschrieben, die die Existenz von Sektionen
und Optionen prüfen. Sie sollten selbsterklärend sein:
func int MEM_GothOptSectionExists (var string sectionname)
func int MEM_GothOptExists (var string sectionname,
var string optionname)
func int MEM_ModOptSectionExists (var string sectionname)
func int MEM_ModOptExists (var string sectionname,
var string optionname)
Um schreibend auf die Gothic.ini zuzugreifen, gibt es folgende Funktion:
func void MEM_SetGothOpt (var string section,
var string option,
var string value)
Dabei wird die Option option in der Sektion section auf den Wert value
gesetzt. Falls die Sektion und/oder Option nicht existiert, werden beide
im Zweifelsfall angelegt.
Die .ini Datei der Mod kann leider nicht beschrieben werden, da Gothic
Änderungen daran niemals auf die Festplatte zurückschreibt.
//BEACHTE:
1.) Falls du neue Optionen einführst gebietet der gute Stil, dass du
dies in einer eigenen Sektion tust und die Optionen verständlich
benennst! Als Norm schlage ich vor, dass eine Mod mit Namen "myMod" nur
in der Sektion "MOD_myMod" neue Eigenschaften einführen darf.
2.) Die Gothic.ini wird erst beim Verlassen von Gothic physikalisch
beschrieben. Falls Gothic abstürzt, kann es also sein, dass Änderungen
verloren gehen.
3.) Manche Änderungen werden erst nach einem Neustart von Gothic Wirkung
zeigen.
//Nutzungsmöglichkeiten:
-In der .ini Datei der Mod könnten dem Spieler zusätzliche
Konfigurationsmöglichkeiten gegeben werden, etwa könnte man dort ein
bestimmtes Features abschalten können, falls absehbar ist, dass nicht
jeder Spieler es mögen wird.
-In der Gothic.ini könnte zum Beispiel der Schwierigkeitsgrad der Mod
hinterlegt sein, der sich dadurch auf alle Savegames auswirkt, und nicht
für jedes Savegame neu festgelegt werden muss.
-In der Gothic.ini könnte gespeichert sein, ob die Mod bereits
mindestens einmal durchgespielt wurde. So könnte das zweite mal
Durchspielen verschieden gestaltet werden.
-In der Gothic.ini könnten Highscores (evtl. zusammen mit idComputerName
gehasht zur Verifikation) hinterlegt sein.
-In der Gothic.ini könnten statistische Informationen festgehalten
werden, etwa wie oft ein Spieler (insgesamt oder in der letzten Zeit)
geladen hat. Denkbar wäre ein System, dass den Schwierigkeitsgrad
drosselt, falls der Spieler sehr oft in kurzen Abständen stirbt.
-uvm.
//######################################
// 8.) Tastendrücke erkennen
Eine einfache Funktion ist folgende:
func int MEM_KeyPressed(var int key)
Liefert 1, falls die Taste gedrückt ist, die zu dem virtuellen
Tastencode key gehört. Die Tastencodes sind in Ikarus_Const.d zu finden.
Mit MEM_KeyPressed(KEY_RETURN) kann man zum Beispiel abfragen ob die
ENTER Taste gedrückt ist.
//######################
Häufig wird man in einer Triggerschleife einem Tastendruck auflauern
wollen. Oft möchte man dabei nur einmal auf einen Tastendruck reagieren,
auch wenn der Spieler die Taste für eine bestimmt Zeitspanne festhält.
Dann ist es nötig zu unterscheiden, ob die Taste gerade neu gedrückt
wurde oder bloß noch gehalten wird. Dafür gibt es die Funktion:
func int MEM_KeyState(var int key)
Sie zieht neben der Tatsache, ob eine Taste tatsächlich gedrückt ist
oder nicht, auch noch in Betracht, was das letzte mal für diese Taste
zurückgegeben wurde. Es gibt die folgende Rückgabewerte:
KEY_UP: Die Taste ist nicht gedrückt und war auch vorher nicht gedrückt.
("nicht gedrückt")
KEY_PRESSED: Die Taste ist gedrückt und war vorher nicht gedrückt. ("neu
gedrückt")
KEY_HOLD: Die Taste ist gedrückt und war auch vorher gedrückt. ("immer
noch gedrückt")
KEY_RELEASED: Die Taste ist nicht gedrückt und war vorher gedrückt.
("losgelassen")
KEY_PRESSED oder KEY_RELEASED werden also zurückgeben, wenn sich der
Zustand der Taste seit der letzten Abfrage geändert hat.
KEY_UP oder KEY_HOLD werden zurückgegeben, wenn sich der Zustand nicht
geändert hat.
Beachte: Wenn sich der Tastenzustand zwischen zwei Abfragen zweimal
ändert (zum Beispiel die Taste ganz schnell gedrückt und wieder
losgelassen wird), dann wird MEM_KeyState das nicht bemerken. Die
Funktion kann nur zu den Zeitpunkten an denen sie aufgerufen wird den
Tastenzustand überprüfen.
Beachte auch: Die Funktion wird nie zweimal direkt hintereinander
KEY_PRESSED zurückgeben. Ein Aufruf von MEM_KeyState wird also die
Rückgabewerte von späteren Aufrufen verändern. Folgendes ist zum
Beispiel FALSCH FALSCH FALSCH:
//******* SO NICHT! ********
if (MEM_KeyState (KEY_RETURN) == KEY_UP){Print("Die Taste ist oben!");
} else if (MEM_KeyState (KEY_RETURN) == KEY_PRESSED){Print("Die Taste wurde gerade gedrückt!");
};
//**************************
Der else if Block wird niemals betreten werden. Wenn die Taste gerade
gedrückt wird, wird KEY_PRESSED nur in der ersten if-Abfrage auftauchen
und ist dann "verbraucht". Die zweite if-Abfrage bekommt dann nur noch
KEY_HOLD ab.
So ist es besser:
//**************************
var int returnState;
returnState = MEM_KeyState (KEY_RETURN);
if (returnState == KEY_UP){Print("Die Taste ist oben!");
} else if (returnState == KEY_PRESSED){Print("Die Taste wurde gerade gedrückt!");
};
//**************************
Wenn mehrere Events auf die gleiche Taste hören, konkurrieren sie also
auch um die KEY_PRESSED Rückgabewerte!
//######################
Eine Funktion zum simulieren von Tastendrücken ist:
func void MEM_InsertKeyEvent(var int key)
In manchen Fällen wird die Engine (im nächsten Frame) so reagieren, als
wäre die Taste gedrückt, die mit dem virtuellen Tastencode key in
Verbindung steht. Zum Beispiel öffnet MEM_InsertKeyEvent(KEY_ESC) das
Hauptmenü oder schließt geöffnete Dokumente und
MEM_InsertKeyEvent(KEY_TAB) öffnet das Inventar, falls die Einstellungen
des Spielers TAB als Taste für das Inventar vorsieht.
In anderen Fällen funktioniert diese Funktion nicht, zum Beispiel ist es
nicht möglich das Inventar auf diese Weise zu schließen.
Das klingt nicht nur willkürlich sondern ist auch so. Das Problem ist,
dass die Engine auf verschiedene Arten und Weise abfragen kann ob eine
Taste gedrückt wurde und nur eine dieser Varianten auf
MEM_InsertKeyEvent anspringt. Was zum Beispiel funktioniert hat:
-Inventar öffnen. (TAB)
-Quicksave (F5)
-Charaktermenü öffnen (C)
-Pause togglen / Quickload (F9)
-Tagebuch-Öffnen (L)
-Hauptmenü öffnen / Dokument schließen (ESC)
Beachte: Verschiedene Spieler nutzen verschiedene Tasten für bestimmte
Aktionen! Es ist aber möglich mit MEM_GetGothOpt an die Einstellungen
(Gothic.ini) heranzukommen. Hier sind ein oder zwei Tasten als
Hexadezimalstring (Vorsicht: Little Endian!) für die einzelnen Aktionen
registriert.
//######################################
// 9.) Verschiedenes
func int MEM_SearchVobByName (var string str)
Liefert die Adresse eines zCVobs mit dem Namen str, falls ein solches
Vob existiert. Andernfalls wird 0 zurückgegeben.
Als Abwandlung davon gibt es
func int MEM_SearchAllVobsByName (var string str)
Diese Funktion erzeugt ein zCArray in dem alle Zeiger auf Vobs mit dem
Namen str stehen. Falls kein Vob mit dem Namen existiert wird ein leeres
zCArray erzeugt. Ein Zeiger auf das erzeugte zCArray wird dann
zurückgegeben. Dieses kann ausgewertet werden, sollte aber noch vor Ende
des Frames (bevor der Spieler Laden kann) wieder mit MEM_ArrayFree
freigegeben werden um Speicherlecks zu vermeiden.
Die Klasse zCArray ist in Misc.d zu finden.
//******************
func int Hlp_Is_oCMob(var int ptr)
func int Hlp_Is_oCMobInter(var int ptr)
func int Hlp_Is_oCMobLockable(var int ptr)
func int Hlp_Is_oCMobContainer(var int ptr)
func int Hlp_Is_oCMobDoor(var int ptr)
func int Hlp_Is_oCNpc(var int ptr)
func int Hlp_Is_oCItem(var int ptr)
func int Hlp_Is_zCMover(var int ptr)
func int Hlp_Is_oCMobFire(var int ptr)
Diese Funktionen können u.a. nützlich sein, wenn es darum geht den Fokus
des Helden auszuwerten. Die Funktionen geben 1 zurück, falls der
übergebene Zeiger auf ein Objekt der angegebenen Klasse oder eine
Unterklasse dieser Klasse zeigt.
Für einen Stuhl würden zum Beispiel Hlp_Is_oCMob und Hlp_Is_oCMobInter 1
zurückgeben, die anderen Funktionen 0.
Natürlich kann man diese Funktionen noch für andere Objekttypen als Mobs
schreiben, wenn das nötig ist.
//******************
func void MEM_SetShowDebug (var int on)
Setzt die Variable, die auch von "toggle debug" getoggelt wird. Dadurch
landen mit PrintDebug ausgegebenen Meldungen im Spy (wenn dort
Informationen geloggt werden). Es empfielt sich als Filter "Skript" im
Spy einzustellen, sonst gehen die Meldungen unter einem Haufen nutzlosem
Enginezeug unter.
//******************
func string MEM_GetCommandLine ()
Gibt den Inhalt der Kommandozeile zurück, die an Gothic Übergeben wurde.
Diese könnte zum Beispiel so aussehen:
"-TIME:7:35 -GAME:TEST_IKARUS.INI -ZREPARSE -ZWINDOW -ZLOG:5,S -DEVMODE
-ZMAXFRAMERATE:30"//******************
func int MEM_ArrayCreate ()
func void MEM_ArrayFree (var int zCArray_ptr)
func void MEM_ArrayClear (var int zCArray_ptr)
func void MEM_ArrayInsert (var int zCArray_ptr, var int value)
func void MEM_ArrayRemoveIndex (var int zCArray_ptr, var int index)
func void MEM_ArrayRemoveValue (var int zCArray_ptr, var int value)
func void MEM_ArrayRemoveValueOnce (var int zCArray_ptr, var int value)
zCArrays werden an vielen Stellen von der Engine verwendet und ist eine
sehr einfache Feld-Datenstruktur.
Manchmal möchte man in diese Strukturen etwas einfügen oder etwas daraus
entfernen. Das Einfügen ist nicht einfach, da eventuell der Platz im
Array nicht ausreicht und vergrößert werden muss. Daher habe ich diese
Funktion zur Verfügung gestellt. Die anderen Funktionen sind sozusagen
Beiwerk und leisten einfachere Dinge.
zCArray_ptr ist in allen Funktionen ein Zeiger auf ein zCArray (siehe
Misc.d).
MEM_ArrayCreate: Erzeuge ein leeres zCArray und gebe seine Adresse
zurück.
MEM_ArrayFree: Gebe sowohl das zCArray als auch seine Daten frei.
MEM_ArrayClear: Gibt die Daten des zCArray frei. Es wird zu einem
leeren Array.
MEM_ArrayInsert: Fügt value ans Ende des Arrays an. Das Array wird
automatisch vergrößert falls es zu klein ist.
Entfernen von Elementen passiert durch auffüllen der Lücke mit dem
letzten Element. Wird zum Beispiel aus einem Array (1,2,3,4,5) die 3
entfernt, wird das Ergebnis so aussehen: (1,2,5,4). Die 5 ist in die
entstehende Lücke gewandert.
MEM_ArrayRemoveIndex: Entfernt das Element an Position index.
MEM_ArrayRemoveValue: Sucht alle Vorkommen von value im Array und
entfernt diese.
MEM_ArrayRemoveValueOnce: Sucht das erste Vorkommen von value im Array
und entfernt dies. Wird value nicht gefunden wird eine Warnung
ausgegeben.
//Hinweis:
Diese Funktionen erwarten Pointer auf ein zCArray! Nicht verwechseln mit
der Adresse des ersten Datenelements oder dem Symbolindex irgendeines
"var zCArray"! Manchmal wird man sich die Adresse eines zCArrays erst
ausrechnen müssen und zwar aus der Adresse des Objekts, dass dieses
zCArray beinhaltet und dem Offset des zCArrays in der entsprechenden
Klasse (Bytes zählen!).
//Vorsicht:
Beim Zerstören der Spielsession werden mit MEM_ArrayCreate angelegte
Arrays nicht freigegeben (-> Speicherleck), sind aber spätestens beim
Beenden des Spiels zerstört und nicht im Savegame (-> ungeeignet um
Storyinformationen zu speichern).
//Einsatzmöglichkeit:
Arrays dieser Art können sinnvoll zur Übergabe von großen Datenmengen
sein. Zum Beispiel nutzt MEM_SearchAllVobsByName Arrays um gleich eine
große Menge von Vobs zurückzugeben. Dabei wird verlangt, dass der Nutzer
das Array noch im selben Frame (bevor der Spieler Laden oder Speichern
könnte) wieder freigibt (mit MEM_ArrayFree).
//******************
func void MEM_CopyBytes (var int src, var int dst, var int byteCount)
func void MEM_CopyWords (var int src, var int dst, var int wordcount){
Kopiert entsprechend viele Bytes bzw. Worte beginnend von Speicherstelle
src ans Ziel beginnend an Speicherstelle dst. Ein Wort ist 4 Byte groß,
was der Größe der meisten primitiven Typen entspricht. MEM_CopyWords
kopiert also einfach wordcount * 4 bytes.
Wichtig: Die Speicherbereiche dürfen sich nicht überlappen, das heißt am
Beispiel von MEM_CopyBytes: Die Intervalle [src, src + byteCount - 1]
und [dst, dst + byteCount - 1] müssen disjunkt sein.
//######################################
// 10.) Obskure Funktionen
Einige der internen Zwischenschritte könnten auch für andere nützlich
sein. Ich möchte ein paar hier für Interessierte kurz auflisten. Die
meisten, die diese Paket benutzen, werden sie nicht brauchen.
//******************
func int MEM_GetBufferCRC32 (var int buf, var int buflen)
Berechnet einen Hashwert aus einem Bytearray, dass an buf beginnt und
Länge buflen hat. Es wird die selbe Hashfunktion verwendet wie in Gothic.
func int MEM_GetStringHash (var string str){
Berechnet den Hashwert eines Strings. Es wird die selbe Hashfunktion
verwendet wie in Gothic.
Bemerkung: Diese Funktion wird von MEM_SearchVobByName benutzt.
//******************
func int MEM_Alloc (var int amount)
Mit MEM_Alloc werden amount Byte Speicher alloziert und ein Zeiger auf
den Speicherbereich zurückgegeben.
Gothic hält keine Referenz auf diesen Speicherbereich und kann ihn auch
nicht freigeben (auch nicht beim Zerstören der Session!).
Speicher sollte daher nur dann reserviert werden, wenn er garantiert vor
dem Laden eines Spielstands wieder mit MEM_Free freigegeben werden kann
oder garantiert ist, dass Gothic von diesem Speicherbereich weiß und ihn
selbstständig freigibt.
Vielleicht kann man mit dieser Funktion neue Objekte erzeugen und
dauerhaft in die Objektstruktur von Gothic einbauen. Das Bedarf aber
großer Vorsicht, da die Objektkonstrukturen nicht genutzt werden können.
Man müsste alles von Hand machen.
Sehr gut geeignet dürfte diese Funktion sein um Kleinigkeiten wie
Listenelemente zu bauen und in vorhandenen Listen zu integrieren. Der
neu allozierte Speicher ist stets genullt.
func int MEM_Realloc (var int oldptr, var int oldsize, var int newsize)
Alloziert einen Speicherbereich der größe newsize und gibt einen Zeiger
auf diesen Speicherbereich zurück.
Der Speicherbereich ab Stelle oldptr wird freigegeben.
Falls newsize >= oldsize werden die ersten oldsize Bytes aus dem alten
Speicherbereich in den neuen übernommen. Der zusätzliche Speicher ist
mit Null initialisiert.
Falls newsize <= oldsize werden alle Bytes des neuen Speicherbereichs
mit den entsprechenden Werten des alten Speicherbereichs initialisiert.
Diese Funktion ist dazu gedacht um einen allozierten Speicherbereich zu
vergrößern oder zu verkleinern. Vorhandene Daten bleiben auf natürliche
Art und Weise erhalten.
func void MEM_Free (var int ptr)
Gibt einen allozierten Speicherbereich wieder frei.
Vielleicht kann man so auch Engine-Objekte zerstören. Auch hier ist
große Vorsicht angesagt, da keine Destruktoren aufgerufen werden!
Kleinigkeiten wie Listenelemente können so aber problemlos freigegeben
werden.
//******************
func void MEM_SetParser(var int parserID)
Hiermit lässt sich der aktuelle Parser auf etwas anderes als den
Content-Parser umstellen. Dies ist nötig, wenn man zum Beispiel mit
Symbolen im Menü suchen und bearbeiten will. Kommunikation mit den
Menüscripten wird dadurch möglich. Aber leider ist das ganze nicht ohne
Tücken und vermutlich eher uninteressant, weil die Menüscripte selten
von Gothic aufgerufen werden. Bestimmte Menüobjekte bleiben über die
Session hinaus erhalten.
//######################################
// VI. Gefahren
//######################################
Mit diesem Scriptpaket kann man Gothic auf nie dagewesene Art und Weise
zum Absturz bringen. Wenn es sonst oft so ist, dass bei einem Bug eine
Quest einfach nicht läuft oder eine Option nicht erscheint, hat man
jetzt die Möglichkeit Fehler so einzubauen, dass Gothic den Spielern
zuverlässig um die Ohren fliegt. "Die Anwendung hat ein Problem
festgestellt und muss beendet werden", "Access Violation", "Assertion
failed" gehören zum Alltag beim Debugging, wenn man Features
implementiert, die nicht ganz trivial sind. Natürlich kann man solche
Fehler beheben und wenn man konzentriert und planvoll arbeitet, wird man
schon was vernünftiges zu Stand bringen. Aber frustresistenz sollte man
mitbringen.
Kurz gesagt: Vorsicht! Aber keine Panik, wenn mal was schiefgeht: Mit
guten Debugmeldungen (nicht auf dem Bildschirm, sondern mit PrintDebug
in den Spy!) kann man alles beheben. (ich habe mittlerweile sogar ein
Skript geschrieben, dass ständig einen Skriptseitigen Callstack im zSpy
ausgibt und so hilft herauszufinden wo genau Gothic abstürzt, falls es
innerhalb des Skripte passiert, siehe Thread im Forum)//######################################
// VII. Beispiele
//######################################
//--------------------------------------
// 1.) Funktion zum öffnen der Truhe im Fokus:
func void OpenFocussedChestOrDoor(){
var oCNpc her;
her = Hlp_GetNpc(hero);
//Gar kein Fokusvob?
if (!her.focus_vob){Print("Kein Fokus!");
return;
};
//Fokusvob kein verschließbares Vob?
if (!Hlp_Is_oCMobLockable(her.focus_vob)){Print("Keine Truhe oder Tür im Fokus!");
return;
};
var oCMobLockable Lockable;
MEM_AssignInst (Lockable, her.focus_vob);
if (Lockable.bitfield & oCMobLockable_bitfield_locked){
Lockable.bitfield = Lockable.bitfield & ~
oCMobLockable_bitfield_locked;
Print(ConcatStrings("Folgendes Vob geöffnet: ",
Lockable._zCObject_objectName));
} else {Print(ConcatStrings(
Lockable._zCObject_objectName,
" war gar nicht abgeschlossen!"));
};
};
//--------------------------------------
// 2.) Kameraposition ermitteln:
func void PrintCameraPos(){/* Globale Instanzen (die es nur einmal gibt) initialisieren: *//* Initialisiert MEM_World, MEM_Game, etc. u.a. auch MEM_Camera */
MEM_InitGlobalInst();
/* Das Kameraobjekt ist kein vob (sondern was abstraktes),
* weiß nicht wo und wie da Positionsdaten stehen.
Ich arbeite lieber auf dem Kameravob: */
var zCVob camVob;
MEM_AssignInst (camVob, MEM_Camera.connectedVob);
/*Hier muss man wissen wie die Transformationsmatrix aufgebaut ist:
Sie besteht aus drei Vektoren, die x, y und z Richtung
des lokalen Koordinatensystem des Kameravobs
in Weltkoordinaten angeben (dabei müsste z die
Blickrichtung sein). Ich habe diese Vektoren hier
mit v1, v2, v3 Bezeichnet.
Zusätzlich gibt es in der 4. Spalte die Translation,
das heißt die Position der Kamera.
v1_x v2_x v3_x x
v1_y v2_y v3_y y
v1_z v3_z v3_z z
0 0 0 0
Die Matrix ist Zeilenweise im Speicher abgelegt.
Da wir uns für die letzte Spalte interessieren sind die Indizes
im trafoWorld Array 3, 7 und 11, die wir brauchen.
*/Print(ConcatStrings("x: ",
IntToString(roundf(camVob.trafoObjToWorld[ 3]))));
Print(ConcatStrings("y: ",
IntToString(roundf(camVob.trafoObjToWorld[ 7]))));
Print(ConcatStrings("z: ",
IntToString(roundf(camVob.trafoObjToWorld[11]))));
};
//--------------------------------------
// 3.) Regen starten
func void StartRain(){/* Globale Instanzen initialisieren: */
MEM_InitGlobalInst(); /* Hierrunter fällt auch der Skycontroller *//* man könnte sich jetzt hier was besseres überlegen,
* aber ich machs mal so: *//* start am Anfang vom Tag */
MEM_SkyController.rainFX_timeStartRain = 0; //FLOATNULL;
/* ende am Ende vom Tag */
MEM_SkyController.rainFX_timeStopRain = 1065353216; //FLOATEINS;
/* Bemerkung dazu: Die Start und Endzeiten sind Gleitkommazahlen.
* 0 steht für den Anfang des Tages 1 für das Ende des Tages.
* Zum Aufbau des Gleitkommaformats google man nach IEEE 745.
* Ich habe mal floats in Daedalus implementiert:
* http://forum.worldofplayers.de/forum/showthread.php?t=500080
* Diese Implementierung kann man nutzen um sich
* floats aus Ganzzahlen bauen zu lassen. *//* Ergebnis: Ganzer Tag regen! (es sei denn man ist in einer Zone
* in der es schneit, dann den ganzen Tag Schnee) */};
//--------------------------------------
// 4.) Geschachtelte Schleife
/* Soll alle Paare (x,y) aufzählen mit
0 <= x < max_x,
0 <= j < max_y
*/
func void printpairs(var int max_x, var int max_y){/* System initialisieren */
MEM_InitLabels();
/* PrintDebug soll benutzt werden, also Debugausgabe aktivieren */
MEM_SetShowDebug (1);
var int x; var int y;
x = 0;
/* while (x < max_x) */
var int x_loop; x_loop = MEM_StackPos.position;
if (x < max_x){
y = 0;
/* while (y < max_y) */
var int y_loop; y_loop = MEM_StackPos.position;
if (y < max_y){
var string out; out = "(";
out = ConcatStrings(out, IntToString(x));
out = ConcatStrings(out, ", ");
out = ConcatStrings(out, IntToString(y));
out = ConcatStrings(out, ")");
PrintDebug(out);
y += 1;
/* continue y_loop */
MEM_StackPos.position = y_loop;
};
x += 1;
/* continue x_loop */
MEM_StackPos.position = x_loop;
};
};
/* Ausgabe eines Aufrufs printpairs (4,2) wäre dann:
00:36 Info: 5 U: Skript: (0, 0) .... <zError.cpp,#465>
00:36 Info: 5 U: Skript: (0, 1) .... <zError.cpp,#465>
00:36 Info: 5 U: Skript: (1, 0) .... <zError.cpp,#465>
00:36 Info: 5 U: Skript: (1, 1) .... <zError.cpp,#465>
00:36 Info: 5 U: Skript: (2, 0) .... <zError.cpp,#465>
00:36 Info: 5 U: Skript: (2, 1) .... <zError.cpp,#465>
00:36 Info: 5 U: Skript: (3, 0) .... <zError.cpp,#465>
00:36 Info: 5 U: Skript: (3, 1) .... <zError.cpp,#465>
*///--------------------------------------
// 5.) Aufrufen einer Funktion
// anhand ihres Namens.
/* Dieses Beispiel zeigt nicht, weshalb MEM_CallByString
* praktisch ist, aber wie man die Funktion benutzt. */
var zCVob someObject;
func int MyFunction(var int param1, var string str1,
var int param2, var string str2){Print(ConcatStrings(str1, str2)); //(*)
return 100 * param1 + param2;
};
func void foo(){
var int result;
/* Der Code zwischen A und B ist in diesem Fall
* äquivalent zu:
* result = MyFunction (42, "Hello ", 23, "World!");
* *//* A */
MEM_PushIntParam (42);
MEM_PushStringParam ("Hello ");
MEM_PushIntParam (23);
MEM_PushStringParam ("World!");
MEM_CallByString ("MYFUNCTION");
result = MEM_PopIntResult();
/* B */Print(IntToString(result)); //(**)
};
/* Ausgegeben wird "Hello World" (das macht MyFunction bei (*))
* sowie "4223" (das macht foo bei (**)). *//* Anmerkung: Da Symbolindizes fortlaufend sind
* und der Symbolindex von someObject einfach durch
* someObject selbst gegeben ist, könnte
* MEM_CallByString ("MYFUNCTION");
* hier auch ersetzt werden durch
* MEM_CallByID (someObject + 1); */
Das Scriptpaket heißt nicht umsonst Ikarus:
Man kann die Grenzen von Daedalus hinter sich lassen, aber dabei auch auf die Schnauze fallen. Die sonst sehr sorgsame Kontrolle der Skriptparameter durch die Engine gibt es nicht mehr. Wer Mist macht, bekommt keine zSpy-Warnung, sondern landet auf dem Desktop mit Access Violations, fehlgeschlagenen Assertions und/oder "die Anwendung hat ein Problem festgestellt und muss beendet werden" Meldungen.
Wer etwas umsetzen will und sich nicht sicher ist, ob es mit Ikarus überhaupt geht und falls ja, ob es vielleicht höchst unbequem zu implementieren sein wird, der sei auch an G2Ext verwiesen. Eventuell ist das konkrete Feature leichter, eleganter oder besser damit umzusetzen. Vielleicht auch nicht.
Edit: 23.03.10, 18 Uhr: Neue Version, die IntToString benutzt anstatt mein Kürzel i2s.
Edit: 23.03.10, 19:30 Uhr: Fix in STR_GetCharAt. Die Funktion hat nicht funktioniert. Außerdem Umwandlung von Tabs in Leerzeichen (für Leute mit anderer Tabsize).
Edit: 02.05.10, 22 Uhr: Diverse Fixes am Code und Verbesserungen an der Dokumentation.
Edit: 24.05.10, 17 Uhr: MEM_Realloc und Sprünge ergänzt sowie ein Bugfix und Dokuupdate.
Edit: 13.06.10, 23 Uhr:
CallByString und CallByID Funktionalität ergänzt durch Funktionen, die Parameter pushen und Rückgabewerte popen. Beispiel 5 in der Dokumentation beschäftigt sich damit.
Alte CallByString und CallByID Derivate herausgenommen (ich hoffe die hat noch keiner benutzt).
In zCParser.d ein paar genauere Kommentare zu den Parsertokens eingefügt.
oCMag_Book Klasse hinzugefügt in Misc.d
Edit: 20.06.10, 1 Uhr:
zCOption dokumentiert und Zugriffsmöglichkeit auf die .ini Dateien geschaffen.
MEM_ArrayInsert hinzugefügt (Nebenprodukt des .ini Zugriffs)
Möglichkeit die Kommandozeilenparameter auszulesen
entsprechendes Dokuupdate, rewrite dieses Posts
Edit: 25.06.10, 16 Uhr: Konstanten in zCTrigger.d korrigiert.
Edit: 27.06.10, 11 Uhr: Die Funktion MEM_KeyState hinzugefügt.
Edit: 04.08.10, 17 Uhr:
Reimplementierung des Ikarus Kerns und vieler Funktionen (auf elegantere Weise, dadurch auch kompakter)
Auslagerung aller Konstanten (vorwiegend Adressen) in zugehörige Klassendateien bzw. in eine neue Datei Ikarus_Const.d.
Ikarus.d selbst sollte damit unabhängig von der Gothic-Version sein und um Ikarus Gothic 1 tauglich zu machen müssen nur Ikarus_Const.d und evtl. die Klassendateien angepasst werden.
diverse Bugfixes
Konstanten für virtuelle Tastencodes in großem Umfang hinzugefügt (zu finden in Ikarus_Const.d).
Einige zCArray Funktionen (zum Beispiel um in Dingen wie der activeVobList herumzuschmieren oder als temporärer, skriptinterner Speicher).
Neu: MEM_InsertKeyEvent zum fingieren von Tastendrücken (funktioniert nur begrenzt)
Neu: MEM_CopyBytes / MEM_CopyWords zum Kopieren einer vorgegebenen Datenmenge von einer Quelle zu einem Ziel.
Neu: MEM_SearchAllVobsByName: Baut ein zCArray mit Zeigern auf sämtliche Vobs mit bestimmten Namen und liefert Zeiger auf das zCArray zurück.
Neu: STR_SubStr und STR_Prefix zum Abgreifen von Teilstrings.
Besten Dank, gerade gestern habe ich mir vorgenommen das Scriptpaket sobald es erscheint zu nutzen.
Einziges Problem: Ich bräuchte es wohl für G1. Das heißt es sind wohl Sachen dabei, die es in G1 nicht gibt und soziemlich alle Adressen dürften falsch sein.
Naja, trotzdem danke, ich werde es mir trotzdem am WE mal genauer angucken.
Oparilames nachdem er seinen Gesellenbrief erhalten hat:
»Das war's mit dir, du Mistvieh!«
Coole Sache, muss ich mir dann gleich mal ansehen, vor allem Regen, Marvin, allgemein Tastenkontrolle (müssten ja auch neue Hotkeys klappen) und evtl. SpawnManager, muss ich mir mal angucken Danke jedenfalls schon mal dafür
Besten Dank, gerade gestern habe ich mir vorgenommen das Scriptpaket sobald es erscheint zu nutzen.
Einziges Problem: Ich bräuchte es wohl für G1. Das heißt es sind wohl Sachen dabei, die es in G1 nicht gibt und soziemlich alle Adressen dürften falsch sein.
Ja. Wenn du Glück hast haben sich die Klassen nicht sehr verändert. Aber zumindest die Offsets wirst du korrigieren müssen.
Ob es eine brauchbare Gothic 1 Version mit Debuginformationen gibt, weiß ich nicht, vielleicht kann Nico helfen.
Die Offsets, die ich nutze sind:
Code:
/* Addressen der Parserinstanzen oder Pointer auf Parserinstanzen
Zumindest der Pointer auf den Contentparser ist sehr praktisch */
const int ContentParserAddress = 11223232; //0xAB40C0;
const int vfxParserPointerAddress = 9234156; //0x8CE6EC
const int menuParserPointerAddress = 9248360; //0x8D1E68
const int pfxParserPointerAddress = 9278004; //0x8D9234
/* Menüliste: Manche einmal gebauten Menüs liegen hier: */
const int MEMINT_MenuListOffset = 9248324;
/* Liste von Menüitems: Einmal erzeugte Menüitems liegen hier */
const int MEMINT_MenuItemArrayAddres = 9248508;
/* Pointer auf Spiel und Timer. Zumindest das Spiel ist sehr wichtig
dort kann man die Adressen vieler anderer Objekte nachlesen */
const int MEMINT_oGame_Pointer_Address = 11208836; //0xAB0884
const int MEMINT_zTimer_Address = 10073044; //0x99B3D4
/* Adresse der Hashtabelle um Vobs darin zu finden: */
const int MEMINT_crc_table_offset = 8598048; //0x833220
/* Adresse des Speicherbereichs indem steht, welche Tasten gedrückt sind */
const int MEMINT_KeyEvent_Offset = 9246328; //0x6D1678
/* Zum Spiel pausieren */
const int game_holdTime_Address = 11208840; //0xAB0888 //zBOOL*
Mit dem Pointer auf das globale Game und auf den Contentparser könnte man schon sehr viel machen. Der Rest ist eher nebensächlich.
Wenn man die Adressen nicht direkt rausbekommt, könnte man schauen ob man indirekt über bekannte Referenzen weiterkommt.
Jeder Npc hat eine zCAIPlayer, die hat einen Pointer auf die Welt und die Welt hat einen Pointer auf die Session (das Game). Jetzt bräuchte man noch einen Pointer auf den Helden. Den kriegt man vielleicht mit einer Umkehrung von Nicos Parser Stack Hacking. Habe mich noch nicht ausreichend damit beschäftigt, um was darüber sagen zu können.
Naja, vielleicht erbarmt sich auch ein Assembler-Gott und findet die Referenzen mit der Executable heraus.
Coole Sache, muss ich mir dann gleich mal ansehen, vor allem Regen, Marvin, allgemein Tastenkontrolle (müssten ja auch neue Hotkeys klappen) und evtl. SpawnManager, muss ich mir mal angucken Danke jedenfalls schon mal dafür
Wenn allgemein nutzbare Funktionen (die nicht zu speziell sind) oder neue Ideen wozu man irgendetwas gebrauchen könnte entstehen, könnte man die vielleicht sammeln, damit nicht jeder das Rad neu erfinden muss.
Wirklich schöne Sache. Gerade vor ein paar Tagen erst habe ich mich noch mit künstlichem Regen herumgeschlagen, jetzt ist es hier direkt als Beispiel mit drin.
Jetzt eine Frage: Wo kommen FLOATNULL und FLOATEINS her? Was genau bedeuten sie als Uhrzeiten? (um die Zeiten anpassen zu können; 0 = 0 Uhr, 1 = 24 Uhr, 0.5 = 12 Uhr ? )
Ich nehme an, dass ich an die Funktion auch Parameter übergeben kann, wenn ich sie dahingehend abändere?
Wie kann ich den Regen wieder stoppen? Regnet es nach einmaligem Aufruf von StartRain (); jeden Tag oder nur den ganzen Aufruf-Tag?
Gut gefällt mir auch der Vorschlag, "Erfindungen" hier zu teilen.
Direkt ein Fehler:
IKARUS.D: Undefined function : I2S ( line 1058 )
Die Engine-Klassen werden vorher geparst, allerdings gibt es in der Ikarus.d auch so schon einige FAULT und WARN Meldungen.
Code:
00:00 Info: 10 B: zDSK: Opened file IKARUS.D .... <zDisk.cpp,#689
00:00 Info: 10 B: zDSK: Closed file IKARUS.D .... <zDisk.cpp,#632
00:00 Info: 5 U: PAR: CONTENT\STEINZEIT\IKARUS.D : Parse... .... <zError.cpp,#360
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Unhandled expression: EXP_STRING ( line 162 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Cannot convert from type INSTANCE to INT ( line 357 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Cannot convert from type INSTANCE to INT ( line 402 ) .... <zError.cpp,#366
00:00 Warn: 0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Possible error, function should return value ( line 466 ) .... <zParser.cpp,#2700
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Possible error, function should return value ( line 593 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Possible error, function should return value ( line 658 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 765 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Possible error, function should return value ( line 775 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 812 ) .... <zError.cpp,#366
00:00 Warn: 0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Possible error, function should return value ( line 826 ) .... <zParser.cpp,#2700
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 869 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 872 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 875 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 878 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 881 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 884 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 888 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 892 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 895 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 898 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Comparison is possible only in 'if' expression ( line 968 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Comparison is possible only in 'if' expression ( line 979 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Comparison is possible only in 'if' expression ( line 992 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Comparison is possible only in 'if' expression ( line 1001 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Comparison is possible only in 'if' expression ( line 1010 ) .... <zError.cpp,#366
00:00 Fatal:-1 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Undefined function : I2S ( line 1058 ) .... <zParser.cpp,#849
00:00 --------------
EDIT: Ich sehe gerade Deine Editierung, allerdings verweist der Download-Link noch immer auf das alte Paket.
Jetzt eine Frage: Wo kommen FLOATNULL und FLOATEINS her? Was genau bedeuten sie als Uhrzeiten? (um die Zeiten anpassen zu können; 0 = 0 Uhr, 1 = 24 Uhr, 0.5 = 12 Uhr ? )
Ich nehme an, dass ich an die Funktion auch Parameter übergeben kann, wenn ich sie dahingehend abändere?
Wie kann ich den Regen wieder stoppen? Regnet es nach einmaligem Aufruf von StartRain (); jeden Tag oder nur den ganzen Aufruf-Tag?
Gut gefällt mir auch der Vorschlag, "Erfindungen" hier zu teilen.
Mit den Floatwerten interessiert mich auch, vermute es ist so, wie du vemutest, nur werden ja da nur Int-Werte akzeptiert Allerdings kannst du ja die Zeit abfragen und dann Starten und wenn es aufhören soll manuell wieder beenden. Hab's noch nicht getestet, aber vermutlich die End-Zeit auch auf FLOATNULL setzen.
Finde die Idee mit dem Teilen auch gut.
Und Tasten abfragen klappt schon mal wunderbar, jetzt steht neuen Hotkeys nichts mehr im Wege
@Sekti: Du hast bei den drei Spawnmanager-Konstanten geschrieben, dass die sehr interessant ist. Allerdings sind das doch nur die Adressen, oder? Wäre gut, wenn man mit denen was machen könnte, sind ja für die KI-Glocke wichtig, hat Zerxes ja mal erwähnt
EDIT: Hab jetzt mal den Regen testen wollen, aber da tut sich gar nichts, auch Sonne abstellen ging nicht Dafür klappt schonmal das Anzeigen des Waynets
Naja, vielleicht erbarmt sich auch ein Assembler-Gott und findet die Referenzen mit der Executable heraus.
Das kann jeder selbst herausfinden
Hier ein paar Hinweise, um eine gute Analyse der GothicMod.exe/Gothic2.exe zu bekommen (gilt auch für die freie Version von IDA):
Vorbereitungen:
Unter Vista oder Windows 7 IDA möglichst nicht ins "Programme"-Verzeichnis installieren (vermeidet Ärger mit der Benutzerkontensteuerung).
Die angehängten idauser.cfg und idauserg.cfg nach <IDA>\cfg entpacken (damit man diverse Einstellungen nicht immer wieder neu vornehmen muss).
Die <IDA>\loaders\dbg.ldw umbenennen (zu Beispiel: !dbg.ldw). Der DBG-Loader soll von IDA nicht mehr automatisch zum Einlesen der CodeView-Informationen aufgerufen werden, da das PDB-Plugin bessere Ergebnisse liefert.
Analyse:
IDA starten
New
New disassembly database / Windows / PE Executable
<GOTHIC>\System\GothicMod.exe oder <GOTHIC>\System\Gothic2.exe auswählen
Next >
Next >
File loading / Start analysis now deaktivieren!
Finish
Generating list of strings kann abgebrochen werden
View / Open subviews / Signatures (Shift+F5) alle Einträge (vc32rtf) markieren und entfernen (Del)
Bis auf IDA View-A alle Views schließen (Performance)
File / Load File / PDB file...
...warten bis PDB: total <x> symbols loaded im Log auftaucht
Options / General... / Analysis / Reanalyze program
Danach sollte man seine Arbeit speichern und eine Kopie sichern - in IDA gibt es kein "zurück" und diverse Aktionen lassen sich kaum bis gar nicht wieder rückgängig machen.
"Unter diesen schwierigen Umständen bin ich mir sicher, daß diese guten Menschen meinen augenblicklichen Bedarf an deren Gold verstehen werden." -- Connor
Jetzt eine Frage: Wo kommen FLOATNULL und FLOATEINS her? Was genau bedeuten sie als Uhrzeiten? (um die Zeiten anpassen zu können; 0 = 0 Uhr, 1 = 24 Uhr, 0.5 = 12 Uhr ? )
Ich nehme an, dass ich an die Funktion auch Parameter übergeben kann, wenn ich sie dahingehend abändere?
Hoppla, das hätte ich sagen sollen. Es handelt sich hier um Floats (Gleitkommazahlen). Eine 0 steht für den Anfang vom Tag eine 1 steht für das Ende vom Tag. Da Floats ein anderes Format haben, sieht die Float-Eins als Integer etwas komisch aus. Es ist 3F800000 oder dezimal 1065353216. Ich habe mal Floats in Daedalus aufbauend auf Integern implementiert. Aus diesem Script stammen auch die Konstanten.
Wie kann ich den Regen wieder stoppen? Regnet es nach einmaligem Aufruf von StartRain (); jeden Tag oder nur den ganzen Aufruf-Tag?
Gothic ist hier sehr primitiv: Beim Tageswechsel wird Startzeit und Endzeit des Regens für den nächsten Tag gewürfelt. Das heißt jeden Tag regnet es einmal. Ein Aufruf meiner Funktion betrifft daher nur den aktuellen Tag.
Im Prinzip sagt der Aufruf:
Start vom Regen ist 0:00 Uhr. Ende vom Regen ist 23:59.
Der SkyController merkt dann, dass es zwischen Start und Endzeit ist, und rendert entsprechend den Regen.
Den Regen kannst du stoppen indem du das Ende der Regenzeit passend wählst. Wenn du nicht all zu harte Übergänge haben willst, solltest du berücksichtigen, dass die ersten und letzten 20% der Regenzeit (also zwischen Anfang und Ende) zum Überblenden genutzt werden.
Direkt ein Fehler:
IKARUS.D: Undefined function : I2S ( line 1058 )
[...]EDIT: Ich sehe gerade Deine Editierung, allerdings verweist der Download-Link noch immer auf das alte Paket.
Danke, hab eine alte Datei ins Archiv gepackt. Sollte jetzt stimmen.
Die Engine-Klassen werden vorher geparst, allerdings gibt es in der Ikarus.d auch so schon einige FAULT und WARN Meldungen.
Code:
00:00 Info: 10 B: zDSK: Opened file IKARUS.D .... <zDisk.cpp,#689
00:00 Info: 10 B: zDSK: Closed file IKARUS.D .... <zDisk.cpp,#632
00:00 Info: 5 U: PAR: CONTENT\STEINZEIT\IKARUS.D : Parse... .... <zError.cpp,#360
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Unhandled expression: EXP_STRING ( line 162 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Cannot convert from type INSTANCE to INT ( line 357 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Cannot convert from type INSTANCE to INT ( line 402 ) .... <zError.cpp,#366
00:00 Warn: 0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Possible error, function should return value ( line 466 ) .... <zParser.cpp,#2700
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Possible error, function should return value ( line 593 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Possible error, function should return value ( line 658 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 765 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Possible error, function should return value ( line 775 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 812 ) .... <zError.cpp,#366
00:00 Warn: 0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Possible error, function should return value ( line 826 ) .... <zParser.cpp,#2700
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 869 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 872 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 875 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 878 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 881 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 884 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 888 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 892 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 895 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 898 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Comparison is possible only in 'if' expression ( line 968 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Comparison is possible only in 'if' expression ( line 979 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Comparison is possible only in 'if' expression ( line 992 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Comparison is possible only in 'if' expression ( line 1001 ) .... <zError.cpp,#366
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Comparison is possible only in 'if' expression ( line 1010 ) .... <zError.cpp,#366
00:00 Fatal:-1 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Undefined function : I2S ( line 1058 ) .... <zParser.cpp,#849
00:00 --------------
Erstmal danke dafür. Ich parse mit Gothic, kriege also keine Warnmeldungen. In der Tat war ein grober Fehler in STR_GetCharAt, auf den ich durch die Warnung gestoßen wurde. Hab ihn korrigiert. Das mit i2s ist ja geklärt, was den Rest angeht:
Code:
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Comparison is possible only in 'if' expression ( line 992 ) .... <zError.cpp,#366
Das ist Unfug. Natürlich darf ich die Operatoren || in Rechnungen benutzen.^^
Selbst ">" und "<" muss zugelassen sein. Immerhin sind das Operationen auf Ganzzahlen.
Aber ich habe mal ein "|" draus gemacht, dann sollte auch der GothicSourcer es schlucken. Und in diesem Fall ist es für das Ergebnis egal, welcher Operator genutzt wird.
00:00 Fault:0 U: PAR: CONTENT\STEINZEIT\IKARUS.D: Function 'MEMINT_ASSIGNCONTENTINST': Argument 1: Cannot convert from type INSTANCE to INT ( line 884 ) .... <zError.cpp,#366
Die Feststellung, dass ich syntaktisch nicht zwischen instance und int unterscheide ist zwar richtig, aber das das kann ich beim besten Willen nicht korrigieren. Du musst dem GothicSourcer beibringen, das zu ignorieren. Da gibts ja irgendeine Datei in der man das einstellen kann, wenn ichs richtig weiß.
Der Rest war eine Stilfrage.
Gut gefällt mir auch der Vorschlag, "Erfindungen" hier zu teilen.
Jo, mir auch. Also wenn einer was baut wie:
Info_ClearSingleChoice: Eine einzelne Dialogoption aus der Choice-Liste entfernen. (oCInfo holen, Choiceliste holen, eine Choice suchen und umhängen (um Speicherleaks zu vermeiden am besten an die Choiceliste einer Hilfs-oCInfo, deren Choices man dann cleart)
Effiziente Schleifen (aufbauend auf einer Abwandlung von MEM_CallByString, man sollte aber mindestens das letzte Ergebnis der Symbolsuche cachen, damit zumindest ungeschachtelte Schleifen direkt aufgerufen werden)
Echte dynamische Arrays, die Weltenwechsel überleben (Abwandlung von MEM_Alloc nutzen und Speicher in Parsersymbol integrieren, dieses Parsersymbol zu einem Array machen, am besten im ersten Index den Symbolindex des Arrays Speichern, damit man das Array bequem übergeben kann, bei der Erzeugung müsste man einmalig in einem String angeben, welches Parsersymbol man zu einem Array machen will)
Erzeugen von Vobs (Abwandlung von MEM_Alloc nutzen, Vob in Welt einbauen).
Aufruf von Enginefunktionen (ok, das ist hart!, man müsste Assemblercode ins Datensegment schreiben und einen Externalaufruf hacken, sodass dieser Assemblercode im Datensegment aufgerufen wird, aber es sollte gehen, Datenausführungsverhinderung ist laut Nico i.d.R. kein Problem)
Austauschen von Vob-Visuals (sollte gehen indem man Visual-Pointer austauscht. Kann Probleme wegen Boundingboxen geben. Setzt wohl vorraus, dass in der Welt ein Vob mit dem neuen Visual bereits vorhanden und auffindbar ist. Außerdem sollte es vielleicht noch ein Vob geben, das noch eine Referenz auf das alte Visual hält um Leaks zu vermeiden)
Zugriff auf HUD Objekte und Austausch von Texturen (gerade im anderen Thread gelesen, läuft wohl auf das raussuchen der passenden Pointer raus und setzt vermutlich auch geladene Texturen voraus)
"füge deine tolle Idee hier ein"
der kann das sehr gerne hier posten.
Edit: Und weiter gehts:
@Sekti: Du hast bei den drei Spawnmanager-Konstanten geschrieben, dass die sehr interessant ist. Allerdings sind das doch nur die Adressen, oder? Wäre gut, wenn man mit denen was machen könnte, sind ja für die KI-Glocke wichtig, hat Zerxes ja mal erwähnt
Du musst MEM_ReadInt bzw. MEM_WriteInt benutzen um an diesen Adressen zu schreiben. Vorher musst du das Floatpaket nutzen um die passenden Floats zu bauen (wenn du die Konstante auf einen bestimmten Wert setzen willst).
EDIT: Hab jetzt mal den Regen testen wollen, aber da tut sich gar nichts, auch Sonne abstellen ging nicht Dafür klappt schonmal das Anzeigen des Waynets
Also bei mir macht die Beispielfunktion genau das richtige... Das heißt es regnet. Es sei denn es ist gerade Mitternacht.
Wenn du nicht die Beispielfunktion sondern eine eigene Funktion benutzt schau mal ob du wenigstens den Standardregen am ersten Tag kaputt machst.^^
masterState_sunOn scheint übrigens von der Engine nicht benutzt zu werden. Kein wundern, dass das nicht funktioniert.
Aber vielleicht kannst du den Alphakanal der Sonne auf 0 setzen (oder die Farbwerte). Immerhin gibtes sun_colour[0] und sun_colour[1] zwischen denen vermutlich irgendwas überblendet wird. Müsste man probieren.
@Nico: Danke. Das sollte helfen.
Ah, ich hatte die Gothic.exe nicht die Gothic_mod.exe. Jetzt sieht das brauchbarer aus.
Edit: Und weiter gehts:
Du musst MEM_ReadInt bzw. MEM_WriteInt benutzen um an diesen Adressen zu schreiben. Vorher musst du das Floatpaket nutzen um die passenden Floats zu bauen (wenn du die Konstante auf einen bestimmten Wert setzen willst).
Ah ok, muss ich mal so versuchen (nur erstmal das Floatpaket raussuchen ).
Zitat von Sektenspinner
Also bei mir macht die Beispielfunktion genau das richtige... Das heißt es regnet. Es sei denn es ist gerade Mitternacht.
Hast du Himmelseffekte aktiviert?
Du hast oben die 20% fürs Überblenden geschrieben, das ist bei einem ganzen Tag ja einiges, d.h. ich hab vermutlich nur nicht lange genug gewartet
Zitat von Sektenspinner
masterState_sunOn scheint übrigens von der Engine nicht benutzt zu werden. Kein wundern, dass das nicht funktioniert.
Ich hatte es mit m_bSunVisible versucht, aber da hat sich auch nichts getan
EDIT: Regen funzt jetzt, scheint am fehlenden Floatpaket gelegen zu haben Lässt sich auch einfach wieder beenden, indem man Endzeit auf FLOATNULL setzt, also beliebiges Starten und Beenden des Regens möglich. Müsste man aber wohl ein wenig schöner machen, weil so ist es ein sehr abruptes Ende.
EDIT2: So, auch Spawnrange funzt, bin grad in einem km Entfernung und die KI kämpft trotzdem weiter. Ermöglicht jetzt z.b. größere Botk-Level oder andere Strategie-Mods
Du hast oben die 20% fürs Überblenden geschrieben, das ist bei einem ganzen Tag ja einiges, d.h. ich hab vermutlich nur nicht lange genug gewartet
Bei dir ist standardmäßig nacht? Ist ja gruselig.^^
Übrigens könnte man das "Problem" vermutlich lösen, indem man den Start auf -1 und das Ende auf 2 stellt. Dann liegen die Überblendzeiten außerhalb des Tages.
Ich hatte es mit m_bSunVisible versucht, aber da hat sich auch nichts getan
Das hat nur mit dem Lensflare zu tun und wird jeden Frame überschrieben. Da steht drin, ob grade was zwischen Sonne und Spieler ist denke ich. Könnte man also vielleicht verwenden um zu bestimmen oder der Spieler in einer Höhle steht.
Wobei das unzuverlässig ist, könnte ja auch was anderes im Weg sein. Und nachts funktioniert das auch nicht.
EDIT: Regen funzt jetzt, scheint am fehlenden Floatpaket gelegen zu haben Lässt sich auch einfach wieder beenden, indem man Endzeit auf FLOATNULL setzt, also beliebiges Starten und Beenden des Regens möglich. Müsste man aber wohl ein wenig schöner machen, weil so ist es ein sehr abruptes Ende.
Ja, kriegt man hin. Sei b die Zeit, die du fürs ausblenden haben willst und t die aktuelle Zeit.
Dann setze den Start des Regens auf t - 4*b und das Ende auf t + b.
Auf ähnliche Weise kannst du einblenden.
Rechnen mit floats natürlich nur mit dem Floatpaket. Integeroperationen machen Unsinn mit floats.
EDIT2: So, auch Spawnrange funzt, bin grad in einem km Entfernung und die KI kämpft trotzdem weiter. Ermöglicht jetzt z.b. größere Botk-Level oder andere Strategie-Mods
Bei dir ist standardmäßig nacht? Ist ja gruselig.^^
Übrigens könnte man das "Problem" vermutlich lösen, indem man den Start auf -1 und das Ende auf 2 stellt. Dann liegen die Überblendzeiten außerhalb des Tages.
Nene, lag wohl wirklich nur am Float-Paket, wobei sich da ja nichts geändert hat außer dem Ort, wo die zwei Variablen FLOATNULL und FLOATEINS stehen und das halt jetzt wirklich Floats sind
Aber egal, hauptsache es klappt. Ich experimentiere mal weiter, vllt. finde ich heute noch was hübsches
EDIT: Eine Mob_RemoveItem (var string mob, var C_Item item, var int amount); wäre praktisch und sollte jetzt ja auch machbar sein, werde ich mir mal für die nächsten Tage vornehmen vllt.
EDIT: Eine Mob_RemoveItem (var string mob, var C_Item item, var int amount); wäre praktisch und sollte jetzt ja auch machbar sein, werde ich mir mal für die nächsten Tage vornehmen vllt.
Problematisch ist das Item sauber zu entfernen. Wahrscheinlich geht das mit einem MEM_AssignInst an eine Scriptinstanz und dann einem Wld_RemoveItem.
Anschließend musst du die verkettete Liste der Truhe fixen.
Den amount-Eintrag bei Items auch nicht vergessen, es können ja Itemstapel drinliegen.
Vielleicht ist es einfacher das Inventar von einem Npc und einer Truhe auszutauschen (Pointer umhängen) und dann die Funktionen für Npcs zu benutzen. (und hinterher wieder umhängen)
Achja: Ich habe gerade mal die oCItems dokumentiert, das könnte auch für dich nützlich sein (neue Version ist bereits oben).
Und wenn jemand ein Script braucht, dass Items ins Inventar des Helden transferiert falls dieser Nahe an ein Item herantaucht, hier:
Code:
//[...]
//{
//----------------------------------
// Automatisch oCItem im Fokus
// aufheben beim Tauchen
//----------------------------------
if (C_BodyStateContains (hero, BS_DIVE)) {
var oCNpc her;
her = Hlp_GetNpc (hero);
if (her.focus_vob) {
var oCItem her_focusItem;
MEM_AssignInst (her_focusItem, her.focus_vob);
if (Hlp_IsValidItem (her_focusItem)) {
if (Npc_GetDistToItem (hero, her_focusItem) < 170) {
CreateInvItems (hero, her_focusItem.instanz, her_focusItem.amount); //amount beachten
Wld_RemoveItem (her_focusItem);
var string str; str = ConcatStrings (her_focusItem.name, " aufgehoben!");
PrintScreen (str, -1, -1, FONT_SCREENSMALL, 3);
};
};
};
};
//}
Das Script jede Sekunde aufzurufen sollte genügen und ersetzt mühsame Mover und Trigger Abenteuer.
Mal eine kleine Frage als C++-Programmierer: Wie kannst du wissen, an welchen Stellen im Speicher geschrieben wird?
Meines Wissens ist der Speicher (zunächst einmal) ein Sumpf. Ein unglaublich dichter, beinahe undurchsehbarer Sumpf. Sachen, die du irgendwo ablegst, könnten gelöscht werden, Sachen, die gelöscht wurden, könntest du abfragen.
Dank Betriebsystemen ist der Speicher ein bisschen sicherer geworden. Anstatt nach Lust und Laune Speicher zu reservieren und freizugeben, einen Fehler zu machen und das Programm zum Absturz bringen, überprüft Windows, wie viel Speicher für irgendwas reserviert werden muss, ob eine Lücke in bereits reservierten Speicherstellen ausreicht oder ob man eine andere Stelle nimmt. Dieses System hat einen großen Vorteil: Man kann nicht mehr wie wild in den Speicher reinschreiben, Programme überschreiben und das Betriebsystem verarschen, zudem muss man sich nicht mehr mit komplexer Zeigerarithmetik die Nächte um die Ohren schlagen. Man muss sich nicht mehr darum kümmern, ob das 3-Byte-Objekt der Klasse XYZ in den Speicher der 4-Byte-Ganzzahl ABC reinschreibt, weil der Unterschied zwischen den verschiedenen Speicherstellen nicht 0x03, sondern 0x02 beträgt.
Allerdings hat dieses System auch einen Nachteil: Speicheradressen werden abstrakt. Anstatt zu sagen: "Ich beschließe meine eigene Speicherverwaltung, gebe die Offsets bestimmter Objekte als Konstanten an und hantiere mit Speicheradressen. (was bei guter Programmierarbeit zu einem überaus schnellen Programm führen kann)", kann man mit diesem System nur noch sagen: "Ich weiss, meine Klasse CMyNPC belegt pro Objekt im Speicher 8 doubles, 4 floats, 20 ints und 10 strings (string-Klasse der STL). 8*8+4*4+20*4+10*4 (Zeiger sind immer 4 Byte groß)=64+16+80+40=200 Byte. Also, new-Operator von C++, suche nach einer Stelle im Speicher, wo 200 Byte frei sind, und sichere mein Objekt dort ab!".
Diese Technik ist zwar ziemlich umständlich und nimmt dem Programmierer einen Teil seiner Freiheit, ist aber unglaublich komfortabel, da der Speicher für ein Programm nun zur Laufzeit vom Betriebssyytem gesetzt wird und nicht mehr zur Kompilierzeit vom Programmierer. Somit wird verhindert, dass ein Programm in ein bereits bestehendes Programm geladen wird. An die Speicheradressen kommt man also schlecht ran. Was mich zu meiner Anfangsfrage bringt: Wie kannst du wissen, an welchen Stellen im Speicher geschrieben wird?
Die Speicherverwaltung funktioniert in einigen Punkten so wie du sagst, aber absolute Adressen gibt es trotzdem. Ich versuche das mal zu erläutern:
Es ist so, dass das Betriebssystem jedem Programm einen virtuellen Adressraum zur Verfügung stellt (jeweils im Prinzip die ganze Bandbreite von 0x00000000 bis 0xFFFFFFFF). Manche virtuellen Adressebereiche werden dann auch wirklich mit physikalischem Speicher indentifiziert. Auf welche physikalischen Bereiche, entscheidet das Betriebssystem.
So wird wie du sagst verhindert, dass mehrere Programme den selben physikalischen Speicher nutzen und sich gegenseitig behindern.
Aber diese Abstraktionsschicht ist für das Programm selbst vollständig opak (d.h. undurchsichtig). Das Programm selbst kennt zu keinem Zeitpunkt tatsächliche physikalische Adressen und kann auch niemals gezielt auf physikalische Adressen zugreifen. Die Umrechnung von virtuellen in physikalische Adressen geschieht erst "zwischen" CPU und Speicher in der MMU (Memory Management Unit).
Kurz: Für das Programm sieht es so aus, als hätte es den gesamten Speicher ganz für sich zur Verfügung.
Innerhalb des virtuellen Adressraums kann es durchaus absolute Adressen geben. Bestimmte (nicht alle) virtuellen Speicherplätze sind vom Programmstart an fest reserviert. Ich kann also immer auf den Parser an Adresse 0xAB40C0 zugreifen, das Betriebssystem (die MMU) ist dafür zuständig die tatsächliche physikalsiche Adresse (die variieren kann) zu bestimmen und den Hauptspeicherzugriff durchzuführen.
Die Daten mit absoluten Adressen (im virtuellen Adresseraum) sind sogenannte statische Daten. Beispiel:
Code:
#include<stdio.h>
int meineVariable = 42;
int main (void) {
printf ("%d", meineVariable);
}
Hier ist meineVariable eine statische Variable. Sie wird nicht mit new erzeugt und auch nicht freigegeben. Sie wird IMMER an der selben virtuellen Adresse zu finden sein. Wenn ich also im Kompilat nachschaue wo sie liegt (z.B. mit IDA), kann ich sie im Speicher finden. Genauso kann es auch statische Objekte geben:
Code:
#include<stdio.h>
class meineKlasse {
public:
int var1;
int var2;
};
class meineKlasse meineInstanz;
int main (void) {
meineInstanz.var1 = 42;
printf ("%d", meineInstanz.var1);
getchar();
}
Auch hier tritt nirgendwo new auf. Diese Situation ist aber eher selten (meistens legt man keine große Objekte statisch an). Aber oft gibt es statische Zeiger auf Klassen, zum Beispiel gibt es einen Zeiger auf das aktuelle Game. Das ist so deklariert:
Code:
oCGame* ogame;
Mit IDA kann ich herausfinden wo dieser Pointer im (virtuellen) Speicher liegt. Mit dessen Hilfe kann ich dann das zugehörige oCGame finden (gesetzt den Fall, dass Gothic den Pointer aktuell hält).
Auch alle mit "static" markierten Eigenschaften von Klassen haben feste Adressen.
Ansonsten hast du recht: Ich kann nicht wissen, wo new ein neues Objekt hinlegt, wenn es zur Laufzeit erzeugt wird. Aber genauso wenig kann Gothic das und deswegen muss sich Gothic Referenzen auf das Objekt zwischenspeichern. Und diese Referenzen stehen wiederum irgendwo. Und dieses "irgendwo" ist direkt oder indirekt zugreifbar, man muss nur wissen von welcher statischen Eigenschaft man loslaufen muss um sie zu finden.
Wenn ein Objekt gar nicht mehr direkt oder indirekt an einem statischen Zeiger hinge, könnte man gar nicht mehr hinnavigieren (Gothic aber auch nicht). Das sind Speicherleaks und stellen eine Fehlersituation dar.
Meines Wissens ist der Speicher (zunächst einmal) ein Sumpf. Ein unglaublich dichter, beinahe undurchsehbarer Sumpf. Sachen, die du irgendwo ablegst, könnten gelöscht werden, Sachen, die gelöscht wurden, könntest du abfragen.
Das ist im virtuellen Adressraum immernoch ein Problem. Wenn ich mir einen Item-Zeiger merke und das Item zerstöre ist der Zeiger natürlich ungültig und es kann zu Fehlern kommen. Aber "spontan" (das heißt ohne dass ich als Programmierer das direkt oder indirekt veranlasse) fliegt im Speicher nichts hin und her.
Dank Betriebsystemen ist der Speicher ein bisschen sicherer geworden. Anstatt nach Lust und Laune Speicher zu reservieren und freizugeben, einen Fehler zu machen und das Programm zum Absturz bringen
Programm zum Absturz bringen geht wie eh und je. Aber man kann das Betriebssystem nicht mehr zum Absturz bringen (so der Plan). Wenn ein Programm auf eine virtuelle Adresse zugreift, der kein physikalischer Speicher zugeordnet ist, gibt es eine Unterbrechnung und die Ausnahmebehandlung der Anwendung wird angeworfen, falls vorhanden. Sonst wird "ein Problem festgestellt" und das Programm abgewürgt.
Im Prinzip hat sich durch die "neuen" Betriebssysteme für die Programme nichts geändert. Nur dass sie alleine in einen virtuellen Raum gestellt werden und niemand anderen kaputt machen können.
Ich verstehe in manchen Abschnitten nur Bahnhof, tut mir leid. WO ist dieser "virtuelle Speicher"? Was ist der Unterschied zum reellen Speicher (den ich hier einfach mal als RAM-Baustein ansehe)? Was ist, wenn Speicher auf Adressen reserviert wird, den's gar nicht gibt, weil das 32-Bitsystem mit 4 Milliarden Bytes nur über 2 Milliarden Bytes verfügt? Und wie kann jedes Programm einen eigenen virtuellen Adressraum haben? Läuft das Ganze über die Festplatte und die Auslagerungsdatei?
Und wenn meine Vermutungen stimmen, wie kann Gothic mit dem virtuellen Adressraum umgehen, wenn bereits andere Objekte gespeichert sind? Warum steht der Game-Zeiger nicht bei 0x00000001 - 0x00000004 (4 Bytes pro Zeiger, egal welcher Typ)?
Ich verstehe in manchen Abschnitten nur Bahnhof, tut mir leid. WO ist dieser "virtuelle Speicher"?
Virtuellen Speicher gibt es "nicht wirklich". Das Betriebssystem gaukelt jedem Programm vor, es hätte den gesamten Adressraum zur freien Verfügung. Um für die Programme die Illusion aufrecht zu erhalten muss das Betriebssystem einen ganzen Wust an Tabellen verwalten, die zwischen virtuellen Adressen und physikalischen Adressen übersetzen.
Was ist der Unterschied zum reellen Speicher (den ich hier einfach mal als RAM-Baustein ansehe)? Was ist, wenn Speicher auf Adressen reserviert wird, den's gar nicht gibt, weil das 32-Bitsystem mit 4 Milliarden Bytes nur über 2 Milliarden Bytes verfügt?
Wie genau Speicherallokation funktioniert weiß ich nicht. Ich gehe aber davon aus, dass man hier das Betriebssystem fragt. Das Betriebssystem organisiert dann physikalischen Speicher, gibt dem Programm aber eine virtuelle Adresse um darauf zuzugreifen.
Und wie kann jedes Programm einen eigenen virtuellen Adressraum haben? Läuft das Ganze über die Festplatte und die Auslagerungsdatei?
Nicht der ganz Adressraum ist wirklich mit physikalischem Speicher identifiziert. Anders gesagt: Es gibt Bereiche im virtuellen Adressraum, denen keine physikalsicher Speicher zugeordnet ist. Im allgemeinen wird von Programmen nur ein Bruchteil des riesigen 32 bit Adressraums genutzt.
Entsprechend muss das Betriebssystem auch nur für die benutzen Bereiche physikalischen Speicher reservieren.
Und wenn meine Vermutungen stimmen, wie kann Gothic mit dem virtuellen Adressraum umgehen, wenn bereits andere Objekte gespeichert sind?
Gothic interessiert das wie gesagt nicht. Gothic "glaubt" alleine auf der Maschine zu sein.
Warum steht der Game-Zeiger nicht bei 0x00000001 - 0x00000004 (4 Bytes pro Zeiger, egal welcher Typ)?
Im Prinzip Spräche nichts dagegen, aber erstens ist dieser Game-Zeiger nicht die erste Variable die deklariert wird: Da liegt noch viel mehr herum, vermutlich auch Methodentabellen, viele statische Variablen, Konstanten usw die an niedrigeren Adressen liegen.
Und zweitens sind die niedrigsten Adressen im virtuellen Adressraum für andere Dinge reserviert, genau weiß ich es nicht, aber ich schätze mal, dass das für die Kommunikation mit dem Betriebssystem wichtig ist.
Ich hoffe ich habe deine Fragen richtig verstanden und konnte zumindest teilweise weiterhelfen.
Solange man absolute Adressen im Hauptmodul verwendet ist das relativ unproblematisch. Normalerweise wird die EXE immer an ihre bevorzugte Adresse geladen, da dieser Adressbereich fast immer frei ist (man müsste extra eine spezielle Software entwickeln, damit dies nicht mehr so ist) und das entsprechende Flag für den Loader (welches neuere Windows-Versionen dazu bewegen könnte, es an eine andere Adresse zu laden) in den Gothic-Modulen gar nicht gesetzt ist (das wurde erst danach eingeführt und wird bis heute fast nur von Microsoft verwendet).
Das einzige praktische Problem könnte unter WINE auftreten (dort sind Relozierungen wahrscheinlicher). Aber selbst dort kann man mit einer einfachen Änderung im PE/COFF-Header die Relocations deaktivieren (dann muss der Loader des Betriebssystems es an die "bevorzugte" Adresse laden).
"Unter diesen schwierigen Umständen bin ich mir sicher, daß diese guten Menschen meinen augenblicklichen Bedarf an deren Gold verstehen werden." -- Connor
Eine sehr schöne Sache, aber eben nur für Gothic 2.
Könntest du das gleiche auch für Gothic 1 für Modifikationen erstellen?
Wäre das machbar?
In Punkto Marvin, was ist wenn bei einem der Marvin schon im Vorfeld nicht funktioniert... führt das Script Marvin trotzdem aus und wird auch angezeigt?
Inwiefern ist zCTimer nützlich bzw. sollte man es überhaupt benutzen? Und wenn ja: Wie macht man das?
Oder sollte man für Timer zCWorldTimer benutzen?
Den zCWorldTimer braucht man nicht wirklich. Wenn du nicht gerade die Zahl der vergangenen Tage zurückstellen willst, kommst du mit Wld_SetTime eigentlich überall ran.
Nützlich wäre es allenfalls wenn du die Zeit schneller vergehen lassen willst ("Nacht zu Tag Zauber") ohne dass Routine und Spawnmanager heiß laufen.
Der zCTimer ist ein technischer Timer. Du kannst rausbekommen wieviel Zeit im aktuellen Frame vergangen ist, wieviel Zeit insgesamt vergangen ist und kommst an Referenzzeiten für onTimer Ereignisse.
So kann man zum Beispiel eine Triggerschleife bauen, die in jedem Frame feuer:
Code:
func void meineSchleifenFunktion(){//Es wird gleich MEM_Timer genutzt, der muss initialisiert sein
MEM_InitGlobalInst();
//Triggernachricht senden
Wld_SendTrigger ("MEIN_SCHLEIFEN_TRIGGER"); //ruft meineSchleifenFunktion auf
//Triggerscript holen:
var oCTriggerScript Mein_Schleifen_Trigger;
Mein_Schleifen_Trigger = MEM_PtrToInst (MEM_SearchVobByName ("MEIN_SCHLEIFEN_TRIGGER"));
Mein_Schleifen_Trigger._zCVob_nextOnTimer = MEM_Timer.totalTimeFloat; //wäre eigentlich jetzt schon wieder dran, wird aber erst im nächsten Frame bemerkt
};
Einfach die Verzögerungszeit des Triggers auf 0 setzen geht nicht, da er dann wirklich sofort feuert (dann kehrt der Aufruf von Wld_SendTrigger niemals zurück).
Dieser Trigger feuert auch wirklich jeden Frame, selbst wenn das Spiel angehalten ist (F9, Menü, Charaktermenü...).
Der eingestellte Firedelay bei MEIN_SCHLEIFEN_TRIGGER sollte irgendeinen positiven Wert haben (z.B: 1.0), welcher ist egal.
Ich habe einen solchen Trigger genutzt um einen Zauber zu implementieren, der alles außer den Spieler anhält (freier Kurzstreckenteleport, Spieler läuft zum Zielort und drückt ENTER). Dazu habe ich in jedem Frame die WalkList der Welt verändert.
Zitat von uhrparis
Könntest du das gleiche auch für Gothic 1 für Modifikationen erstellen?
Können schon. Aber es braucht ein bisschen Zeit die ganzen Klassendokumentationen anzupassen.
Wenn wirklich Interesse daran besteht, kann ich mich da mal dransetzen, aber nicht in den nächsten zwei Wochen.
Zitat von uhrparis
In Punkto Marvin, was ist wenn bei einem der Marvin schon im Vorfeld nicht funktioniert... führt das Script Marvin trotzdem aus und wird auch angezeigt?