Fessie Räumt Auf Konservierungsprojekt

Bei mir sieht die Datei so aus:

$ hexdump -C data.fcg 
00000000  ff 00 00 00 64 00 00 00  01 01 01 01 00 00 00 98  |....d...........|
00000010  21 00 00 06 00 00 00 6d  61 72 74 69 6e 91 04 00  |!......martin...|
00000020  00 07 00 00 00 6d 72 20  68 65 6c 69 a6 02 00 00  |.....mr heli....|
00000030  02 00 00 00 72 39 32 02  00 00 05 00 00 00 73 6f  |....r92.......so|
00000040  6e 69 63 2e 02 00 00 05  00 00 00 6d 61 72 69 6f  |nic........mario|
00000050  d3 01 00 00 09 00 00 00  61 63 74 72 61 69 73 65  |........actraise|
00000060  72 d0 01 00 00 04 00 00  00 6c 69 6e 6b 4e 01 00  |r........linkN..|
00000070  00 0a 00 00 00 68 65 72  6f 20 74 6f 6e 6d 61 7c  |.....hero tonma||
00000080  00 00 00 09 00 00 00 76  69 63 20 76 69 70 65 72  |.......vic viper|
00000090  64 00 00 00 03 00 00 00  63 69 64                 |d.......cid|
0000009b

cid hat genau 100 Punkte. Hexadezimal wären das 0x64. Außerdem ist „cid“ genau drei bzw. 0x03 Zeichen lang. Der Hexdump nach nach „vic viper“ sieht so aus:

64 00 00 00  03 00 00 00  63 69 64

Wir sehen also zwei 32-bit-Worte, beide Little Endian unsigned int, eines 3 eines 100 bzw. 0x64 gefolgt von einem drei Zeichen langen String. Damit ist das Format für die eigentliche Highscore klar: Punktzahl, Stringlänge, Name.

Dann schauen wir 15-Byte langen Anfang der Datei:

ff 00 00 00  64 00 00 00  01 01 01 01  00 00 00

Wenn man das als 32-bit-Wörter zu lesen versucht, dann hätten wir zum Beispiel 256 und 100 als little endian unsigned int. Danach vier Byte 0x01 und drei Byte 0x01. Eventuell sind sieben ein-Byte-Werte, vielleicht unsigned int oder gecastetes Bool.

Beim obigen Speicherstand hatte ich das Spiel ein einziges Mal gespielt, hatte Level 1 geschafft und bin in Level 2 gestorben. Nachdem ich Level drei geschafft hatte, wurde der Header zu:

ff 00 00 00 64 00 00 00  01 01 01 03 00 00 00
  • Das zwölfte Byte scheint also die Anzahl der abgeschlossenen Level zu sein.
  • Stelle ich von Stereo auf Mono, wird das neunte Byte von 0x01 zu 0x00.
  • Stelle ich die Auflösung von viel (1024×768) auf wenig (800×600) Pixel, wird das zehnte Byte von 0x01 zu 0x00.
  • Stelle ich Detailstufe von „hoch“ auf „niedrig“ wird das elfte Byte von 0x01 zu 0x00.
  • Die ersten beiden 32-Bitwörter repräsentieren die Lautstärken von Geräuschen und in 256 Stufen.
  • Unklar sind mir die drei 0x00-Byte zwischen Levelstand und Highscore.

Werfen wir mal einen Blick auf deine Datei:

  • Geräusche: 0xFF, also 255/255 = 100% (Ursprungzustand)
  • Musik: 0x64, also 100/255 = 39,2% (Ursprungzustand)
  • Tonkanäle: 0x01, also stereo
  • Auflösung: 0x01, also die höhere
  • Details: 0x01, also mit „Details hoch“
  • Freigeschaltete Level: 0x16, also 22 Stück abgeschlossen
  • drei mysteriöse Bytes: alle drei 0x00, was auch immer das bedeutet

Kann man sich da irgendwie schon eintragen?

Wie bringt man die KI dazu, dir beim Reversen des Codes zu helfen?

2 „Gefällt mir“

Das ist auf jeden Fall sehr aufschlussreich, also werden in der Datei alle Einstellungen des Spiels gespeichert - ist ja überschaubar. :eyes:

Im Prinzip ja, allerdings hatte ich die benötigte Komponente vom Server genommen - kann ich heute Abend wieder hochladen.

„Ich restauriere derzeit das alte Windows-PC-Spiel „Fessie räumt auf“ aus dem Jahre 2001. Unter Windows 11 habe ich es bereits wieder zum Laufen bekommen, allerdings verstehe ich den Highscore-Code nicht. Leider liegt mir der Quellcode nicht vor, da dieser schon vor Jahren gelöscht wurde… [Hierhabe ich alles beschrieben, was ich bezüglich der Übermittlung des Codes schon sehen konnte] In Ghidra habe ich die Stelle mit der URL bereits gefunden, nur verstehe ich nicht, wie der Code generiert wird. Kannst du mir bitte bei der Analyse helfen? Ziel ist es, ein neues Highscore-System zu erstellen, welches die Codes aus dem Original-Spiel validieren kann, um die Echtheit der Einträge zu verifizieren.“

Dann wurde ich nach und nach nach dem relevanten Code aus Ghidra gefragt und habe alle Funktionen erklärt bekommen. Es kamen immer weitere Fragen, zu welchen Funktionen ich den Code schicken sollte und am Ende hieß es: „Perfekt, damit haben wir jetzt alle relevanten Puzzleteile zusammen und wissen, wie die Übermittlungs-Codes berechnet werden.“

Nur der Teil mit dem Zeitstempel ist noch ein Problem, weshalb aktuell lediglich die Codes selbst validiert werden können - aber da habe ich noch 2 Ideen, die ich nochmal ausprobieren werde. :nerd_face:

2 „Gefällt mir“

… und die lokale Highscore, aber ohne Codes zur Eintragung.

Ja, die Codes werden erst generiert, kurz bevor sich im Spiel das Menü öffnet, von wo aus man seinen neuen Highscore-Eintrag übermitteln kann und zu der Website weitergeleitet wird (in meiner 2021er Version ja nicht mehr vorhanden, kann aber nachher eine Version bereitstellen, wo die neue URL hinterlegt ist).

1 „Gefällt mir“

Dient die Zeit nicht einfach nur als Seed für einen Mersenne Twister, um ein wenig Zufall in den Output-String zu bekommen, damit der selbe Highscore nicht immer einen identischen Code erzeugt?

1 „Gefällt mir“

Ja, das scheinr richtig zu sein - zu der Erkenntnis bin ich zumindest auch gekommen. :ok_hand:
Leider wirkt sich das aber nicht positiv auf die Validierungsmöglichkeiten aus - ich denke mal, deshalb wurde mit Prüfsummen gearbeitet… :thinking:

Es muss ja auch nur validiert werden, dass die Highscore-Zahl auch die ist, die vom Spiel angezeigt wurde (und nicht nachher manipuliert wurde); da reicht es im Grunde, im Spiel die 32 Bits der Highscore-Zahl irgendwie eindeutig auf die 48 Bits des Codes zu verteilen, damit man nachher auf Server-Seite nur die eingegebenen 32 Bit wieder verteilt und schaut, ob sie mit den entsprechenden Bits im Code übereinstimmen.

2 „Gefällt mir“

Um endlich Klarheit zu schaffen, hab ich den gesamten Code, welcher zwischen Highscore-Erzeugung und -Übermittlung stattfindet nochmal komplett mit allen Funktionen, Variablen etc. in die KI gehauen und immer wieder nachgehakt…

Hier die wichtigsten Ergebnisse:

KI-generierte Aussagen:

Dass der Name und Score für die Berechnung eine Rolle spielen, scheint sich somit erledigt zu haben - diese werden offensichtlich nur für die Übergabe an den Server einbezogen… :man_shrugging:

3 „Gefällt mir“

Fessie räumt auf inkl. Online-Highscore

Hier nun eine funktionierende Version mit Übermittlungsmöglichkeit an die Highscore-Seite:
https://mirrors.max-reimann.de/games/fessie/Fessie-raeumt-auf_mit-Online-Highscore.zip

Da ich mir kein Code-Signing-Zertifikat leisten kann/möchte, meckert Windows natürlich beim Start-Versuch gerne und blockiert die Ausführung. Die beste Lösung ist da meiner Meinung nach ein Rechtsklick auf die Datei (egal ob ZIP oder EXE) und dann auf „Eigenschaften“:

Hier sind dann die Top 20 zu sehen (Design wird noch überarbeitet):
https://fessie.max-reimann.de/

Außerdem gibt es zusätzlich die Möglichkeit, existierende Codes manuell zu validieren:
Beispiel: https://fessie.max-reimann.de/?code=A94C-9BA3-B9B8

Mal sehen, was da so passiert… :eyes:

2 „Gefällt mir“

Habe es eben mit Winlator ausprobiert. Die Grafik glitcht ganz seltsam mit dxvk, bei wined3d klappt es aber.


Den Link im Browser kann ich in Winlator nicht öffnen. In Wine würde es wohl gehen. Muss mich korrigieren: Bei deiner Version ist der Link doch aufgegangen, bei der ursprünglichen nicht.

1 „Gefällt mir“

Bin erstaunt, dass es in Winlator überhaupt läuft… :open_mouth: Ale native Android-App wäre es natürlich auch nicht schlecht. :thinking:

Naja, auf jeden Fall hat die Eintragung in der Tabelle funktioniert. :ok_hand:
https://fessie.max-reimann.de/?code=2C5E-1C7A-F70C

1 „Gefällt mir“

Webanwendung! Automatisch quasi überall spielbar :beancool:

2 „Gefällt mir“

Dafür gäbe es ja zumindest schon einen Anfang:

Habe ich zu Testzwecken hier mal gehostet (und den Hintergrund aus dem Spiel hinzugefügt), ist auf dem PC auf jeden Fall spielbar:
https://fessie.max-reimann.de/FessieReborn/

Derzeit fehlt allerdings die Optimierung für mobile Endgeräte sowie eine entsprechende Steuerung.

Die unterstützte Levelgröße müsste sich ja hochsetzen lassen. Die normalen Levels sollten sich damit theoretisch nachbauen (oder umwandeln) lassen. Die Art des Mülls wird allerdings zufällig platziert, es gibt nur eine Unterscheidung zwischen „normalem“ und Sondermüll. Für die Bonus-Levels müsste man das ändern.

Leider reichen meine JavaScript-Kenntnisse nicht aus, um hier in absehbarer Zeit etwas brauchbares auf die Beine zu stellen - da findet sich doch bestimmt jemand mit mehr Erfahrug… :thinking:

1 „Gefällt mir“

Die Levelstruktur ist anders als vom Orginalspiel. Die Web Anwendung nutzt andere IDs. Automatisch Fessie-Level einlesen/exportieren kann die Webanwendung leider nicht, das hab ich nachgeschaut bevor ich den Levelconverter gebastelt hab. Das müsste man erst noch einbauen.
Auch fehlen noch ein paar Feinschliffe wie Animationen, Menü, Levelübergänge, Bonuslevel, oder zb die Kamera bewegt sich im Orginalspiel am Levelrand nicht mit. Oder das Verhalten der Sternchen ist leicht anders als im Orginalspiel.

Erfahrung würd bei mir reichen, Zeit aber leider nicht.

1 „Gefällt mir“

Kann man mit boxedwine aus der Windows-Version versuchen oder halt den nativen Web-Klon von DanielEnglisch nehmen

Das Spiel überträgt eigentlich nur Kleinbuchstaben… :thinking:

2 „Gefällt mir“

:nun:

4 „Gefällt mir“

Das ist ja wunderschön geworden!

Falls jemand mit dem unveränderten Original spielen und trotzdem in der Highscore erscheinen will, ist hier ein winziges Userscript (für Greasemonkey, Tampermonkey, u.Ä.)

// ==UserScript==
// @name     Fessie räumt auf Highscore Redirector
// @version  1
// @grant    none
// @include  https://www.fessie.de/highscore.php3?score=*&name=*&code=*
// ==/UserScript==
var query = document.location.search;
document.location.replace('https://fessie.max-reimann.de/h.php' + query);

Falls jemand unter Android mit winlator spielen will, gibt es hier auch noch die Fessie.icp. Das ist der virtuelle Controller.

{
  "id": 4,
  "name": "Fessie",
  "cursorSpeed": 1,
  "elements": [
    {
      "type": "D_PAD",
      "shape": "CIRCLE",
      "bindings": [
        "KEY_UP",
        "KEY_RIGHT",
        "KEY_DOWN",
        "KEY_LEFT"
      ],
      "scale": 1,
      "x": 0.10962963104248047,
      "y": 0.6222222447395325,
      "toggleSwitch": false,
      "text": "",
      "iconId": 0
    },
    {
      "type": "BUTTON",
      "shape": "CIRCLE",
      "bindings": [
        "KEY_CTRL_R",
        "NONE",
        "NONE",
        "NONE"
      ],
      "scale": 1,
      "x": 0.8600000143051147,
      "y": 0.4888888895511627,
      "toggleSwitch": false,
      "text": "TASTEN",
      "iconId": 0
    },
    {
      "type": "BUTTON",
      "shape": "CIRCLE",
      "bindings": [
        "KEY_SHIFT_L",
        "NONE",
        "NONE",
        "NONE"
      ],
      "scale": 1,
      "x": 0.7900000214576721,
      "y": 0.644444465637207,
      "toggleSwitch": false,
      "text": "FEUER",
      "iconId": 0
    },
    {
      "type": "BUTTON",
      "shape": "CIRCLE",
      "bindings": [
        "KEY_SPACE",
        "NONE",
        "NONE",
        "NONE"
      ],
      "scale": 1,
      "x": 0.9300000071525574,
      "y": 0.644444465637207,
      "toggleSwitch": false,
      "text": "BOMBE",
      "iconId": 0
    },
    {
      "type": "BUTTON",
      "shape": "CIRCLE",
      "bindings": [
        "KEY_ENTER",
        "NONE",
        "NONE",
        "NONE"
      ],
      "scale": 1,
      "x": 0.8600000143051147,
      "y": 0.7777777910232544,
      "toggleSwitch": false,
      "text": "ENTER",
      "iconId": 0
    },
    {
      "type": "BUTTON",
      "shape": "SQUARE",
      "bindings": [
        "KEY_ESC",
        "NONE",
        "NONE",
        "NONE"
      ],
      "scale": 1,
      "x": 0.07000000029802322,
      "y": 0.1111111119389534,
      "toggleSwitch": false,
      "text": "ESC",
      "iconId": 0
    }
  ]
}
3 „Gefällt mir“

Ich habe mal ein kleines Stück JavaScript geschrieben, das eine Highscore-Datei ausliest. Vielleicht kann man daraus mal eines Tages ein Tool bauen, das zwei Highscore-Dateien zu einer verschmilzt. Oder einen Downloader, der die Online-Highscore an die eigene Highscore dranschreibt oder so.

Es ist ziemlich unspannend. Mit der Dateiauswahl lädt man die Highscore-Datei hoch. Im Ausgabefeld darunter stehen dann die ausgelesenen Einstellungen und Highscore. Ihr könnt das zum Beispiel hier reinpasten, um es schnell auszuprobieren: Create a new tinker - JSTinker

Javascript:

fileinput = document.getElementsByTagName("input")[0];
resultpre = document.getElementById("result");
fileinput.addEventListener('change', function() {
    var reader = new FileReader();
    reader.onload = function() {

    var arrayBuffer = this.result,
      array = new Uint8Array(arrayBuffer),
      binaryString = String.fromCharCode.apply(null, array),
      view = new DataView(arrayBuffer),
      offset = 0;
      
    console.log(binaryString, arrayBuffer);
    resultpre.append("Geräusche: " + view.getUint32(offset, true) + "\n");
    offset += 4;
    resultpre.append("Musik: " + view.getUint32(offset, true) + "\n");
    offset += 4;
    resultpre.append("Stereo: " + view.getUint8(offset, true) + "\n");
    offset += 1;
    resultpre.append("Auflösung: " + view.getUint8(offset, true) + "\n");
    offset += 1;
    resultpre.append("Details: " + view.getUint8(offset, true) + "\n");
    offset += 1;
    resultpre.append("Level: " + view.getUint32(offset, true) + "\n");
    offset += 4;
    while(offset < view.byteLength) {
        score = view.getUint32(offset, true);
        offset += 4;
        strlen = view.getUint32(offset, true);
        offset += 4;
        username = "";
        for(charpos =0; charpos<strlen; charpos++)
        {
            username += String.fromCharCode(view.getUint8(offset, true));
            offset++;
        }
        resultpre.append(score + " "+ username +"\n");
    }
  }
  reader.readAsArrayBuffer(this.files[0]);
  console.log(fileinput);
}, false);

html:

<input type="file">
<pre id="result"></pre>

Wenn man ganz verrückt ist, könnte man den Level-Converter von @etothepii mit derselben Technik Javascript nachbauen.

Ganz anderes Thema:

Sag mal @Fessie was macht der der Funktion 0x0040fe7a(int *param_1) eigentlich der param_1?

1 „Gefällt mir“

Interessante Idee. :eyes:

Hier die Funktion um die es geht:

void __cdecl FUN_0040fe7a(int *param_1)

{
  DWORD DVar1;
  int iVar2;
  _TIME_ZONE_INFORMATION local_d0;
  _SYSTEMTIME local_24;
  _SYSTEMTIME local_14;
  
  GetLocalTime(&local_14);
  GetSystemTime(&local_24);
  if (local_24.wMinute == DAT_0042b7f0._2_2_) {
    if (local_24.wHour == (WORD)DAT_0042b7f0) {
      if (local_24.wDay == DAT_0042b7ec._2_2_) {
        if (local_24.wMonth == DAT_0042b7e8._2_2_) {
          if (local_24.wYear == (WORD)DAT_0042b7e8) goto LAB_0040ff24;
        }
      }
    }
  }
  DVar1 = GetTimeZoneInformation(&local_d0);
  if (DVar1 == 0xffffffff) {
    DAT_0042b7e0 = -1;
  }
  else if (((DVar1 == 2) && (local_d0.DaylightDate.wMonth != 0)) && (local_d0.DaylightBias != 0)) {
    DAT_0042b7e0 = 1;
  }
  else {
    DAT_0042b7e0 = 0;
  }
  DAT_0042b7e8._0_2_ = local_24.wYear;
  DAT_0042b7e8._2_2_ = local_24.wMonth;
  DAT_0042b7ec._0_2_ = local_24.wDayOfWeek;
  DAT_0042b7ec._2_2_ = local_24.wDay;
  DAT_0042b7f0._0_2_ = local_24.wHour;
  DAT_0042b7f0._2_2_ = local_24.wMinute;
  DAT_0042b7f4._0_2_ = local_24.wSecond;
  DAT_0042b7f4._2_2_ = local_24.wMilliseconds;
LAB_0040ff24:
  iVar2 = FUN_00410ce1((uint)local_14.wYear,(uint)local_14.wMonth,(uint)local_14.wDay,
                       (uint)local_14.wHour,(uint)local_14.wMinute,(uint)local_14.wSecond,
                       DAT_0042b7e0);
  if (param_1 != (int *)0x0) {
    *param_1 = iVar2;
  }
  return;
}

Und dazu die:

int __cdecl
FUN_00410ce1(int param_1,int param_2,int param_3,int param_4,int param_5,int param_6,int param_7)

{
  bool bVar1;
  undefined3 extraout_var;
  int iVar2;
  uint uVar3;
  int iVar4;
  int local_28 [2];
  int local_20;
  int local_18;
  uint local_14;
  int local_c;
  
  uVar3 = param_1 - 0x76c;
  if (((int)uVar3 < 0x46) || (0x8a < (int)uVar3)) {
    iVar2 = -1;
  }
  else {
    iVar4 = *(int *)(&DAT_0041ffdc + param_2 * 4) + param_3;
    if (((uVar3 & 3) == 0) && (2 < param_2)) {
      iVar4 = iVar4 + 1;
    }
    FUN_00415594();
    local_20 = param_4;
    local_18 = param_2 + -1;
    iVar2 = ((param_4 + (uVar3 * 0x16d + iVar4 + (param_1 + -0x76d >> 2)) * 0x18) * 0x3c + param_5)
            * 0x3c + DAT_0041fef8 + 0x7c558180 + param_6;
    if ((param_7 == 1) ||
       (((param_7 == -1 && (DAT_0041fefc != 0)) &&
        (local_14 = uVar3, local_c = iVar4, bVar1 = FUN_00415807(local_28),
        CONCAT31(extraout_var,bVar1) != 0)))) {
      iVar2 = iVar2 + _DAT_0041ff00;
    }
  }
  return iVar2;
}

Was hier passiert:

[Aufruf von FUN_0040fe7a]
        │
        ▼
[Hole Systemzeit: GetSystemTime → local_24]
[Hole Lokalzeit: GetLocalTime → local_14]
        │
        ▼
[Vergleiche local_24 mit gespeicherten Werten]
        │
     (Falls Zeit neu ist)
        ▼
[Berechne Zeitzonen-Offset mit GetTimeZoneInformation]
→ Ergebnis wird in DAT_0042b7e0 gespeichert
        │
        ▼
[Aktualisiere globale Zeitstruktur:
 DAT_0042b7e8 bis DAT_0042b7f4]
        │
        ▼
[Berechne Zeitwert mit FUN_00410ce1]
 Übergabe:
  ┌────────────────────────────────────────────┐
  │ param_1 = Jahr        → local_14.wYear     │
  │ param_2 = Monat       → local_14.wMonth    │
  │ param_3 = Tag         → local_14.wDay      │
  │ param_4 = Stunde      → local_14.wHour     │
  │ param_5 = Minute      → local_14.wMinute   │
  │ param_6 = Sekunde     → local_14.wSecond   │
  │ param_7 = ZeitOffset  → DAT_0042b7e0       │
  └────────────────────────────────────────────┘
        │
        ▼
[Funktion FUN_00410ce1 führt Zeitberechnung durch]
        │
        ▼
[Ergebnis = iVar2]
        │
        ▼
[Wenn param_1 ≠ NULL → *param_1 = iVar2]
        │
        ▼
[Rückgabe beendet – param_1 enthält Zeitwert]

Dort wird also der Zeitstempel (zwischen-)gespeichert.

1 „Gefällt mir“