From 92135cf15a1ca8b1e405023c80993388c1c1a2f4 Mon Sep 17 00:00:00 2001 From: Mark Langen Date: Sun, 25 Jun 2023 21:41:09 -0700 Subject: [PATCH] 100% Match of MxDSFile * ...almost, MxDSFile::Open is still not quite matching but all of the other methods are 100% matching. * Turns out that most of the virtual methods and some of the members are actually on the MxDSSource base class, which I've pulled out as part of this. * In order to implement the methods I added the MXIOINFO class, which seems to be a thin wrapper around the MMIOINFO windows structure. We can tell this because MMIOINFO::~MMIOINFO was included in the DLL exports, and calls down to a function which deconstructs something looking exactly like MMIOINFO. --- LEGO1/mxdsfile.cpp | 135 ++++++++++++++++++++++++++++++++++++++++++++- LEGO1/mxdsfile.h | 48 ++++++++++++---- LEGO1/mxdssource.h | 30 ++++++++++ LEGO1/mxioinfo.cpp | 49 ++++++++++++++++ LEGO1/mxioinfo.h | 12 ++++ LEGO1/mxstring.h | 2 + isle.mak | 41 ++++++++++++++ isle.mdp | Bin 50176 -> 51712 bytes 8 files changed, 305 insertions(+), 12 deletions(-) create mode 100644 LEGO1/mxdssource.h create mode 100644 LEGO1/mxioinfo.cpp diff --git a/LEGO1/mxdsfile.cpp b/LEGO1/mxdsfile.cpp index 27d6668d..146d5dbe 100644 --- a/LEGO1/mxdsfile.cpp +++ b/LEGO1/mxdsfile.cpp @@ -1,6 +1,139 @@ #include "mxdsfile.h" +#include "stdio.h" + +#define SI_MAJOR_VERSION 2 +#define SI_MINOR_VERSION 2 + +// OFFSET: LEGO1 0x100bffd0 +void MxDSSource::SomethingWhichCallsRead(void* pUnknownObject) +{ + // TODO: Calls read, reading into a buffer somewhere in pUnknownObject. + Read(NULL, 0); +} + +// OFFSET: LEGO1 0x100bfff0 +long MxDSSource::GetLengthInDWords() +{ + return m_lengthInDWords; +} + +// OFFSET: LEGO1 0x100cc4b0 +MxDSFile::MxDSFile(const char *filename, unsigned long skipReadingChunks) +{ + m_filename = filename; + m_skipReadingChunks = skipReadingChunks; +} + +// OFFSET: LEGO1 0x100bfed0 +MxDSFile::~MxDSFile() +{ + Close(); +} + +// OFFSET: LEGO1 0x100cc590 +long MxDSFile::Open(unsigned long uStyle) +{ + // No idea what's stopping this one matching, but I'm pretty + // confident it has the correct behavior. + memset(&m_io, 0, sizeof(MXIOINFO)); + if (m_io.Open(m_filename.Data(), uStyle) != 0) { + return -1; + } + + m_io.SetBuffer(NULL, 0); + m_position = 0; + + long longResult = 1; + if (m_skipReadingChunks == 0) + { + longResult = ReadChunks(); + } + if (longResult != 0) + { + Close(); // vtable + 0x18 + return longResult; + } + Seek(0, 0); // vtable + 0x24 + return 0; +} + +// OFFSET: LEGO1 0x100cc780 +long MxDSFile::Read(unsigned char *pch, unsigned long cch) +{ + if (m_io.Read((char*)pch, cch) != cch) + return -1; + + m_position += cch; + return 0; +} + +// OFFSET: LEGO1 0x100cc620 +long MxDSFile::ReadChunks() +{ + _MMCKINFO topChunk; + _MMCKINFO childChunk; + char tempBuffer[80]; + + topChunk.fccType = 0x494e4d4f; + if (m_io.Descend(&topChunk, NULL, MMIO_FINDRIFF) != 0) { + return -1; + } + childChunk.ckid = 0x6448784d; + if (m_io.Descend(&childChunk, &topChunk, 0) != 0) { + return -1; + } + + m_io.Read((char*)&m_header, 0xc); + if ((m_header.majorVersion == SI_MAJOR_VERSION) && (m_header.minorVersion == SI_MINOR_VERSION)) + { + childChunk.ckid = 0x664f784d; + if (m_io.Descend(&childChunk, &topChunk, 0) != 0) { + return -1; + } + unsigned long* pLengthInDWords = &m_lengthInDWords; + m_io.Read((char *)pLengthInDWords, 4); + m_pBuffer = malloc(*pLengthInDWords * 4); + m_io.Read((char*)m_pBuffer, *pLengthInDWords * 4); + return 0; + } + else + { + sprintf(tempBuffer, "Wrong SI file version. %d.%d expected.", SI_MAJOR_VERSION, SI_MINOR_VERSION); + MessageBoxA(NULL, tempBuffer, NULL, 0x10); + return -1; + } +} + +// OFFSET: LEGO1 0x100cc7b0 +long MxDSFile::Seek(long lOffset, int iOrigin) +{ + return (m_position = m_io.Seek(lOffset, iOrigin)) == 0xFFFFFFFF ? -1 : 0; +} + +// OFFSET: LEGO1 0x100cc7e0 unsigned long MxDSFile::GetBufferSize() { - return this->m_buffersize; + return m_header.bufferSize; } + +// OFFSET: LEGO1 0x100cc7f0 +unsigned long MxDSFile::GetStreamBuffersNum() +{ + return m_header.streamBuffersNum; +} + +// OFFSET: LEGO1 0x100cc740 +long MxDSFile::Close() +{ + m_io.Close(0); + m_position = -1; + memset(&m_header, 0, sizeof(m_header)); + if (m_lengthInDWords != 0) + { + m_lengthInDWords = 0; + free(m_pBuffer); + m_pBuffer = NULL; + } + return 0; +} \ No newline at end of file diff --git a/LEGO1/mxdsfile.h b/LEGO1/mxdsfile.h index d28928dc..86702519 100644 --- a/LEGO1/mxdsfile.h +++ b/LEGO1/mxdsfile.h @@ -1,20 +1,46 @@ #ifndef MXDSFILE_H #define MXDSFILE_H -class MxDSFile +#include "mxcore.h" +#include "mxstring.h" +#include "mxioinfo.h" +#include "mxdssource.h" +class MxDSFile : public MxDSSource { public: - __declspec(dllexport) MxDSFile(const char *,unsigned long); - __declspec(dllexport) virtual ~MxDSFile(); - __declspec(dllexport) virtual long Close(); - __declspec(dllexport) virtual unsigned long GetBufferSize(); - __declspec(dllexport) virtual unsigned long GetStreamBuffersNum(); - __declspec(dllexport) virtual long Open(unsigned long); - __declspec(dllexport) virtual long Read(unsigned char *,unsigned long); - __declspec(dllexport) virtual long Seek(long,int); + __declspec(dllexport) MxDSFile(const char *filename, unsigned long skipReadingChunks); + __declspec(dllexport) ~MxDSFile(); + __declspec(dllexport) long Open(unsigned long); + __declspec(dllexport) long Close(); + __declspec(dllexport) long Read(unsigned char *,unsigned long); + __declspec(dllexport) long Seek(long,int); + __declspec(dllexport) unsigned long GetBufferSize(); + __declspec(dllexport) unsigned long GetStreamBuffersNum(); + private: - char m_unknown[0x70]; - unsigned long m_buffersize; + long ReadChunks(); + struct ChunkHeader { + ChunkHeader() + : majorVersion(0) + , minorVersion(0) + , bufferSize(0) + , streamBuffersNum(0) + {} + + unsigned short majorVersion; + unsigned short minorVersion; + unsigned long bufferSize; + short streamBuffersNum; + short reserved; + }; + + MxString m_filename; + MXIOINFO m_io; + ChunkHeader m_header; + + // If false, read chunks immediately on open, otherwise + // skip reading chunks until ReadChunks is explicitly called. + unsigned long m_skipReadingChunks; }; #endif // MXDSFILE_H diff --git a/LEGO1/mxdssource.h b/LEGO1/mxdssource.h new file mode 100644 index 00000000..7ee01490 --- /dev/null +++ b/LEGO1/mxdssource.h @@ -0,0 +1,30 @@ +#ifndef MXDSSOURCE_H +#define MXDSSOURCE_H + +#include "mxcore.h" + +class MxDSSource : public MxCore +{ +public: + MxDSSource() + : m_lengthInDWords(0) + , m_pBuffer(0) + , m_position(-1) + {} + + virtual long Open(unsigned long) = 0; + virtual long Close() = 0; + virtual void SomethingWhichCallsRead(void* pUnknownObject); + virtual long Read(unsigned char *, unsigned long) = 0; + virtual long Seek(long, int) = 0; + virtual unsigned long GetBufferSize() = 0; + virtual unsigned long GetStreamBuffersNum() = 0; + virtual long GetLengthInDWords(); + +protected: + unsigned long m_lengthInDWords; + void* m_pBuffer; + long m_position; +}; + +#endif // MXDSSOURCE_H \ No newline at end of file diff --git a/LEGO1/mxioinfo.cpp b/LEGO1/mxioinfo.cpp new file mode 100644 index 00000000..374eaedb --- /dev/null +++ b/LEGO1/mxioinfo.cpp @@ -0,0 +1,49 @@ +#include "mxioinfo.h" + +// OFFSET: LEGO1 0x100cc800 +MXIOINFO::MXIOINFO() +{ + memset(&m_info, 0, sizeof(MMIOINFO)); +} + +// OFFSET: LEGO1 0x100cc820 +MXIOINFO::~MXIOINFO() +{ + m_info.cchBuffer = 0; +} + +// OFFSET: LEGO1 0x100cc830 +unsigned short MXIOINFO::Open(const char *filename, DWORD fdwOpen) +{ + return 0; +} + +// OFFSET: LEGO1 0x100cc8e0 +void MXIOINFO::Close(long arg) +{ + +} + +// OFFSET: LEGO1 0x100cc930 +unsigned long MXIOINFO::Read(HPSTR pch, LONG cch) +{ + return 0; +} + +// OFFSET: LEGO1 0x100cca00 +LONG MXIOINFO::Seek(LONG lOffset, int iOrigin) +{ + return 0; +} + +// OFFSET: LEGO1 0x100ccbc0 +void MXIOINFO::SetBuffer(LPSTR pchBuffer, LONG cchBuffer) +{ + +} + +// OFFSET: LEGO1 0x100cce60 +unsigned short MXIOINFO::Descend(LPMMCKINFO pmmcki, const MMCKINFO *pmmckiParent, UINT fuDescend) +{ + return 0; +} \ No newline at end of file diff --git a/LEGO1/mxioinfo.h b/LEGO1/mxioinfo.h index d3f2a40e..ce8c4eb5 100644 --- a/LEGO1/mxioinfo.h +++ b/LEGO1/mxioinfo.h @@ -1,10 +1,22 @@ #ifndef MXIOINFO_H #define MXIOINFO_H +#include "windows.h" +#include "mmsystem.h" class MXIOINFO { public: + MXIOINFO(); __declspec(dllexport) ~MXIOINFO(); + + unsigned short Open(const char *filename, DWORD fdwOpen); + void Close(long arg); + LONG Seek(LONG lOffset, int iOrigin); + unsigned long Read(HPSTR pch, LONG cch); + void SetBuffer(LPSTR pchBuffer, LONG cchBuffer); + unsigned short Descend(LPMMCKINFO pmmcki, const MMCKINFO *pmmckiParent, UINT fuDescend); + + MMIOINFO m_info; }; #endif // MXIOINFO_H diff --git a/LEGO1/mxstring.h b/LEGO1/mxstring.h index 0f9ff9f3..12194447 100644 --- a/LEGO1/mxstring.h +++ b/LEGO1/mxstring.h @@ -16,6 +16,8 @@ class MxString : public MxCore void ToLowerCase(); const MxString &operator=(MxString *); + inline const char *Data() const { return m_data; } + private: char *m_data; unsigned short m_length; diff --git a/isle.mak b/isle.mak index 59570097..47d21221 100644 --- a/isle.mak +++ b/isle.mak @@ -61,7 +61,9 @@ CLEAN : -@erase "$(INTDIR)\mxautolocker.obj" -@erase "$(INTDIR)\mxcore.obj" -@erase "$(INTDIR)\mxcriticalsection.obj" + -@erase "$(INTDIR)\mxdsfile.obj" -@erase "$(INTDIR)\mxdsobject.obj" + -@erase "$(INTDIR)\mxioinfo.obj" -@erase "$(INTDIR)\mxomni.obj" -@erase "$(INTDIR)\mxomnicreateflags.obj" -@erase "$(INTDIR)\mxomnicreateparam.obj" @@ -138,7 +140,9 @@ LINK32_OBJS= \ "$(INTDIR)\mxautolocker.obj" \ "$(INTDIR)\mxcore.obj" \ "$(INTDIR)\mxcriticalsection.obj" \ + "$(INTDIR)\mxdsfile.obj" \ "$(INTDIR)\mxdsobject.obj" \ + "$(INTDIR)\mxioinfo.obj" \ "$(INTDIR)\mxomni.obj" \ "$(INTDIR)\mxomnicreateflags.obj" \ "$(INTDIR)\mxomnicreateparam.obj" \ @@ -181,7 +185,9 @@ CLEAN : -@erase "$(INTDIR)\mxautolocker.obj" -@erase "$(INTDIR)\mxcore.obj" -@erase "$(INTDIR)\mxcriticalsection.obj" + -@erase "$(INTDIR)\mxdsfile.obj" -@erase "$(INTDIR)\mxdsobject.obj" + -@erase "$(INTDIR)\mxioinfo.obj" -@erase "$(INTDIR)\mxomni.obj" -@erase "$(INTDIR)\mxomnicreateflags.obj" -@erase "$(INTDIR)\mxomnicreateparam.obj" @@ -260,7 +266,9 @@ LINK32_OBJS= \ "$(INTDIR)\mxautolocker.obj" \ "$(INTDIR)\mxcore.obj" \ "$(INTDIR)\mxcriticalsection.obj" \ + "$(INTDIR)\mxdsfile.obj" \ "$(INTDIR)\mxdsobject.obj" \ + "$(INTDIR)\mxioinfo.obj" \ "$(INTDIR)\mxomni.obj" \ "$(INTDIR)\mxomnicreateflags.obj" \ "$(INTDIR)\mxomnicreateparam.obj" \ @@ -885,6 +893,32 @@ DEP_CPP_MXPAL=\ $(CPP) $(CPP_PROJ) $(SOURCE) +# End Source File +################################################################################ +# Begin Source File + +SOURCE=.\LEGO1\mxioinfo.cpp +DEP_CPP_MXIOI=\ + ".\LEGO1\mxioinfo.h"\ + + +"$(INTDIR)\mxioinfo.obj" : $(SOURCE) $(DEP_CPP_MXIOI) "$(INTDIR)" + $(CPP) $(CPP_PROJ) $(SOURCE) + + +# End Source File +################################################################################ +# Begin Source File + +SOURCE=.\LEGO1\mxdsfile.cpp +DEP_CPP_MXDSF=\ + ".\LEGO1\mxdsfile.h"\ + + +"$(INTDIR)\mxdsfile.obj" : $(SOURCE) $(DEP_CPP_MXDSF) "$(INTDIR)" + $(CPP) $(CPP_PROJ) $(SOURCE) + + # End Source File # End Target ################################################################################ @@ -982,21 +1016,28 @@ SOURCE=.\ISLE\main.cpp DEP_CPP_MAIN_=\ ".\ISLE\define.h"\ ".\ISLE\isle.h"\ + ".\ISLE\res\resource.h"\ ".\LEGO1\lego3dmanager.h"\ ".\LEGO1\lego3dview.h"\ + ".\LEGO1\legoanimationmanager.h"\ + ".\LEGO1\legobuildingmanager.h"\ ".\LEGO1\legoentity.h"\ ".\LEGO1\legogamestate.h"\ ".\LEGO1\legoinc.h"\ ".\LEGO1\legoinputmanager.h"\ + ".\LEGO1\legomodelpresenter.h"\ ".\LEGO1\legonavcontroller.h"\ ".\LEGO1\legoomni.h"\ + ".\LEGO1\legopartpresenter.h"\ ".\LEGO1\legoroi.h"\ ".\LEGO1\legovideomanager.h"\ + ".\LEGO1\legoworldpresenter.h"\ ".\LEGO1\mxatomid.h"\ ".\LEGO1\mxbackgroundaudiomanager.h"\ ".\LEGO1\mxbool.h"\ ".\LEGO1\mxcore.h"\ ".\LEGO1\mxcriticalsection.h"\ + ".\LEGO1\mxdirectdraw.h"\ ".\LEGO1\mxdsaction.h"\ ".\LEGO1\mxdsfile.h"\ ".\LEGO1\mxdsobject.h"\ diff --git a/isle.mdp b/isle.mdp index 390064aa600f2bb5cc48156405e1040876938d6d..ddf03fe6832fd4a6bb5c9f412179b8c99384bdb8 100644 GIT binary patch literal 51712 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 literal 50176 zcmeHQOLN@D5$+{LhNNWcK~l11OOchg6R-J@Wm|qE)>2la(6J-hk&~VkBZg~GWIck%p zV+2mYH$)Sl*MtBeKnM^5ga9Ex2oM5<03kpK5CVh%Auz!RT$zTefBJk1{tkbJ(HD=W zK0O7{xz+t>;KqY)rx$yJ#IwTgM)WB3Bg+|dy@Bs`H}Bkif3+Ko`j+n|iQBgO{p$?r@<%tbO0}%p*03kpK5CVh%AwUQa0)zk|KnM^5gupQ)K>5Fung9D1d>g(4 z--YkN_u&WdBK+`}?U06lk_e>vKX%z~>E71noog>$d+3GhYu6sQzH1HK8EEA{s|SCo zpNZ%??v5AoP5`ae%bGl_mjHYG^f>~{3sa6Y%`sv(?OD){?N;mUgPGFS2EMx+S)mtL zi5G=gqk`UBChXfo&v)>H{yF!nCfvJL;0_Y(QPA$)GU4;W{xHdheA9$Ah#c4NBVKNp zU|WWs2Eqkctf@k~_lC)M*g_!@hSM;#Vz@ZM(Tsp8wVdb~0rMz|eMgw7HZZ|x+@SE0x~s`tT*5z302Rht&2u!6@+p zHa+c406AhS9C)QI#9*fTR_s~Z*kFhKGJ4dCPpBBUGe81T5wM~g_-TYzZgBh%r9ph5jmOsFUg*hC~|C+vKS;HjqBYO{x z#7xi3v^bXmq=l9Svn&=louvUl1#8~;sMRvyFFa=I^|-$8=mT8g-^P<_s)M#|L_mGe;%+!7~98ju#3rDja#fM8cJ?PJU)N zScO8oo(l3Th-5Ms3l$tO?;#--<}LUU3E{Nk;*u;uXT$Bgq2q@3XB|vbV>!`{#|A%# zg%e%{X~9{Mjv0JGlGZ)cB=by7FSOe|HO*N@v+iX1kp{{$DdoQBV%0<~kdtBA&PzUP zBIDx&ET1EPjx>N=C~9$PiI>&Y zH#O5@-A^?rc7_$FvWk{kC{8!4VX29@tZ`njVaRjA$lXWd&_D@EgWcKftT6|Lz;YtlyjaXx)mbg!^ROZx@(W;7CoSF=0IsPJp27PhKEA=+D z!1-)LZjiT(H1(RAS|*NPsiLWA5m8-d^0ZhhYjU_~b=8f&LA$4zo7Y%S99}r*GzjFb zc9?ir*;nh0bF60j8>naImNxD`q}>$H&9s~1cZxKa3hkz}oALmYc2nGZ+0_xX6QoLQo#xe z(gv%U;f|$+-Nv&bNRfmcCsuU=%JC;ma7wE5>@44N8VD9}Gl_<=jn`?_QjR=3dr`GD zsq|P@Y)mTpBTxV8B(aTB#7j)0`tGJu=qt!5bCI=uP&I)g&p&ej>N}une2fROkB+Df z^-ux!(NsC`C|)hjH9S$6uo6H0q1K49<90&Xar~f)+C(TjuH618JI<~%d{y~uI)11& zD>{B0T+pWDM|#$6|Mc-+|H<>f-OfS%+Nl5QB=mp%7=8u6hTlLJK7rrD@8HwPHF46c IhQK`h4@5i>LI3~&