Die Innenseiten von Retro-Spielen: Punch-Out für NES

Bild

Teil 1. Passwörter


Das Punch-Out von NES-Spiel Mike Tyson verwendet ein Passwortsystem, mit dem Spieler das Spiel von einem bestimmten Punkt aus fortsetzen können. Jedes Passwort besteht aus 10 Ziffern, die zwischen 0 und 9 liegen können. Das Spiel kann zwei Arten von Passwörtern akzeptieren, die ich als "normale" und "spezielle" Passwörter bezeichne. Spezielle Passwörter sind bestimmte Kombinationen von 10 Ziffern, auf deren Eingabe das Spiel auf einzigartige Weise reagiert. Eine vollständige Liste spezieller Passwörter sieht folgendermaßen aus:

  • 075 541 6113 - Besetztzeichen 1
  • 800 422 2602 - Besetztzeichen 2
  • 206 882 2040 - Besetztzeichen 3
  • 135 792 4680 - Spiel in einem versteckten Turnier: "Another World Circuit" (damit das Passwort akzeptiert wird, müssen Sie die Auswahltaste gedrückt halten und A + B drücken)
  • 106 113 0120 - Anzeige der Titel (damit das Passwort akzeptiert wird, müssen Sie die Auswahltaste gedrückt halten und A + B drücken)
  • 007 373 5963 - versetzt den Spieler in den Kampf mit Mike Tyson

Die zweite Art von Passwörtern, die vom Spiel akzeptiert werden, sind reguläre Passwörter. In regulären Passwörtern wird der Fortschritt, den der Spieler im Spiel gemacht hat, verschlüsselt. Die folgenden Spieldaten sind in einem regulären Passwort verschlüsselt:

  • Anzahl der Karrieregewinne
  • Anzahl der Karriereverluste
  • Knockout gewinnt
  • Nächster Gegner

Passwortcodierung


Um beispielsweise die Passwortgenerierung zu untersuchen, verwenden wir ein Spiel mit 24 Siegen, 1 Niederlage, 19 Ausscheidungswettkämpfen und beginnen das Weltturnier mit einem Kampf gegen Super Macho Man.

Der Vorgang des Codierens des Status eines Spiels in einem Passwort beginnt mit dem Sammeln der Anzahl von Gewinnen, Verlusten und Knockouts im Puffer. Das Spiel präsentiert jede Zahl in Form eines binären Dezimalcodes , der aus 8 Bits pro Ziffer und 2 Ziffern für jeden Wert besteht. Das heißt, für 24 Siege benötigen Sie ein Byte mit einem Wert von 2 und ein zweites Byte mit einem Wert von 4. Dasselbe gilt für Paare von Bytes für Verluste und Knockouts, dh es werden insgesamt 6 Bytes Daten erhalten. In der folgenden Abbildung sind diese 6 Bytes mit dezimalen und binären Werten angegeben.


Der nächste Schritt besteht darin, eine Prüfsumme für diese 6 Bytes zu generieren. Das Prüfsummenbyte wird berechnet, indem 6 separate Bytes addiert und das Ergebnis von 255 subtrahiert werden. In unserem Fall 2 + 4 + 0 + 1 + 1 + 9 = 17, dh 255 - 17 = 238.

Dann schreiben wir ein paar Bits von 6 Bytes in einen neuen Puffer. Dieser Puffer kann als ein 28-Bit-Zwischenwert interpretiert werden, den wir Schritt für Schritt ausfüllen werden. Bits aus dem ersten Puffer werden in Zweiergruppen unterteilt und an verschiedene fest codierte Positionen des zweiten Puffers verschoben. Dies ist der erste von mehreren Schritten, deren einzige Aufgabe darin besteht, die Daten einfach zu verschleiern, um das Generieren von Passwörtern für Spieler zu erschweren.


Beachten Sie, dass nicht alle Bits aus dem ursprünglichen Puffer in den neuen Zwischenpuffer übertragen werden. Diese Bits werden ignoriert, da bekannt ist, dass sie immer 0 sind. Dank der Spielregeln reicht es aus, die Anzahl der Verluste im Passwort mit nur 2 Informationsbits zu übertragen. Wenn die Gesamtzahl der Verluste 3 erreicht, tritt ein Spielende auf und der Spieler erhält kein Passwort. Daher reicht es völlig aus, die Anzahl der Verluste mit den Zahlen 0, 1 und 2 zu beschreiben, und dafür reichen nur 2 Bits aus.

Dann schreiben wir andere Bitpaare in den Zwischenpuffer. Die ersten vier Paare werden aus dem zuvor berechneten Prüfsummenwert entnommen. Ein weiteres Paar wird vom Wert des Feindes genommen. Der Wert eines Gegners ist eine Zahl, die angibt, gegen welchen Gegner ein Spieler nach Eingabe eines Passworts kämpfen wird. Drei mögliche feindliche Werte können verwendet werden:

0 - DON FLAMENCO (erster Kampf des großen Turniers)

1 - PISTON HONDA (erster Kampf des Weltturniers)

2 - SUPER MACHO MAN (letzter Kampf des Weltturniers)

Da wir ein Passwort generieren wollten, das uns mit Super Macho Man konfrontiert, verwenden wir 2 als Wert des Gegners. Anschließend werden die Prüfsummenbits und die Werte des Gegners wie folgt in die Zwischenbits geschrieben:


Der nächste Schritt besteht darin, mehrere zyklische Permutationen der Zwischenbits links durchzuführen. Eine zyklische Permutation nach links bedeutet, dass alle Bits um eine Position nach links verschoben werden und das am weitesten links liegende Bit sich bewegt und zum am weitesten rechts liegenden wird. Um die Anzahl der Permutationen auf der linken Seite zu berechnen, nehmen wir die Summe aus dem Wert des Gegners und der Anzahl der Verluste, addieren 1 und dividieren dieses Ergebnis durch 3. In unserem Fall stellt sich heraus, dass 2 + 1 + 1 = 4. Dann ist der Rest von 4/3 1, sodass wir die Zwischenbits einmal zyklisch nach links verschieben.


Zu diesem Zeitpunkt sind die Zwischenbits bereits gründlich gemischt und es ist Zeit, sie zu brechen, um die Zahlen zu erhalten, aus denen das Passwort besteht. Passwörter sollten aus 10 Ziffern bestehen, daher werden 28 Zwischenbits in 10 separate Zahlen aufgeteilt, die wir als Passwortwerte P0, P1, P2 usw. bezeichnen. Jeder der ersten neun Passwortwerte empfängt 3 Datenbits und der letzte erhält nur eines der Zwischenbits. Um den fertigen Passwortwert zu vervollständigen, werden wir auch Bits einfügen, die die Anzahl der im vorherigen Schritt durchgeführten Permutationen angeben.


Schließlich fügen wir jedem Passwortwert einen eindeutigen, fest codierten Offset hinzu. Die fertige Passwortziffer ist der Rest dieser Summe aus der Division durch 10. In der siebten Position verwenden wir beispielsweise den Versatz 1, dh wir erhalten 5 + 1 = 6, und die letzte Ziffer ist der Rest von 6/10, dh 6. In der vierten Position verwenden wir Offset 7, das heißt, wir erhalten 5 + 7 = 12, und die endgültige Zahl entspricht dem Rest 12/10, dh 2.


Also haben wir vorgefertigte Passwortziffern bekommen, die im Spiel überprüft werden können.


Passwortdecodierung


Das Dekodieren von Passwörtern auf die Anzahl der Gewinne / Verluste / Knockouts und den Wert des Gegners ist eine einfache Implementierung in umgekehrter Reihenfolge aller oben beschriebenen Schritte. Ich werde es den Lesern als Aufgabe überlassen. Das Spiel weist jedoch zwei bemerkenswerte Fehler auf, die beim Dekodieren und Überprüfen der vom Spieler eingegebenen Passwörter auftreten.

Der erste Fehler tritt im allerersten Schritt des Dekodierens des Kennworts auf, dh beim Subtrahieren von Offsets, um zu den Kennwortwerten zurückzukehren. Die anfänglichen Passwortwerte enthielten jeweils 3 Datenbits, dh ihre Werte vor dem Anwenden von Offsets sollten im Bereich von 0 bis 7 liegen. Der Spieler kann jedoch ein Passwort eingeben, das nach Abzug des Versatzes einen Passwortwert von 8 oder 9 ergibt (dividiert durch 10 mit dem Rest). Anstatt ein solches Passwort sofort abzulehnen, überprüft das Spiel diesen Fall fälschlicherweise nicht und ermöglicht es Ihnen, dem Passwortwert ein zusätzliches Datenbit hinzuzufügen, das den Satz von Zwischenbits so verschmutzen kann, dass Passwörter nicht mehr eindeutig sind. Da bestimmte Zwischenbits entweder mit der entsprechenden Ziffer des Passworts oder mit einem zusätzlichen Bit des benachbarten Passwortwerts gesetzt werden können, gibt es viele Passwörter, die in denselben Satz von Zwischenbits konvertiert werden können. Aus diesem Grund können Sie verschiedene Passwörter finden, die das gleiche Ergebnis im Spiel liefern, obwohl sie eindeutig sein sollten.

Der zweite Fehler ist ein Fehler in der Logik, mit der das Spiel Daten nach dem Entschlüsseln des Passworts überprüft. Das Spiel versucht, die folgenden Bedingungen anzuwenden:

  • Die im Passwort gespeicherte Prüfsumme entspricht der Prüfsumme, die unter Berücksichtigung der Anzahl der im Passwort gespeicherten Gewinne / Verluste / Knockouts erhalten werden soll
  • Verlustwert ist 0, 1 oder 2
  • Der feindliche Wert ist 0, 1 oder 2
  • Die Anzahl der im Passwort gespeicherten zyklischen Permutationen ist die richtige Anzahl, wobei der Wert der Verluste und der Wert des im Passwort gespeicherten Gegners berücksichtigt werden
  • Alle im Passwort gespeicherten Gewinn- / Verlust- / Ko-Nummern liegen im Bereich von 0 bis 9
  • gewinnt> = 3
  • gewinnt> = Ko

Wenn eine dieser Bedingungen nicht erfüllt ist, muss das Spiel das Passwort ablehnen. Es gibt jedoch einen Fehler bei der Implementierung der Endprüfung (nämlich beim Überprüfen der von BCD codierten Zahlen). Anstatt den Sieg> = Knockouts zu überprüfen, erlaubt das Spiel Fälle, in denen die obere Anzahl der Siege 0, die untere Anzahl der Siege> = 3 und die obere Anzahl der Knockouts kleiner als die untere Anzahl ist gewinnt. Zum Beispiel wird ein Rekord mit 3 Siegen, 0 Verlusten und 23 Ko-Spielen vom Spiel akzeptiert (was das Passwort 099 837 5823 bestätigt), obwohl er abgelehnt werden sollte (da es unmöglich ist, 23 Kämpfe durch Ko zu gewinnen, wenn Sie in nur 3 Kämpfen gewonnen haben).

Fazit


Die besonderen Details eines solchen Codierungsschemas sind nur bei Punch-Out zu finden, aber die allgemeine Idee, wichtige Teile des Spielzustands zu erhalten, sie mit der Möglichkeit der Wiederherstellung zu konvertieren, um den Anfangszustand zu verschleiern, und sie dann zu verwenden, um eine bestimmte Anzahl von Zeichen zu generieren, um dem Spieler als Passwort zu demonstrieren, ist ein ziemlich universeller Ansatz. Sie können Prüfsummen verwenden, damit versehentliche Kennwortänderungen (z. B. wenn ein Spieler einen Fehler macht) meistens zu dessen Ablehnung führen, anstatt ein anderes Kennwort mit einem zufälligen Spielstatus zu erstellen.

Teil 2. Überblick über Punch-Out


Jeder Kämpfer bei Mike Tysons Punch-Out !!! gesteuert durch ein oder mehrere interpretierte Bytecode-Skripte. Der Charakter des Spielers, Little Mac, führt ein einfaches Skript aus, das die Logik jeder Aktion enthält, die dem Spieler zur Verfügung steht (Ausweichen, Blockieren, Schlagen usw.). Die Charaktere des Gegners werden von drei Ebenen unabhängiger Skripte gesteuert, die zusammen das Verhalten des Charakters erzeugen.


Match-Skript


Das Feindskript der höchsten Ebene wird in allen drei Kampfrunden ausgeführt und steuert die ehrgeizigsten Änderungen im Verhalten des Gegners. Ich werde dieses Skript "Match-Skript" nennen. Seine Hauptaufgabe ist es, die Verhaltensweisen auszuwählen, die der Feind als Reaktion auf verschiedene Ereignisse während des Kampfes ausführen wird. Zum Beispiel beginnt ein bestimmtes Verhalten unmittelbar nach dem Aufstehen des Gegners nach einem Niederschlag oder wenn dem Spieler die Herzen ausgehen und er müde wird. Diese Verhaltensweisen werden in die Tabelle geschrieben und von der Spiel-Engine als Reaktion auf die entsprechenden Ereignisse aufgerufen. Das Match-Skript legt auch die Anfangswerte für die Konfigurationsoptionen fest, die sich auf die Komplexität des Kampfes beziehen (z. B. die Zeit, die der Gegner nach einem verpassten Treffer verwundbar bleibt). Schließlich wartet das Match-Skript während des Kampfes auf bestimmte temporäre Markierungen, um Änderungen an den zuvor festgelegten Werten vorzunehmen .

Verhaltensskript


Das Skript eines Gegners auf einer niedrigeren Ebene ist ein "Verhaltensskript". Diese Ebene ist für die Abfolge bestimmter Schläge und Angriffe verantwortlich, die der Gegner im Rahmen des aktuellen Verhaltens ausführen muss (festgelegt durch das Match-Skript). Verhaltensskripte führen Befehle wie „Anwenden des rechten Stoßes, Pause für 28 Frames, zufälliges Anwenden des linken oder rechten Uppercuts, Wiederholen aller es ist 5 mal. " Das Skript verfügt auch über Befehle zum Lesen und Schreiben an eine beliebige Adresse im Speicher der Spiel-Engine, sodass das Verhalten sehr dynamisch sein kann.

Animationsskript


Das Gegner-Skript der untersten Ebene ist ein „Animationsskript“. Solche Skripte führen die Details jedes einzelnen Treffers, Blocks oder Spezialangriffs als Teil des Verhaltens aus (definiert durch das Verhaltensskript). Auf dieser Ebene können Befehle wie "Sprite 23 dem aktuellen Frame der feindlichen Animation zuweisen, es jedes zweite Frame um 1 Pixel nach unten und rechts verschieben." Ändern Sie für die nächsten 10 Bilder das Animationsbild in Sprite 24 und spielen Sie den Soundeffekt 7 "ab. Zusätzlich zu Animationsbefehlen führen Animationsskripte auch Sequenzen verschiedener Änderungen der Spielzustände aus, die eng mit den Bewegungen des Feindes zusammenhängen. In einer langen Animation eines Spezialangriffs kann ein Animationsskript beispielsweise Befehle einfügen, die den Feind mit einem Treffer über einen sehr kurzen Zeitraum für Niederschlag anfällig machen. Wie Verhaltensskripte können Animationsskripte beliebige Speicheradressen in der Spiel-Engine lesen und schreiben, um dynamischere Effekte zu erzielen.

Skript Little Mac


Das vom Charakter des Little Mac-Players ausgeführte Skript ist den feindlichen Animationsskripten am ähnlichsten. Es ändert den aktuellen Frame der reflektierten Animation und bewegt den Player über den Bildschirm. Wie Animationsskripte führt das Little Mac-Skript Sequenzen bestimmter Spielereignisse aus, z. B. zu welchem ​​Zeitpunkt der Mac den Feind treffen sollte oder wann er einen Block oder eine Umgehung ausführen sollte. Das Little Mac-Skript steuert die Eingabe des Players, ähnlich wie Verhaltensskripte feindliche Animationsskripte steuern.

Jedes dieser vier Skripte wird von einem eigenen Interpreter verarbeitet. Obwohl viele von ihnen dieselbe Funktionalität haben, z. B. grundlegende Steuerungssteuerung und direkten Zugriff auf den Speicher, implementiert jedes System seine eigene Version und teilt den gemeinsamen Code (oder den Opcode-Bereich) nicht mit anderen Systemen. Dadurch kann jeder Skripttyp sehr spezifisch sein und effektiv einen kleinen Satz von Zielbefehlen verwenden. Skriptdaten machen etwa 22% der nicht grafischen Daten der Spielekassette aus (der Maschinencode für die Spiel-Engine selbst macht nur 17% aus), daher war es sehr wichtig, dass die Skripte ein kompaktes Aussehen haben.

Teil 3. Punch-Out-Match-Skript


Das Skript des Spiels steuert das Verhalten des Gegners auf höchster Ebene. Die Hauptoperation, die er immer wieder ausführt, besteht darin, auf eine bestimmte Zeit der Runde zu warten und in diesem Moment Änderungen an den Konfigurationsdaten des Gegners vorzunehmen. Das Video zeigt die erste Runde des ersten Kampfes gegen Bald Bull sowie ein Match-Skript, das das Gesamtverhalten steuert.


Es gibt drei Hauptoperationen, die ein Übereinstimmungsskript ausführen kann. Der erste besteht darin, zu warten, bis der runde Timer einen bestimmten Wert erreicht. Die zweite ist zu fragen, ob sich das aktuelle Verhalten des Gegners geändert hat. Verhaltensweisen werden in der Kampfkonfigurationstabelle im Speicher aufgezeichnet und dann zu unterschiedlichen Zeiten von den Match-Skripten und der Spiel-Engine selbst aufgerufen. Die Tabelle enthält zwei Verhaltenssegmente, die von Übereinstimmungsskripten verwendet werden. Ich nenne sie "grundlegendes" Verhalten und "spezielles" Verhalten. Besondere Verhaltensweisen sind beispielsweise ein Bull Charge-Schlag eines Gegners von Bald Bull oder Honda Rush eines Gegners von Piston Honda, und die Hauptverhaltensweisen sind die üblichen Treffer, die der Gegner für den Rest der Zeit liefert. Die spezifischen Verhaltensskripte, die zur Implementierung dieser Verhaltensweisen verwendet werden, können durch das Match-Skript direkt während des Kampfes geändert werden, sodass die Kämpfer mit einem Hauptverhalten beginnen und später zu einem anderen wechseln können (wie im Video zu sehen ist, tut Bald Bull dies, wenn der Timer 0 erreicht : 20.)

Eine Funktion zum Ändern von Verhaltensweisen, die von Match-Skripten ausgeführt werden, besteht darin, dass sie durch Verhaltensänderungen ersetzt werden können, die von der Spiel-Engine angefordert werden. Die Spiel-Engine verwendet vier Verhaltenssegmente, um neue Verhaltensweisen anzufordern, wenn der Mac alle Herzen verliert und müde wird und wenn der Gegner nach einem Niederschlag aufsteht. Wenn das Match-Skript die Anforderung zum Ändern des Verhaltens erfüllt hat, aber eines dieser vier Ereignisse der Spiel-Engine auftritt, bevor die Anforderung verarbeitet wird (Anforderungen können nicht verarbeitet werden, bis der Gegner in den Wartezustand wechselt), legt die Spiel-Engine das gewünschte Verhalten fest und die Anforderung für das Match-Skript wird abgelehnt. Einige Kämpfer, wie beispielsweise Bald Bull, fordern innerhalb kurzer Zeit mehrmals ein besonderes Verhalten. Dies scheint nur erforderlich zu sein, um die Wahrscheinlichkeit zu verringern, dass eine dieser Anforderungen versehentlich gelöscht wird.

Die dritte Hauptoperation des Match-Skripts ist das Patchen des Speichers. Die meisten Speicher-Patches wirken sich auf die Battle-Konfigurationstabelle aus, in der Verhaltensskripte aufgezeichnet werden. Zusätzlich zu den Verhaltenssätzen enthält die Tabelle Daten zur Komplexität des Kampfes. Wenn der Timer im Video beispielsweise 0:30 erreicht, ändert Bald Bull seine Sicherheitseinstellungen. Dies führt dazu, dass der Spieler ihn nicht länger täuschen kann, indem er nach oben drückt und dann einen Schlag auf den Körper ausführt. Match-Skripte können außerdem beliebige Speicheradressen patchen, diese Funktion wird jedoch nur einmal verwendet - zu Beginn der zweiten Runde mit Mike Tyson, sodass der Spieler zum ersten Mal einen Stern erhält, wenn er ihn trifft, der sich im Standby-Modus befindet.

Teil 4. Punch-Out-Verhaltensskript


Nun betrachten wir Verhaltensskripte, die direkt an der Implementierung von Verhalten beteiligt sind.

Das Video zeigt eine Interpretation dessen, wie das konkurrierende Verhaltensskript von Piston Honda 1 in englischen Teams aussehen könnte.


Animationsbefehle


Verhaltensskripte sind für das sequentielle Starten von Animationen verantwortlich, genauso wie Match-Skripte für das Starten von Animationen verantwortlich waren. Der Befehl anim spielt eine bestimmte Animation ab, und der Befehl anim_rnd führt eine Animation aus, die zufällig aus einer Liste von 8 Optionen ausgewählt wurde. Im obigen Video wird zum Zeitpunkt einer zufälligen Auswahl aus einer Liste von Optionen die ausgewählte Option vorübergehend rot hervorgehoben. Wenn Piston Honda seine ersten beiden Stöße ausführt, anim für jeden von ihnen eine anim verwendet. Danach wählt er mit anim_rnd zufällig aus einem Set mit 6 Hook-Animationen und 2 leeren Animationen. Infolgedessen hakt er in 75% der Fälle und unternimmt in 25% der Fälle nichts.

Aus Sicht des Skripts wird das Verhalten der Animationen synchron abgespielt, da der Skriptinterpreter angehalten wird, wenn sich das Animationssystem nicht im Leerlaufmodus befindet.

Ausführungssteuerungsbefehle


Es gibt mehrere Befehle, die die Ausführung des Verhaltensskripts selbst ändern. pause können die Skriptausführung für eine bestimmte Anzahl von Frames oder für die Anzahl von Frames anhalten, die zufällig aus einer Liste von 2 Optionen ausgewählt wurden.

Es gibt verschiedene Verzweigungsbefehle, die unter bestimmten Bedingungen optional zu verschiedenen Teilen des Verhaltensskripts wechseln. Der branch_rnd hat eine bestimmte Wahrscheinlichkeit, dass bei jeder Ausführung eine Verzweigung auftritt. Ein Sonderfall der probabilistischen Verzweigung ist der Befehl branch_always mit einer Verzweigungswahrscheinlichkeit von 100%.

In den Verhaltensskriptinterpreter ist ein einfacher Schleifenmechanismus integriert. Der Befehl set_loop_count legt den aktuellen Wert des Schleifenzählers fest. Jedes Mal, branch_while_loop Befehl branch_while_loop verringert er den Wert des Schleifenzählers um eins und führt nur dann eine Verzweigung durch, wenn der branch_while_loop größer als Null ist.

Die letzte Art der Verzweigung überprüft den Speicherinhalt, um eine Entscheidung über die Verzweigung zu treffen. Piston Honda verwendet diesen Befehl branch_mem_test um zu überprüfen, ob sein letzter Treffer in einem bestimmten Verhalten erfolgreich war. Wenn der Treffer das Ziel trifft, verzweigt er sich für den nächsten Treffer. Wenn der Treffer nicht erfolgreich war, wird der Befehl branch_while_loop , um den Treffer nur dann fortzusetzen, wenn sich 5 fehlgeschlagene Treffer ansammeln.

Verhaltensbefehle


Es gibt zwei Befehle, mit denen Verhaltensskripte das Verhaltenssystem selbst steuern können. Der Befehl begin_behavior_main verwendet, um das aktuelle ausführbare Verhalten zu beenden und das Hauptverhalten zu starten. Dies unterscheidet sich von der Verzweigung im Verhaltensskript, da der Teil des Skripts, der als aktuelles „Hauptverhalten“ betrachtet wird, während des Spiels durch das Übereinstimmungsskript geändert werden kann (siehe den vorherigen Teil des Artikels über Übereinstimmungsskripte).

Ein weiterer verhaltensbezogener Befehl ist enable_behavior_change . Wenn ein neues Verhalten gestartet wird, beginnt es mit einem Sperrstatus, wenn alle weiteren Anforderungen zum Ändern des Verhaltens blockiert sind. Mit dem Befehl enable_behavior_change signalisiert enable_behavior_change Skript, dass es bereit ist, andere Verhaltensweisen zuzulassen. Beispielsweise wird im speziellen Verhalten von Piston Honda der Befehl enable_behavior_change nie ausgeführt. Wenn der Mac während dieser Zeit müde ist, wird das spezielle Verhalten fortgesetzt. Knockdown-Ereignisse umgehen dieses System jedoch. Wenn also während des besonderen Verhaltens des Kolben-Honda die Hauptfigur niedergeschlagen wird, wird das Verhalten in jedem Fall geändert.

Source: https://habr.com/ru/post/de434550/


All Articles