Beiträge: 2.178
Themen: 31
Registriert seit: Mar 2013
Bewertung:
14
Ich finde die Schrift im ersten Bild am angenehmsten zu lesen.
Ist das Zufall oder gewollt, dass im Nachbau die Zeilen mit dem jeweils gleichen Wort des Originals enden?
"Alrik war durstig und hat getrunken."
Beiträge: 3.248
Themen: 115
Registriert seit: Aug 2006
Bewertung:
23
Ich denke, das ist technisch bedingt - genau das würde sich mit einem "eigenen" Zeilenumbruch ja ändern, womit man die Textbox effizienter und sinnvoller befüllen könnte.
Beiträge: 202
Themen: 2
Registriert seit: May 2020
Bewertung:
13
04.11.2023, 17:24
(Dieser Beitrag wurde zuletzt bearbeitet: 04.11.2023, 17:26 von cmfrydos.)
Danke für eure Ideen, ich bin ganz eurer Meinung. Es wäre zwar möglich, den Text dem Original sehr ähnlich zu gestalten, doch würde das an Lesbarkeit kosten. Also besser wir nehmen Schriftschnitt Medium, normale Leerzeichen, und etwas kleineren Text, bzw. größeren Zeilenabstand - im Original können zwei Buchstaben 'verschmelzen', wenn bspw. ein 'g' über einem 'i' steht. Um gewisse Zeilenabstände zu gewährleisten, müsste man die Textboxen aber selbst malen, auch um einen eigenen Textfluss zu ermöglichen:
(03.11.2023, 11:17)aeyol schrieb: Ich denke, das ist technisch bedingt - genau das würde sich mit einem "eigenen" Zeilenumbruch ja ändern, womit man die Textbox effizienter und sinnvoller befüllen könnte.
Genau, aktuell habe ich nur den Befehl für das Zeichnen von Textketten ersetzt. Der nächste Schritt wäre, den Befehl zum Malen der Textboxen anzupassen. So könnte man längere Texte selbstständig umbrechen und die Größe der Textboxen entsprechend anpassen. Das sollte subtil genug sein, um nicht aufzufallen – ob eine Box nun 5 oder 7 Zeilen hat, dürfte kaum jemanden stören. Hauptsache, die Texte bleiben gut lesbar und die Box sieht nicht fehl am Platz aus. Ein zusätzlicher, bisher nicht genannter Vorteil wäre, dass ich die Textboxgrafiken nicht strecken müsste, wie man es im ersten Beispiel meines letzten Posts passiert ist.
Was PT Mono betrifft, werde ich das später ausprobieren, aber ich bezweifle, dass die Originalschriftart eine Monospace-Font ist. Besonders bei Buchstaben wie 'i' oder 't' sieht man die schmalere Breite – und umgekehrt beim Leerzeichen, wie weit es ist. Also scheint es, dass jeder Buchstabe seine eigene fixe Breite hat – aber noch nicht ganz so ausgefeilt wie bei modernen Schriftarten, die Abstände abhängig von den nebeneinanderstehenden Buchstaben anpassen (Stichwort 'Kerning').
Beiträge: 202
Themen: 2
Registriert seit: May 2020
Bewertung:
13
31.01.2024, 22:48
(Dieser Beitrag wurde zuletzt bearbeitet: 02.02.2024, 21:04 von cmfrydos.
Bearbeitungsgrund: Formatierung / Ergänzung
)
Ich hatte mich kürzlich mal mit dem BOB-Dateiformat auseinander gesetzt, die offenen Fragen des Wikis klären können, und den Dateiaufbau zusammen gefasst. Zu Dokuzwecken poste ich es mal hier:
[BOB-Dateiformat in RIVA]
Grundsätzlich alles Unsigned / Little Endian.
BOBs sind gif-ähnliche Bilddateien, die aus einem Hauptbild und vielen kleinen Ausschnitts-Animationen bestehen. Das sind hauptsächlich die zahlreichen Animationen, wenn Bewohner ihre Tür öffnen und die Helden ansprechen. Hier bewegen sich Mund, Augen, Stirnrunzeln usw. unabhängig voneinander und oft auch mit unterschiedlichen Animationslängen, sodass der Gesamteindruck nicht repetitiv wirkt.
Datenaufbau:
Header: 16 Bytes + ein 0x00-Trennbyte
---------------------------------------------------------------------
Off Typ Name Anmerkung
---------------------------------------------------------------------
0 char[3] Signatur 3 Bytes Signatur "BOB"
3 char Trennbyte Ein 0x00-Trennbyte
4 short Version 2 Bytes Version 0x0101
6 byte AnzAnimation 1 Byte Anzahl der Animationen
7 short Konstante 2 Bytes immer 0x010A
9 char[8] Leer8Bytes 8 Bytes immer leer
17 byte LeerByte 1 Byte immer leer, Trennung zum folgenden Abschnitt
---------------------------------------------------------------------
Gibt es Animationen, folgt nun eine Beschreibung, welche (Achtung, einsbasierte) Bildindizes für wie lange gezeigt werden. Dieser Teil der Daten ist doppelt vorhanden! Die einzelnen Teile kommen auch in der jeweiligen Beschreibung der Animation (im Animationsheader) erneut vor.
Miniheader, fasst Längen der nachfolgenden Datenpakete zusammen.
---------------------------------------------------------------------
Datenlängen - Miniheader
---------------------------------------------------------------------
Off Typ Name Anmerkung
---------------------------------------------------------------------
0 char DatenLaenge_1 1 Byte Datenlänge für Animation 1 (Größe in Bytes / 4)
1 char DatenLaenge_2 1 Byte Datenlänge für Animation 2 (Größe in Bytes / 4)
2 char DatenLaenge_3 1 Byte Datenlänge für Animation 3 (Größe in Bytes / 4)
... ... ... ...
n-1 char DatenLaenge_n 1 Byte Datenlänge für Animation n (Größe in Bytes / 4)
---------------------------------------------------------------------
Nun folgen dem Datenlängen - Miniheader die eigentlichen Datenpakete, von denen es für jede Animation eine gibt:
---------------------------------------------------------------------
Animationen Datenpakete
---------------------------------------------------------------------
Off Typ Name Anmerkung
---------------------------------------------------------------------
0 byte StartByte Immer 0x00
1 byte DatenLaenge Datenlänge für die folgende Animationsbeschreibung (entspricht Miniheader)
2 word Bildindex_1 1 Word Bildindex für Animation 1 (einsbasiert)
4 word Anzeigedauer_1 1 Word Anzeigedauer für Animation 1
6 word Bildindex_2 1 Word Bildindex für Animation 2 (einsbasiert)
8 word Anzeigedauer_2 1 Word Anzeigedauer für Animation 2
... ... ... ...
n-4 word Bildindex_n 1 Word Bildindex für Animation n (einsbasiert)
n-2 word Anzeigedauer_n 1 Word Anzeigedauer für Animation n
---------------------------------------------------------------------
n = 2 + DatenLaenge * 4
Die Anzeigedauer ist in einer Einheit von ~45 ms, das heißt ein Bild, das für 10 Anzeigeeinheiten gezeigt wird, wird für etwa 450 ms angezeigt. Sowohl Bildindex als auch Anzeigedauer sind immer < 256, außer bei Adran. Adrans BOB Animation wird jedoch nie im Spiel gezeigt, sodass sich der Effekt nicht beobachten lässt. Was etwas verwunderlich ist, dass die Alternative "Kein Bild", d.h., den Ausschnitt des Hauptbildes zu verwenden, scheinbar nie benutzt wird.
---------------------------------------------------------------------
Header (0 ist auch 'Headerbeginn')
---------------------------------------------------------------------
Off Typ Name Anmerkung
---------------------------------------------------------------------
0 dword Headerlaenge 1 DWord Headerlänge (einschließlich dieser 4 Bytes)
4 dword OffsetDatenEnde 1 DWord Offset zum Datenende vom Headerbeginn aus
8 word BreiteHauptbild 1 Word Breite Hauptbild
10 byte HoeheHauptbild 1 Byte Höhe Hauptbild
11 byte AnzOffsets 1 Byte Anzahl der Offsets (entspricht Anzahl der Animationen)
12 udword OffsetAnimation1 1 UDWord Offset zur Startposition des Animationsheader 1 vom Headerbeginn aus
16 udword OffsetAnimation2 1 UDWord Offset zur Startposition des Animationsheader 2 vom Headerbeginn aus
... ... ... ...
n-4 udword OffsetAnimation_AnzOffsets 1 UDWord Offset zur Startposition der Animation n
---------------------------------------------------------------------
n = 12 + AnzAnimationen * 4
Nun folgt für jede Animation ein Animationsheader
---------------------------------------------------------------------
Animationsheader
---------------------------------------------------------------------
Off Typ Name Anmerkung
---------------------------------------------------------------------
0 char[4] Name 4 Bytes Name/Abkürzung der Animation
4 word XOffset 1 Word X-Offset
6 byte YOffset 1 Byte Y-Offset
7 byte Hoehe 1 Byte Höhe
8 word Breite 1 Word Breite
10 byte ImmerNull 1 Byte Immer 0
11 byte AnzTeilbilder 1 Byte Anzahl der Teilbilder
12 udword StartOffset_1 1 UDWord Startoffset der Bilddaten vom Start des Hauptheaders aus für Teilbild 1
16 udword StartOffset_2 1 UDWord Startoffset der Bilddaten vom Start des Hauptheaders aus für Teilbild 2
... ... ... ...
n-4 udword StartOffset_n 1 UDWord Startoffset der Bilddaten vom Start des Hauptheaders aus für Teilbild n
n byte AnzFramezeiten 1 Byte Anzahl der Framezeiten (entspricht der Anzahl aus dem Miniheader)
n+1 dword Framezeit_1 1 DWord Framezeit für Teilbild 1 (entspricht den Daten aus dem Miniheader: 2 Byte Bildindex + 2 Byte Anzeigedauer)
n+5 dword Framezeit_2 1 DWord Framezeit für Teilbild 2
... ... ... ...
m-4 dword Framezeit_o 1 DWord Framezeit für Teilbild o
---------------------------------------------------------------------
m = n + 1 + 4 * AnzFramezeiten
n = 12 + 4 * AnzTeilbilder
Nun folgen die eigentlichen Bilddaten. Es lohnt sich an dieser Stelle zu bestimmen, ob die Daten komprimiert wurden. Dies steht am Datenende.
Wenn die Daten komprimiert sind, sind diese PP20 komprimiert, wobei anstatt der Signatur "PP20" die komprimierte Größe, die 4 Bytes eingeschlossen, am Anfang steht.
Die Daten des Hauptbildes und jede der Animationen sind dabei einzeln komprimiert, wobei die Teilbilder einer Animation zusammen komprimiert vorliegen. D.h. nach dem Entpacken haben wir:
- Hauptbild-Breite mal Höhe Daten für das Hauptbild und
- Anzahl der Teilbilder * dessen Höhe mal dessen Breite an Bytes für jede Animation. Diese Daten muss man dann einfach in gleich große Pakete für die Einzelbilder zerteilen.
---------------------------------------------------------------------
Datenende
---------------------------------------------------------------------
Off Typ Name Anmerkung
---------------------------------------------------------------------
0 word Unbekannt_1 2 unbekannte Words < 60
2 word Unbekannt_2
4 byte FFByte 1 Byte immer 0xFF
5 byte Komprimiert 1 Byte 1 bei komprimierten Daten, 0 bei unkomprimierten Daten
6 byte[768] Palette_RGB 256 * 3 Bytes Rot-Grün-Blau-Werte der Palette
---------------------------------------------------------------------
Wie Shihan hier entdeckte, muss man jeden Farbwert leicht verändern: c = (c & 0x3f) * 4. Was in den höchsten 2 Bits kodiert ist, ist noch unbekannt.
Zum Anzeigen muss man nun das Hauptbild zeichnen und jede Animation starten. Diese laufen dabei durch ihre (Index, Dauer) - Liste und rendern das dem Index entsprechende Teilbild. Die Zeichenreihenfolge ist dabei noch unklar. Die Kräuterhändlerin beispielsweise sieht immer falsch aus. Entweder bewegt sich nicht der Mund, weil die Tasse diesen überdeckt, oder beim Pusten sehen die Backen falsch aus. Was bei ihr funktioniert, ist statt Index-1 Bilder (die Ersten) zu zeichnen, die Animation auszulassen, d.h., den Ausschnitt vom Hauptbild zu verwenden. Dieses Vorgehen wiederum funktioniert bei anderen .BOBs gar nicht. Dies bleibt also noch ein letztes Rätsel. Ich habe mal ein Screenshot des ungewöhnlichen .BOB von Adran, das ich im Spiel nicht triggern konnte, beigefügt:
Beiträge: 202
Themen: 2
Registriert seit: May 2020
Bewertung:
13
03.02.2024, 17:32
(Dieser Beitrag wurde zuletzt bearbeitet: 05.02.2024, 13:41 von cmfrydos.
Bearbeitungsgrund: Komprimierungsmodus 0x02 in AIF
)
Mhh, ich weiß nicht wie ich das übersehen konnte, aber ich habe wohl etwas mühselig gezeigt, dass das "Neue Format", einfach das "Alte Format" mit einem vorangestellten Pre-Header ist. Ab dem 'Headerbeginn' sind beide Formate identisch.
Der Sinn dahinter erschließt sich mir noch nicht ganz. Vielleicht hat der Pre-Header das Abstimmen der Animationen etwas erleichtert?
@Shihan, falls du mich zum Wiki / Repo hinzufügen magst, kann ich die Artikel zu BoB, 3DM und einigen anderen Formate gerne ergänzen. Da kam jetzt über das letzte Jahr doch so einiges zusammen.
Ich glaube, wir haben dann schon fast alle Formate in Riva geknackt! Was fehlt, sind noch .MOV/.MOF für die Autobewegungen, was sich mit eigenen 'Aufnahmen' in der 1.62 Demo wohl leicht entziffern lässt, sowie 5 Formate von dem MODULEAUTOMAP (.ANN .APA .LST .MSK .MST), die aber übersichtlich aussehen. Ah und dann noch die Dialogformate .LXT und .XDF . Und zuletzt fehlt noch der Komprimierungsmodus 0x02 in AIF, den man noch ausmachen muss.
Ergänzung:
Ich hatte zuletzt noch Spaß mit dem Komprimierungsmodus 0x02 in AIF, der in Riva zwar nur von einem Bild benutzt wird, aber dann doch nachgegeben hat:
Es ist eine Form von Lauflängenkodierung (RLE).
Im Endeffekt muss man immer nur 1 Byte b lesen und entscheiden, ob dieses < 128 ist:
Fall I) b < 128 leitet eine Folge von b + 1 Bytes ein, b selbst nicht mitgezählt, die 1:1 in das Ergebnis kopiert werden.
Fall II) b >= 128 steht immer vor einem Farbcode c. c wird nun 257 - b mal in das Ergebnis geschrieben.
Anders ausgedrückt: Das komprimierte Bild ist also in Pakete b_0, p_0, b_1, p_1, b_2, p_2, b_3, p_3, ... unterteilt,
wobei jedes b_n die Länge von p_n diktiert, und besagt, ob man p_n entweder ein- oder mehrfach in das Ergebnis schreibt.
Auf Fall I) folgt wohl immer Fall II), außer eine Folge ist zu lang, aber dies kommt in dem einen Rivabild nicht vor.
Dass die Fallgrenze bei < 128 liegt, habe ich auch nur geraten, und könnte man nur verifizieren, falls dieser Komprimierungsmodus z.B. auch in Sternenschweif oder der Schicksalsklinge benutzt wird.
Beiträge: 302
Themen: 2
Registriert seit: Oct 2012
Bewertung:
3
Hi cmfrydos,
bin nach wie vor absolut überwältigt, wie weit Du hier gekommen bist, nachdem ich den Reverse-Engineer-Hut an den Nagel gehängt habe. Grandios, chapeau dafür!
Ich habe Dich mal als Collaborator für das Wiki eingeladen. Sag Bescheid, wenn das klappt oder auch nicht klappt.
Bin lange weg gewesen, weil mein RL mich gewaltig beschäftigt hat. Bis auf ein paar Arbeiten an einem Factorio-Projekt (YAFC CE) habe ich auch wenig hobbymäßiges Entwickeln gemacht. Alles ein wenig eingeschlafen.
Aber wenn Du im Wiki ein paar Dinge nachpflegst, schau ich gerne drüber und gebe meinen Senf ab, wenn gewünscht.
Auch Feedback und / oder Hilfe bei Deinem RivaHDViewer kann ich gerne geben, wenn es was konkretes gibt, wo Du Input brauchst. Kann nicht garantieren, dass ich innerhalb weniger Stunden antworte, aber ich habe mir vorgenommen, hier wieder öfter reinzusehen.
Beiträge: 202
Themen: 2
Registriert seit: May 2020
Bewertung:
13
28.07.2024, 13:42
(Dieser Beitrag wurde zuletzt bearbeitet: 28.07.2024, 15:47 von cmfrydos.)
Hi Shihan,
ich bin momentan auch sehr vom echten Leben eingenommen, aber ich hoffe, bald mal wieder Zeit fürs Reverse-Engineering und Modden an der Nordlandtrilogie zu finden. Ich freue mich darauf, die Dinge ins Wiki nachzutragen - danke, das mit der Einladung hat geklappt!
Ich bin bezüglich der Repositories zum HDViewer und zum Probenlogger etwas im Rückstand und sollte auch dort mal einen Push vorbereiten. Auf meinem Rechner hatte ich zuletzt viel experimentiert.
Zuletzt, das war noch im Frühjahr, hatte ich einen kleinen Eureka-Moment, als ich merkte, dass es beim Aufbau eines Open-Source-Klons gar nicht notwendig wäre, bottom-up zu arbeiten, also erst die Funktionen zu implementieren, die keine anderen Funktionen aufrufen, dann die, die nur die schon implementierten aufrufen, und so weiter. Man kann auch mit dem Main-Loop anfangen, der bei BrightEyes erst ganz zum Schluss fertiggestellt werden konnte. Um das zu bewerkstelligen, startet man quasi eine DOS-Emulation an genau der Stelle, an der die noch nicht implementierte Unterfunktion im Speicher startet. Das klappt auch praktisch ganz gut. Zuletzt hing ich nur daran, dass, wenn ich zu viele der originalen Funktionen durch meine ersetze, manchmal Endlosschleifen auftreten, da Interrupts nicht aufgerufen werden. Hier rätsel ich zurzeit daran, wie ich diese Interrupt-Mechanismen am besten ersetze. Oft ist es nur ein: f1 liest in Endlosschleife die globale Variable a aus, bis ein Zeit-Interrupt a auf false setzt - sollte eigentlich machbar sein, ich stehe nur auf dem Schlauch.
Code: bool a;
void f1(){
a = true;
while(a){
// do nothing
}
}
void interrupt(){ // called by CPU interrupt
a = false;
}
Beiträge: 302
Themen: 2
Registriert seit: Oct 2012
Bewertung:
3
Schön, dass die Einladung geklappt hat. Dann freue ich mich auf Deine Infos
Der Ansatz, den Du da beschreibst, ist durchaus vielversprechend! KeeperFX (ein Klon von Dungeon Keeper) hat das auch so gemacht, bis alles ersetzt war.
Im Rahmen der Recherche nach diesem Ansatz bin ich mal auf dieses Tool gestoßen: https://github.com/OpenRakis/Spice86
Leider, leider, leider funktioniert das nur mit Real Mode, was bei Riva schlecht ist, da Riva ja Protected Mode nutzt.
Deshalb meine Frage: Wie hast Du das hinbekommen, dass Du Funktionen ersetzen kannst? In welcher Umgebung läuft das Ding dann, Dosbox?
Beiträge: 202
Themen: 2
Registriert seit: May 2020
Bewertung:
13
28.07.2024, 15:15
(Dieser Beitrag wurde zuletzt bearbeitet: 28.07.2024, 15:20 von cmfrydos.)
Ich fange in der DosBox die Call-Befehle ab, schaue, ob ich eine C-Funktion dazu habe, und rufe dann diese stattdessen auf. Ich vermute, dass das recht ähnlich läuft wie bei BrightEyes. Mit dem Protected Mode hatte ich bisher keine großen Schwierigkeiten, man muss nur aufpassen, da je nach Modus die Operanden- und Adressweiten verschieden sind. Lustigerweise mache ich genau das, was Spice macht, nur bisher nur im Protected-Mode und mit einer C-Ausgabe. Ich jage den Dosbox-Disassembler über den Startpunkt einer Funktion, dieser zerstückelt mir den Byte-Code in die einzelnen Befehle, und diese schiebe ich durch ein Python-Skript, das mir äquivalenten DosBox-C-Code ausgibt.
Ich habe, glaube ich, etwa 99,9% des verwendeten Befehlssatzes implementiert, und damit > 90% des gesamten Riva-Bytecode.
Raus kommt dann so etwas wie das:
(Habe den Ausschnitt nur schnell kopiert, um den aktuellen Stand darzustellen. Die Funktion an sich ist vielleicht nicht so interessant, und generierter Code ist nie schön, aber vielleicht ein Startpunkt )
Code: void f_1DB2B0()
{
CPU_Push32(reg_ebx);
CPU_Push32(reg_ecx);
CPU_Push32(reg_esi);
CPU_Push32(reg_edi);
CPU_Push32(reg_ebp);
reg_ecx = reg_eax; // 32,32,mov ecx,eax
reg_esi = reg_edx; // 32,32,mov esi,edx
reg_edx = mem_readd_inline(reg_eax + 0x003D); // 32,32,mov edx,[eax+003D]
test_32(reg_edx, reg_edx);
if (FLG_NE) {
goto l1DB2C8;
}
reg_ebx = mem_readd_inline(reg_eax + 0x0031); // 32,32,mov ebx,[eax+0031]
reg_edi = mem_readd_inline(reg_eax + 0x0039); // 32,32,mov edi,[eax+0039]
goto l1DB2CD;
l1DB2C8:
reg_edi = reg_edx; // 32,32,mov edi,edx
reg_ebx = mem_readd_inline(reg_eax + 0x0035); // 32,32,mov ebx,[eax+0035]
l1DB2CD:
reg_eax = mem_readd_inline(reg_ecx + 0x0004); // 32,32,mov eax,[ecx+0004]
mem_writed_inline(reg_ecx + 0x0029, reg_esi); // 32,32,mov [ecx+0029],esi
reg_ebp = mem_readd_inline(reg_eax); // 32,32,mov ebp,[eax]
test_xor_32(reg_edx, reg_edx);
reg_edx = reg_edx ^ reg_edx;
decide_call(mem_readd_inline(reg_ebp + 0x0044), std::nullopt); // call near dword [ebp+0044]
// ABS
reg_eax = mem_readd_inline(reg_ecx + 0x0004); // 32,32,mov eax,[ecx+0004]
reg_ebp = mem_readd_inline(reg_eax); // 32,32,mov ebp,[eax]
test_xor_32(reg_edx, reg_edx);
reg_edx = reg_edx ^ reg_edx;
decide_call(mem_readd_inline(reg_ebp + 0x0010), std::nullopt); // call near dword [ebp+0010]
// ABS
reg_eax = mem_readd_inline(reg_ecx + 0x0004); // 32,32,mov eax,[ecx+0004]
reg_ebp = mem_readd_inline(reg_eax); // 32,32,mov ebp,[eax]
reg_edx = reg_edi; // 32,32,mov edx,edi
decide_call(mem_readd_inline(reg_ebp + 0x0024), std::nullopt); // call near dword [ebp+0024]
// ABS
reg_eax = mem_readd_inline(reg_ecx + 0x0004); // 32,32,mov eax,[ecx+0004]
test_xor_32(reg_ebx, reg_ebx);
reg_ebx = reg_ebx ^ reg_ebx;
test_xor_32(reg_edx, reg_edx);
reg_edx = reg_edx ^ reg_edx;
reg_edi = mem_readd_inline(reg_eax); // 32,32,mov edi,[eax]
reg_bx = mem_readw_inline(reg_esi + 0x0012); // 16,32,mov bx,[esi+0012]
reg_dx = mem_readw_inline(reg_esi + 0x0014); // 16,32,mov dx,[esi+0014]
decide_call(mem_readd_inline(reg_edi + 0x0058), std::nullopt); // call near dword [edi+0058]
// ABS
reg_eax = mem_readd_inline(reg_ecx + 0x0004); // 32,32,mov eax,[ecx+0004]
reg_edx = mem_readd_inline(reg_eax); // 32,32,mov edx,[eax]
decide_call(mem_readd_inline(reg_edx + 0x0028), std::nullopt); // call near dword [edx+0028]
// ABS
reg_ebx = mem_readd_inline(reg_ecx + 0x003D); // 32,32,mov ebx,[ecx+003D]
test_32(reg_ebx, reg_ebx);
if (FLG_E) {
goto l1DB31F;
}
reg_eax = reg_ebx; // 32,32,mov eax,ebx
CPU_Push32(-1);
f_1CA710();
CPU_Pop32(); // CALL // call 001CA710 ($-10c08) REL
mem_writed_inline(reg_ecx + 0x003D, 0x00000000); // 32,32,mov dword [ecx+003D],00000000
l1DB31F:
reg_eax = mem_readd_inline(reg_ecx + 0x0039); // 32,32,mov eax,[ecx+0039]
CPU_Push32(-1);
f_1CA710();
CPU_Pop32(); // CALL // call 001CA710 ($-10c17) REL
mem_writed_inline(reg_ecx + 0x0039, 0x00000000); // 32,32,mov dword [ecx+0039],00000000
mem_writed_inline(reg_ecx + 0x0035, 0x00000000); // 32,32,mov dword [ecx+0035],00000000
reg_eax = mem_readd_inline(reg_ecx + 0x0035); // 32,32,mov eax,[ecx+0035]
mem_writed_inline(reg_ecx + 0x0031, reg_eax); // 32,32,mov [ecx+0031],eax
reg_ebp = CPU_Pop32();
reg_edi = CPU_Pop32();
reg_esi = CPU_Pop32();
reg_ecx = CPU_Pop32();
reg_ebx = CPU_Pop32();
return; // ret
}
Als nächstes wollte ich per Graphenanalyse die `goto`s durch `if`s, `while`s und `switch`-Statements ersetzen und mit ähnlicher Analyse eventuell die globalen Register durch lokale Variablen. Das war bisher nur eine Spielerei, und ich weiß gar nicht, was das Ziel ist. Die Performance im DosBox dynamic-cpu Mode (JIT-Übersetzung von DOS-ASM in x64) ist fantastisch, und der Source-Code von Riva scheint ja noch in den Händen von Attic zu sein und nicht verloren, wie bei der Schicksalsklinge. Für stellenweise Analysen, unter anderem, da ich so Breakpoints von Visual Studio aus setzen und interessante Stellen kommentieren kann, ist es aber praktisch.
Beiträge: 302
Themen: 2
Registriert seit: Oct 2012
Bewertung:
3
Klingt richtig gut!
Vielleicht will man nicht unbedingt den ganzen Source so ersetzen. Aber falls man bei einer Neuimplementierung nicht genau weiß, wie etwas abläuft, oder Fehler suchen und fixen will, da könnte sowas schon echt hilfreich sein. Vor allem, wenn man dann größere Teile von Riva mit dem VS-Debugger durchlaufen kann. Das wäre ein Traum!
Könntest Du sowas hier gebrauchen:
Das ist noch aus meiner Analyse mit Ghidra. Die Offsets sind zwar verschoben, weil ich nur den LE-Teil der Exe analysiert habe, aber das sollte kein großes Problem darstellen.
Wenn Du damit was anfangen könntest, will ich mal sehen, wie ich die Daten exportiert bekomme.
Hast Du denn Pläne, die nachgebauten C-Funktionen zu teilen?
Beiträge: 202
Themen: 2
Registriert seit: May 2020
Bewertung:
13
Das ist tatsächlich interessant, da ich noch nicht herausfinden konnte, wo die ALF-Extraktion stattfindet. Besonders interessant wäre es, die Funktion zu finden, die die Adressen zu den im RAM entpackten Dateien zurückgibt. Damit könnte man vielleicht auch die Funktionen zum Importieren der Spieldateien (für Bilder, Videos oder andere Dateien) finden, um hier noch offene Fragen zu klären. Wenn ich deine Grafik richtig lese, liegen die am Anfang des Programmes? Ich frage mich, wie Ghidra das den Quelldateinamen zuordnet, ich habe zwar textuelle Verweise im Hex gesehen, aber bei meinem Disasemblierverfahren sind die verloren gegangen..
Groß händisch leicht lesbar nachgebaut habe ich bisher noch nichts. Am ehesten noch die Zufallsfunktionen, aber auch die noch nicht vollständig. Wenn da etwas fertig wird, teile ich das gerne. Bei den anderen ~350.000 Zeilen rein maschinell generierten Codes fühle ich mich nicht wohl, diese bereitzustellen, da es Attics Rechte tangieren könnte und weil da die schöpferische Eigenleistung, oder wie man das auch nennen mag, noch fehlt. Ich wäre aber offen, meine Skripte und DosBox zu teilen, damit könnte man sich das selbst generieren.
Beiträge: 302
Themen: 2
Registriert seit: Oct 2012
Bewertung:
3
Wie gesagt, da ich den DOS/4GW-Stub am Anfang entfernt habe, um das Ding überhaupt laden zu können, stimmen die Offsets sicherlich hinten und vorne nicht. Aber ich könnte sicher die ersten N Bytes einer Funktion mit ausgeben, damit Du sie darüber zuordnen kannst. Mal sehen, der Python-Dialekt in Ghidra ist etwas eigenartig.
Die Namen der Dateien kommen von Debug-Ausgaben im Code. Viele, leider nicht alle, Funktionen/Methoden machen eine solche Debug-Ausgabe. Die läuft zwar anscheinend ins Leere, aber dabei gibt es immer wieder Verweise auf CPP-Dateien. Und mit einem anderen Python-Skript bin ich alle erkannten Funktionen durchgelaufen und habe geprüft, ob da eine solche Debug-Ausgabe existiert und dann, wenn dem so ist, den Dateinamen vor den Funktionsnamen gesetzt.
Wenn Dir nicht wohl ist, die Sachen zu teilen, ist das ok. Kann ich gut verstehen. Aber wenn es ein paar kleine Teaser gibt... da würde ich nicht nein sagen
Beiträge: 202
Themen: 2
Registriert seit: May 2020
Bewertung:
13
29.07.2024, 13:39
(Dieser Beitrag wurde zuletzt bearbeitet: 29.07.2024, 13:42 von cmfrydos.)
(29.07.2024, 10:01)Shihan schrieb: Aber ich könnte sicher die ersten N Bytes einer Funktion mit ausgeben, damit Du sie darüber zuordnen kannst.
Das wäre super! Dann könnte ich den Offset zu dir berechnen. Am besten von mehreren Stellen, denn wenn du eine absolute Adresse erwischst, könnte das bei mir nicht vorkommen, da diese beim Laden neu ausgerichtet werden. Mein Code wurde während der Runtime generiert (ich habe nur Funktionen, die während einer "Recording"-Session aufgerufen wurden), während dein Code statisch in der EXE liegt, so wie ich es verstanden habe. Da gibt es immer wieder Differenzen; in der EXE ist es, glaube ich, enger gepackt. Aber vielleicht kann Ghidra das umschalten?
Die Debug-Ausgaben, also auch Verweise auf die Dateinamen, liegen am Ende der EXE hintereinander. Ich konnte nur leider noch keine Stelle finden, die darauf per Offset zugreift und damit das Zugriffsmuster verstehen.
Beiträge: 302
Themen: 2
Registriert seit: Oct 2012
Bewertung:
3
Bin noch nicht dazu gekommen, sorry. Chef und halbes Team ist im Urlaub und Kunde will Reklamationen durchboxen... Letzte Woche gab es fast 20 Überstunden...
Aber diese Woche ist wieder etwas Ruhe im Spiel. Mal sehen, ob ich das Export-Skrypt fertig bekomme.
|