[Pokémon Feuerrote und Smaragd-Edition] Multitasking - Callbacks


  • Multitasking in Pokemon - Das System der Callbacks





    Hi Leute,


    viele von euch fragen sich bestimmt das ein oder andere Mal, wie es funktioniert, dass Pokemon ROMs mehrere Dinge gleichzeitig ablaufen lassen, obwohl der GBA nur über einen Prozessor verfügt und demnach nur einen Maschienenbefehl gleichzeitig ausführen kann. Die Lösung liegt auf der Hand - Multitasking. Einige kennen womöglich den Begriff Thread, wenn nicht, ist das aber auch nicht schlimm. Um mehrere Dinge "gleichzeitig" ausführen zu lassen, bedient sich Gamefreak eines Callbacksystems.

    Was ist ein Callback?


    Gerade ist der Begriff Callbacksystem gefallen, was die Frage nahelegt: "Was ist ein Callback?". Ein Callback ist ein einzelner Auftrag, der paralell zu anderen Callbacks ausgeführt werden kann. Wenn also mehrere Dinge gleichzeitig geschehen sollen, wird für diesen Auftrag ein Callback erzeugt. Jedes aktive Callback wird vom Spiel einmal pro Frame aufgerufen und ausgeführt. Der Code des Callbacks selbst ändert sich dabei jedoch nicht und ist immer der gleiche.

    Wie sehen diese Callbacks aus?


    Im Grunde besteht ein Callback immer mindestens aus einem Pointer auf die Fuktion, die aufgerufen werden soll. Dieser befindet sich im RAM. Jeden Frame wird diese Funktion dann einmal aufgerufen und bis zum Ende ausgeführt. Manche Callbacks verfügen auch über einen Platz für lokale Variablen und Parameter, die nur von diesem Callback benutzt werden. Auf diese Weise kann man beispielsweise mitzählen, wie viele Frames bereits vergangen sind und demnach den Code anpassen.


    Welche Arten von Callbacks gibt es?


    Ich habe gerade bereits davon gesprochen, dass es verschiedene Arten von Callbacks gibt, die das Game benutzt. Meine Informationen beziehen sich auf Feuerrot, laut Sturmvogel aber sind sie im Smaragd nicht allzu unterschiedlich. Man unterscheidet zwischen zwei verschiedenen Callbacks, den Hauptcallbacks (Main Callbacks oder Small Callbacks), wie ich sie nenne, und den ebenfalls von mir so genannten "Big Callbacks". Small Callbacks bestehen lediglich aus einem Pointer auf die Funktion und können nicht aktiviert oder deaktiviert werden. Sie müssen also seperat aufgerufen werden, was das ganze komplizierter macht. Das Spiel bietet Platz für maximal 8 derartiger Callbacks. Dabei wird nur das Callback0, also das Callback mit der höchsten Prioriät jeden Frame automatisch aufgerufen. Will man also, dass auch Callback1 oder Callback2 jeden Frame automatisch aufgerufen werden, so müssen diese Aufrufe Teil von Callback0 sein.
    Weiterhin gibt es dann noch die Big Callbacks, die ich so nenne, weil sie über Variablenplatz im RAM verfügen. Ingesammt erhält ein Callback dabei 40 (0x28) Bytes zur Verfügung. Die ersten vier davon stellen, wie bei den Small Callbacks, einen Pointer auf die Funktion dar. Die restlichen Variablen sind entweder geerbte Parameter oder freie Variablen. Der Vorteil dieser Callbacks ist es, dass sie sowohl aktiviert als auch deaktiviert werden können. Jedes aktive Callback wird dabei einmal pro Frame aufgerufen (der Aufruf erfolgt aus dem von GameFreak standardisierten Callback0).


    Small Callbacks


    Eben habe ich bereits angedeuted, dass es für die SmallCallbacks standardisierte Versionen gibt. Das Callback, das normalerweise auf Callback0 angeordnet ist, ändert sich meist während des gesamten Spielverlaufes nicht, beziehungsweise wird immer für die gleichen Dinge genutzt. Es ist unter anderem für die Verarbeitung von Hardwareinformationen zuständig, so etwa den Tasteneingaben. Auch ruft es andere Callbacks auf, wie das Callback1 und indirekt auch alle Big Callbacks, die aktiv sind.
    Das Callback1 ist dagegen Spielsituationsabhängig. Es existiert ein Callback1 für das Overworldsystem. In diesem wird die Funktionsweise des Overworlds geregelt, zum Beispiel wie der Spieler sich bewegt. Auch für Kämpfe, den Beutel, etc. gibt es eigene Callback1. Meist benötigen diese aber dann andere Callbacks, um ganz zu funktionieren. So schreibt zum Beispiel das Callback1, das das Overworldsystem initialisiert automatisch Pointer an andere Callbacks und überschreibt schließlich auch sich selbst mit der Routine, die für das Aktivhalten der Overworldfunktionen zuständig ist. Indem wir also an den Ort von Callback1 diese Intialisierungsroutine als Pointer schreiben, laden wir das gesamte Overworldsystem neu, so wie es etwa nach dem Schließen des Beutels getan wird.

    Big Callbacks


    Die Big Callbacks dagegen sind für nebenläufige Dinge wie Animationen oder Bewegungen zuständig. So erhalten Overworlds ihre eigenen BigCallbacks, um sich gleichzeitig zu bewegen. Im Gegensatz zu den Small Callbacks ist es einfach ein Big Callback zu erzeugen und wieder zu löschen. Auf diese Weise kann man selbst sehr effektiv Animationen erzeugen, die dann parallel zum Rest des Spielgeschehens ablaufen. Jedem Big Callback wird dabei eine Nummer, also eine ID, zugeordnet, welche beim Erzeugen des Callbacks festgelegt wird. Über diese Nummer kann man auf alle internen Variablen des Callbacks zugreifen und dieses auch wieder löschen. Will ich zum Beispiel während meinem Script, dass es dunkler wird, so kann ich diesen Prozess als Big Callback schreiben und dieses dann per ASM erzeugen. Da Big Callbacks sich selbst über ihre Nummer auch löschen können, muss ich das dann gar nicht mehr im Script tun.


    Pokemon Feuerrot: Big Callbacks im Detail


    Alle nun folgenden Informationen beziehen sich auf Feuerrot (D), lassen sich höchstwahrscheinlich bis auf Offsets auch auf andere Roms übertragen. Ich werde nun erläutern, wie man Big Callbacks benutzen kann. Beginnen wir mit der Initialisierungsroutine eines solchen Callbacks.


    Initialisieren


    Code
    1. u8 init_big_cb (u32 functionptr, u8 unkown)


    Diese Funktion erzeugt ein Big Callback. Als Übergabeparameter weist sie folgende auf:
    r0 = Pointer auf die Funktion, die aufgerufen werden soll (für thumb +1)
    r1 = lokaler Parameterwert, wird an Byte 0x7 des lokalen Variablenbereichs des Callbacks geschrieben


    Als Rückgabewert liefert uns diese Funktion einen unsinged Integer der Größe 8-Bits, also einen Byte (in r0). Dieser ist schlicht und ergreifend einfach die ID des Big Callbacks, über welches wir auf dieses später zugreifen können. Wollen wir das Callback also von außen noch weiter kontrollieren, so müssen wir diese Nummer speichern, da sie, solange das Callback noch nicht gelöscht wurde, einzigartig ist und deshalb eindeutig zu diesem Callback führt. Wir können aber nicht beliebig viele Big Callbacks gleichzeitig aktiv haben. Das Limit für diese beträgt 16. Wird die Routine trotzdem aufgerufen, so wird einfach kein Callback erzeugt. Als Rückgabewert erhalten wir dann 0.


    Offset der Funktion:


    Interne und externe Kontrolle


    Wie greifen wir nun intern, also innerhalb der Routine des Callbacks und extern, also außerhalb der Funktion des Callbacks, auf unser Callback zu? Einige, die bis hier her mitgedacht haben, wissen es bereits - über seine ID. Mittels dieser können wir nämlich den RAM-Platz des Callbacks errechnen. Die Formel dazu lautet (Achtung in Hexadezimal):


    Code
    1. CallbackID * 0x28 + base_offset


    mit base_offset =


    Und schon haben wir das Offset des RAM-Platzes, das das Callback benutzt. Jetzt können wir alle Variablen auslesen und verändern, wenn wir das wollen. Wollen wir also außerhalb des Callbacks auf diese Variablen zugreifen, so brauchen wir immer die CallbackID, um den Platz zu errechnen. Wir müssen diese ID also irgendwo abspeichern. Wie das gemacht wird, ist unterschiedlich.
    Der interne Zugriff funktioniert genauso. Mittels der Formel wird errechnet, wo die Variablen im RAM liegen. Im Gegensatz zum externen Zugriff aber müssen wir die CallbackID nicht zwischenspeichern. Wird nämlich ein Callback aufgerufen, so übergibt der Handler für die Big Callbacks immer auch gleich die Nummer an dieses Callback mit (in r0). So können wir innerhalb des Callbacks immer problemlos auf interne Variablen zugreifen.


    Pausieren


    Die nächste Information ist nicht beweisen, sondern nur "erprobt". Bugs sind mir noch keine aufgefallen. Scheinbar kann man Big Callbacks nämlich auch pausieren, ohne sie gleich ganz zu löschen. Dafür müssen wir lediglich die Flag, die aussagt, ob das Callback "läuft" auf 0 setzen. Diese Flag ist der Byte 0x4 innerhalb des Variablenbereichs des Callbacks. Er ist normalerweise auf 0x1, wenn das Callback benutzt wird. Setzen wir ihn auf 0x0 pausiert es solange bis wir den Byte wieder auf 0x1 setzen. Die Funktion wird also während des Zeitraums nicht aufgerufen.

    Löschen


    Wir können Big Callbacks ohne Probleme auch wieder löschen. Mit diesem Vorgang wird die Funktion nicht mehr aufgerufen und die ID ist wieder belegbar. Der Ram-Platz wird auch für neue Callbacks wieder freigegeben. Um ein Callback zu löschen, benötigen wir lediglich dessen ID. Diese Funktion erledigt das dann für uns:


    void delete_big_callback (u8 CB_ID)


    Als Übergabeparameter weist sie folgenden auf:
    r0 = ID des Callbacks, das gelöscht werden soll.


    Offset der Funktion:




    Schlusswort


    Ich hoffe, dass diese Informationen über Callbacks dem ein oder anderem beim Umsetzen einiger Features helfen kann. In diesem Sinne stehe ich bei Fragen gerne zur Verfügung. Auch konkrete Ergebnisse zu gefunden Offsets einzelner Callbacks werde ich gerne in den Startpost aufnehmen, sofern jemand etwas herausfindet.


    Edit: Danke an Kairyon für die Offsets in Smaragd ROMs [BPEE, BPED]

    Wo war Gondor, als meine Klausurenphase begann?

    Edited once, last by Wodka ().

  • Hatteste mi ja schonmal erlät aber finds Gut das du dies nun Veröffentlicht hast :)
    (hab gestern noch das Offset gesucht ^^)

    ------------------------------------------------------------------------------------
    ~ ~ ~ SoulK3 ~ ~ ~


    :thumbsup: Wer Rechtschreibfehler findet darf sie gerne Behalten :thumbsup:


    Drei Menschen können ein Geheimnis bewahren.
    Wenn Zwei von ihnen Tot sind.
    :evil:
    ------------------------------------------------------------------------------------

  • Ich hätte nochmal ne Anmerkung zu den Callbacks bei den Overworlds.
    Ich hab zwar grundsätzlich nicht viel Ahnung davon, aber ich bin mir ziemlich sicher, dass die Overwords sich über die Spriteanimationscallbacks integriert und nicht ihr eigenes Callback haben (falls du damit nicht das gleiche meist).
    Auf jedenfalls hab ich mal das Callback1 (ich hoffe ich meine das richtige) mit ein bisschen C Code überschrieben und die Animationen der Overworlds liefen so lange weiter bis man alle Sprites gekillt hat.

    Du möchtest mich mal treffen? Dann sag hallo und besuche mich an der FAU in Erlangen.

  • Ja, nicht jeder OW erhält sein eigenes Big Callback. Das ist auch mehr zur Veranschaulichung gedacht. Die Overworlds nutzen ihr eigenes Callback gemeinsam. Soll aber auch nicht Thema dieses Theards sein ;)

  • Falls es jemanden interessiert, hier die Offsets für Smaragd:


    Smaragd (BPED)


    Callback erzeugen

    Code
    1. 0x0A8FCC


    Interne und externe Kontrolle

    Code
    1. CallbackID * 0x28 + 0x03005E00


    Callback löschen

    Code
    1. 0x0A90B8


    Emerald (BPEE)


    Callback erzeugen

    Code
    1. 0x0A8FB0


    Interne und externe Kontrolle

    Code
    1. CallbackID * 0x28 + 0x03005E00


    Callback löschen

    Code
    1. 0x0A909C
  • Hallo Wodka,


    SoulK3 hat mich darauf hingewiesen, dass du in deinem Startbeitrag das Offset der Callback-Funktion für Smaragd falsch abgeschrieben hast. Du hast geschrieben 0xA8FFC, es ist aber 0xA8FCC, siehe meinen Beitrag hier oben.