From 51ec2c97c60e107b27e15e06b4db8301add35966 Mon Sep 17 00:00:00 2001 From: Mark Langen Date: Tue, 27 Jun 2023 11:44:02 -0700 Subject: [PATCH] 100% Match of MxDSFile (#51) * 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. * Add mxdssource.cpp * mattkc feedback * some accuracy improvements * Use FOURCC macro * Tirival solve in mxioinfo.cpp * Update mxdsfile.cpp 0xFFFFFFFF -> -1 --------- Co-authored-by: Christian Semmler --- LEGO1/mxdsfile.cpp | 126 ++++++++++++++++++++++++++++++++++++++++++- LEGO1/mxdsfile.h | 40 +++++++++++--- LEGO1/mxdssource.cpp | 14 +++++ LEGO1/mxdssource.h | 30 +++++++++++ LEGO1/mxioinfo.cpp | 49 +++++++++++++++++ LEGO1/mxioinfo.h | 12 +++++ LEGO1/mxstring.h | 2 + isle.mak | 83 ++++++++++++++++++++++++++++ isle.mdp | Bin 50176 -> 52224 bytes 9 files changed, 348 insertions(+), 8 deletions(-) create mode 100644 LEGO1/mxdssource.cpp create mode 100644 LEGO1/mxdssource.h create mode 100644 LEGO1/mxioinfo.cpp diff --git a/LEGO1/mxdsfile.cpp b/LEGO1/mxdsfile.cpp index 27d6668d..327fab46 100644 --- a/LEGO1/mxdsfile.cpp +++ b/LEGO1/mxdsfile.cpp @@ -1,6 +1,130 @@ #include "mxdsfile.h" +#include + +#define SI_MAJOR_VERSION 2 +#define SI_MINOR_VERSION 2 + +#define FOURCC(a, b, c, d) (((a) << 0) | ((b) << 8) | ((c) << 16) | ((d) << 24)) + +// 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. + long longResult = 1; + memset(&m_io, 0, sizeof(MXIOINFO)); + + if (m_io.Open(m_filename.GetData(), uStyle) != 0) { + return -1; + } + + m_io.SetBuffer(NULL, 0, 0); + m_position = 0; + + if (m_skipReadingChunks == 0) { + longResult = ReadChunks(); + } + + if (longResult != 0) { + Close(); // vtable + 0x18 + } + else { + Seek(0, 0); // vtable + 0x24 + } + + return longResult; +} + +// 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 = FOURCC('O', 'M', 'N', 'I'); + if (m_io.Descend(&topChunk, NULL, MMIO_FINDRIFF) != 0) { + return -1; + } + childChunk.ckid = FOURCC('M', 'x', 'H', 'd'); + 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 = FOURCC('M', 'x', 'O', 'f'); + 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, MB_ICONERROR); + return -1; + } +} + +// OFFSET: LEGO1 0x100cc7b0 +long MxDSFile::Seek(long lOffset, int iOrigin) +{ + return (m_position = m_io.Seek(lOffset, iOrigin)) == -1 ? -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; } diff --git a/LEGO1/mxdsfile.h b/LEGO1/mxdsfile.h index d28928dc..e1e170d4 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) MxDSFile(const char *filename, unsigned long skipReadingChunks); __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 Close(); __declspec(dllexport) virtual long Read(unsigned char *,unsigned long); __declspec(dllexport) virtual long Seek(long,int); + __declspec(dllexport) virtual unsigned long GetBufferSize(); + __declspec(dllexport) virtual 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.cpp b/LEGO1/mxdssource.cpp new file mode 100644 index 00000000..8612c5c4 --- /dev/null +++ b/LEGO1/mxdssource.cpp @@ -0,0 +1,14 @@ +#include "mxdssource.h" + +// 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; +} \ No newline at end of file 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..ebe2a480 --- /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() +{ + Close(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, LONG unk) +{ + +} + +// 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..59ee3807 100644 --- a/LEGO1/mxioinfo.h +++ b/LEGO1/mxioinfo.h @@ -1,10 +1,22 @@ #ifndef MXIOINFO_H #define MXIOINFO_H +#include "legoinc.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, LONG unk); + 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..a9a25ba1 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 *GetData() const { return m_data; } + private: char *m_data; unsigned short m_length; diff --git a/isle.mak b/isle.mak index 59570097..7eaf3d48 100644 --- a/isle.mak +++ b/isle.mak @@ -61,7 +61,10 @@ CLEAN : -@erase "$(INTDIR)\mxautolocker.obj" -@erase "$(INTDIR)\mxcore.obj" -@erase "$(INTDIR)\mxcriticalsection.obj" + -@erase "$(INTDIR)\mxdsfile.obj" -@erase "$(INTDIR)\mxdsobject.obj" + -@erase "$(INTDIR)\mxdssource.obj" + -@erase "$(INTDIR)\mxioinfo.obj" -@erase "$(INTDIR)\mxomni.obj" -@erase "$(INTDIR)\mxomnicreateflags.obj" -@erase "$(INTDIR)\mxomnicreateparam.obj" @@ -138,7 +141,10 @@ LINK32_OBJS= \ "$(INTDIR)\mxautolocker.obj" \ "$(INTDIR)\mxcore.obj" \ "$(INTDIR)\mxcriticalsection.obj" \ + "$(INTDIR)\mxdsfile.obj" \ "$(INTDIR)\mxdsobject.obj" \ + "$(INTDIR)\mxdssource.obj" \ + "$(INTDIR)\mxioinfo.obj" \ "$(INTDIR)\mxomni.obj" \ "$(INTDIR)\mxomnicreateflags.obj" \ "$(INTDIR)\mxomnicreateparam.obj" \ @@ -181,7 +187,10 @@ CLEAN : -@erase "$(INTDIR)\mxautolocker.obj" -@erase "$(INTDIR)\mxcore.obj" -@erase "$(INTDIR)\mxcriticalsection.obj" + -@erase "$(INTDIR)\mxdsfile.obj" -@erase "$(INTDIR)\mxdsobject.obj" + -@erase "$(INTDIR)\mxdssource.obj" + -@erase "$(INTDIR)\mxioinfo.obj" -@erase "$(INTDIR)\mxomni.obj" -@erase "$(INTDIR)\mxomnicreateflags.obj" -@erase "$(INTDIR)\mxomnicreateparam.obj" @@ -260,7 +269,10 @@ LINK32_OBJS= \ "$(INTDIR)\mxautolocker.obj" \ "$(INTDIR)\mxcore.obj" \ "$(INTDIR)\mxcriticalsection.obj" \ + "$(INTDIR)\mxdsfile.obj" \ "$(INTDIR)\mxdsobject.obj" \ + "$(INTDIR)\mxdssource.obj" \ + "$(INTDIR)\mxioinfo.obj" \ "$(INTDIR)\mxomni.obj" \ "$(INTDIR)\mxomnicreateflags.obj" \ "$(INTDIR)\mxomnicreateparam.obj" \ @@ -524,7 +536,9 @@ DEP_CPP_LEGOO=\ ".\LEGO1\mxdsaction.h"\ ".\LEGO1\mxdsfile.h"\ ".\LEGO1\mxdsobject.h"\ + ".\LEGO1\mxdssource.h"\ ".\LEGO1\mxeventmanager.h"\ + ".\LEGO1\mxioinfo.h"\ ".\LEGO1\mxmusicmanager.h"\ ".\LEGO1\mxnotificationmanager.h"\ ".\LEGO1\mxobjectfactory.h"\ @@ -645,8 +659,11 @@ DEP_CPP_MXOMN=\ SOURCE=.\LEGO1\mxvideoparam.cpp DEP_CPP_MXVID=\ ".\LEGO1\legoinc.h"\ + ".\LEGO1\mxbool.h"\ + ".\LEGO1\mxcore.h"\ ".\LEGO1\mxpalette.h"\ ".\LEGO1\mxrect32.h"\ + ".\LEGO1\mxresult.h"\ ".\LEGO1\mxvariabletable.h"\ ".\LEGO1\mxvideoparam.h"\ ".\LEGO1\mxvideoparamflags.h"\ @@ -684,6 +701,7 @@ DEP_CPP_MXOMNI=\ ".\LEGO1\mxomnicreateparambase.h"\ ".\LEGO1\mxpalette.h"\ ".\LEGO1\mxrect32.h"\ + ".\LEGO1\mxresult.h"\ ".\LEGO1\mxstring.h"\ ".\LEGO1\mxvariabletable.h"\ ".\LEGO1\mxvideoparam.h"\ @@ -708,6 +726,7 @@ DEP_CPP_MXOMNIC=\ ".\LEGO1\mxomnicreateparambase.h"\ ".\LEGO1\mxpalette.h"\ ".\LEGO1\mxrect32.h"\ + ".\LEGO1\mxresult.h"\ ".\LEGO1\mxstring.h"\ ".\LEGO1\mxvariabletable.h"\ ".\LEGO1\mxvideoparam.h"\ @@ -773,7 +792,9 @@ DEP_CPP_LEGON=\ ".\LEGO1\mxdsaction.h"\ ".\LEGO1\mxdsfile.h"\ ".\LEGO1\mxdsobject.h"\ + ".\LEGO1\mxdssource.h"\ ".\LEGO1\mxeventmanager.h"\ + ".\LEGO1\mxioinfo.h"\ ".\LEGO1\mxmusicmanager.h"\ ".\LEGO1\mxnotificationmanager.h"\ ".\LEGO1\mxobjectfactory.h"\ @@ -842,6 +863,7 @@ DEP_CPP_MXUNK=\ ".\LEGO1\mxbool.h"\ ".\LEGO1\mxcore.h"\ ".\LEGO1\mxcriticalsection.h"\ + ".\LEGO1\mxresult.h"\ ".\LEGO1\mxunknown100dc6b0.h"\ @@ -861,6 +883,7 @@ DEP_CPP_MXVIDEO=\ ".\LEGO1\mxcriticalsection.h"\ ".\LEGO1\mxpalette.h"\ ".\LEGO1\mxrect32.h"\ + ".\LEGO1\mxresult.h"\ ".\LEGO1\mxunknown100dc6b0.h"\ ".\LEGO1\mxvariabletable.h"\ ".\LEGO1\mxvideomanager.h"\ @@ -878,13 +901,62 @@ DEP_CPP_MXVIDEO=\ SOURCE=.\LEGO1\mxpalette.cpp DEP_CPP_MXPAL=\ + ".\LEGO1\mxbool.h"\ + ".\LEGO1\mxcore.h"\ ".\LEGO1\mxpalette.h"\ + ".\LEGO1\mxresult.h"\ "$(INTDIR)\mxpalette.obj" : $(SOURCE) $(DEP_CPP_MXPAL) "$(INTDIR)" $(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\mxbool.h"\ + ".\LEGO1\mxcore.h"\ + ".\LEGO1\mxdsfile.h"\ + ".\LEGO1\mxdssource.h"\ + ".\LEGO1\mxioinfo.h"\ + ".\LEGO1\mxstring.h"\ + + +"$(INTDIR)\mxdsfile.obj" : $(SOURCE) $(DEP_CPP_MXDSF) "$(INTDIR)" + $(CPP) $(CPP_PROJ) $(SOURCE) + + +# End Source File +################################################################################ +# Begin Source File + +SOURCE=.\LEGO1\mxdssource.cpp +DEP_CPP_MXDSS=\ + ".\LEGO1\mxbool.h"\ + ".\LEGO1\mxcore.h"\ + ".\LEGO1\mxdssource.h"\ + + +"$(INTDIR)\mxdssource.obj" : $(SOURCE) $(DEP_CPP_MXDSS) "$(INTDIR)" + $(CPP) $(CPP_PROJ) $(SOURCE) + + # End Source File # End Target ################################################################################ @@ -944,7 +1016,9 @@ DEP_CPP_ISLE_=\ ".\LEGO1\mxdsaction.h"\ ".\LEGO1\mxdsfile.h"\ ".\LEGO1\mxdsobject.h"\ + ".\LEGO1\mxdssource.h"\ ".\LEGO1\mxeventmanager.h"\ + ".\LEGO1\mxioinfo.h"\ ".\LEGO1\mxmusicmanager.h"\ ".\LEGO1\mxnotificationmanager.h"\ ".\LEGO1\mxobjectfactory.h"\ @@ -982,25 +1056,34 @@ 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"\ + ".\LEGO1\mxdssource.h"\ ".\LEGO1\mxeventmanager.h"\ + ".\LEGO1\mxioinfo.h"\ ".\LEGO1\mxmusicmanager.h"\ ".\LEGO1\mxnotificationmanager.h"\ ".\LEGO1\mxobjectfactory.h"\ diff --git a/isle.mdp b/isle.mdp index 390064aa600f2bb5cc48156405e1040876938d6d..9449bd89ad80f94b71cded5a7863892a0bbb9186 100644 GIT binary patch literal 52224 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 l%ncu}wfH6?d2la(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~&