Tracker, die still sterben
Zwei Runden Fehlersuche an Portalen, die sich nicht schließen. Im Protokoll sah alles erfolgreich aus — weil eine Methode true zurückgibt, auch wenn sie nichts tut.
1032 Wörter · 5 Min. Lesezeit
- minecraft
- eigenbau
- fehlersuche
Die animierten Portale in meiner Welt hatten ein Problem, das in zwei Wellen aufgetreten ist. Erst: „Das Portal schließt sich nicht." Dann, nach der ersten Behebung: „Nach einem Neustart reagiert es gar nicht mehr."
Beide Male sah im Protokoll alles gut aus. Beide Male war die Ursache dieselbe Eigenschaft der zugrundeliegenden Schnittstelle.
Was ein Tracker ist
Das Modell-Plugin verwaltet für jedes dargestellte Objekt einen Verwalter — nennen wir ihn Tracker. Über ihn startet und stoppt man Animationen. Mein Code hat sich diese Verwalter gemerkt, um sie später anzusprechen.
Das war der Fehler.
Falle 1: Tracker werden still neu aufgelegt
Das Plugin speichert die Modelldaten am Objekt selbst und legt aus diesen Daten neue Verwalter an — bei einem Neuladen des Plugins und bei jedem erneuten Laden des Weltbereichs. Die alten werden geschlossen.
Eine gemerkte Referenz ist danach tot. Sie zeigt auf einen Verwalter, den niemand mehr beachtet. Sichtbar ist die Neuauflage, und die weiß von meinen Animationen nichts.
Das Tückische: Ein Weltbereich wird neu geladen, sobald sich ein Spieler nähert. Bei einem Portal, das an einem Weg steht, passiert das mehrmals am Tag.
Falle 2: Der Rückgabewert lügt
Die Methode zum Austauschen einer laufenden Animation gibt einen Wahrheitswert zurück. Ich hatte ihn als Erfolgsmeldung gelesen und protokolliert.
Er bedeutet aber nur: Das Modell kennt die angeforderte Bewegung. Ob unter dem angegebenen Namen tatsächlich etwas läuft, ob der Verwalter überhaupt noch lebt — das sagt er nicht. Läuft nichts, ist der Aufruf ein stiller Leerlauf und meldet trotzdem Erfolg.
Genau deshalb sahen zwei Runden Fehlersuche im Protokoll erfolgreich aus. Ich habe protokolliert, was die Schnittstelle behauptet, nicht was passiert ist.
Erst das eingebaute Fehlersuchprotokoll des Plugins hat die Neuauflagen sichtbar gemacht — und damit war innerhalb von Minuten klar, was los ist.
Warnung
Verallgemeinert: Ein Rückgabewert bedeutet das, was die Dokumentation sagt, nicht das, was der Name nahelegt. Ein true von einer Methode namens replace liest sich wie „ersetzt". Es kann auch „ich wüsste, wie ich das ersetzen würde" bedeuten. Bei allem, was in meinem Protokoll als Erfolg erscheint, frage ich mich seitdem, ob es einen beobachtbaren Effekt gibt, den ich stattdessen prüfen könnte.
Falle 3: Der Name bleibt am ersten Eintrag
Die dritte Eigenheit, die ich beim Nachlesen im dekompilierten Code gefunden habe: Beim Austausch behält der Eintrag den Namen des zuerst gestarteten. Nach einem Wechsel von A zu B läuft B unter dem Namen A weiter.
Wer danach B stoppen will, greift ins Leere. Wer A stoppt, trifft.
Das ist eine dieser Kleinigkeiten, die man nirgends findet, weil sie in keiner Dokumentation steht und im Normalfall nicht auffällt.
Die Lösung
Nie einen Verwalter merken. Bei jedem Aufruf frisch auflösen: über die Registrierung anhand der Kennung des Objekts.
Einen eigenen Animationsschlüssel führen. Damit weiß mein Code, unter welchem Namen seine Animation tatsächlich läuft — unabhängig davon, was die Schnittstelle daraus macht.
Neuauflagen erkennen und behandeln. Ein Pflegetakt prüft regelmäßig, ob das Objekt noch den erwarteten Zustand hat, und setzt ihn andernfalls neu.
Und der entscheidende Nachtrag: Die Ruhe-Animation muss sofort bei der Erzeugung des Verwalters gesetzt werden, nicht erst im Pflegetakt.
Der Grund ist eine Eigenheit der Modelle: Ihre Grundpose — also das, was ohne Animation dargestellt wird — ist das geöffnete Portal. Erst die Ruhe-Animation klappt es zu. Ein frisch erzeugter Verwalter zeigt also die offene Pose, bis meine Animation angestoßen wird.
Mit einem Pflegetakt alle dreißig Sekunden und Neuauflagen bei jeder Annäherung stand das Portal fast durchgehend offen. Mit dem Setzen bei der Erzeugung ist es sofort richtig.
Merksatz, den ich mir aufgeschrieben habe: Bei Modellen, deren Grundpose nicht dem Ruhezustand entspricht, muss die Ruhe-Animation an der Erzeugung hängen, nicht an einem Zeitgeber.
Was das für die Bedrock-Seite bedeutet
Angenehmer Nebeneffekt: Die Übersetzungsschicht liest die so gesetzte Ruhe-Animation als laufende Animation und stellt auf Bedrock denselben Zustand dar. Ein Fix, der beide Seiten gleichzeitig richtigstellt — was bei Crossplay eher die Ausnahme ist.
Warum ich dekompiliert habe
Ein Punkt, der Erklärung verdient: Ich habe den Programmcode des Plugins auseinandergenommen, um zu verstehen, was es tut.
Der Anlass war, dass zwei Fehlersuchen im Protokoll erfolgreich aussahen. Wenn Beobachtung und Verhalten sich widersprechen, ist die Beobachtung unvollständig — und die einzige verbleibende Quelle ist der Code selbst.
Der Ertrag waren drei Eigenschaften, die in keiner Beschreibung stehen: dass Verwalter still neu aufgelegt werden, dass ein Rückgabewert etwas anderes bedeutet als sein Name nahelegt, und dass ein Name beim Austausch am ersten Eintrag hängen bleibt.
Die Grenze, die ich mir dabei gesetzt habe: verstehen, nicht ändern. Ich habe nichts am Plugin angefasst. Die Erkenntnisse sind in meinen eigenen Code geflossen, der es benutzt.
Diese Unterscheidung ist mir wichtig. Ein fremdes Programm zu lesen, um seine Schnittstelle richtig zu bedienen, ist etwas anderes, als es umzuschreiben — und in den allermeisten Fällen reicht das Lesen völlig aus.
Was ich beim Bedienen fremder Schnittstellen jetzt anders mache
Aus diesem Fall sind drei Gewohnheiten geworden.
Keine Referenzen auf fremde Objekte behalten. Bei jedem Zugriff frisch auflösen, es sei denn, die Dokumentation sichert ausdrücklich zu, dass ein Objekt gültig bleibt. Diese Zusicherung gibt es fast nie.
Rückgabewerte nicht als Erfolgsmeldung lesen. Ein Wahrheitswert bedeutet das, was in der Beschreibung steht — und wenn dort nichts steht, weiß man es nicht. Der Ersatz ist, eine beobachtbare Wirkung zu prüfen.
Einen eigenen Zustand führen. Wenn ich wissen muss, was gerade läuft, verlasse ich mich nicht auf die Auskunft einer fremden Komponente, sondern merke es mir selbst — und gleiche regelmäßig ab.
Der dritte Punkt kostet etwas Code und ist der Grund, warum die Portale seitdem zuverlässig funktionieren. Er bedeutet auch: Der Abgleich muss stattfinden, sonst hat man zwei Wahrheiten — und deshalb hängt er bei mir am Ereignis, das die Neuauflage erzeugt, und nicht nur an einem Zeitgeber.
Was ich mitnehme
Fremde Schnittstellen haben Lebenszyklen, die man kennen muss. „Ich hole mir das Objekt und merke es mir" ist eine Annahme über Lebensdauer, die man belegen sollte.
Wenn zwei Fehlersuchen im Protokoll erfolgreich aussehen, ist das Protokoll falsch. Nicht die Suche.
Dekompilieren ist ein legitimes Mittel, um zu verstehen, was passiert. Ich habe dabei drei Eigenschaften gefunden, die in keiner Dokumentation stehen. Ohne diesen Schritt hätte ich weiter geraten.