From 6e8b93b07c818cfe15cc490428e7e762d6007494 Mon Sep 17 00:00:00 2001 From: MattKC <34096995+itsmattkc@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:56:59 -0700 Subject: [PATCH 1/2] Update CONTRIBUTING.md --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 331b5746..2c6519ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Generally, decompilation is a fairly advanced skill. If you aren't already famil ## Ghidra Server -For documenting the original binaries and generating pseudocode that we decompile with, we primarily use [Ghidra](https://ghidra-sre.org/) (it's free and open source). To help with collaboration, we have a shared Ghidra repository with all of our current work. It is available to the public but read-only; to contribute to it yourself, you'll need approval from a current maintainer. +For documenting the original binaries and generating pseudocode that we decompile with, we primarily use [Ghidra](https://ghidra-sre.org/) (it's free and open source). To help with collaboration, we have a shared Ghidra repository with all of our current work. You are free to check it out and mess around with it locally, however to prevent sabotage, you will need to request permission before you can push back to the server (ask in the Matrix room). To access the Ghidra repository, use the following details: @@ -21,6 +21,7 @@ In general, we're not exhaustively strict about coding style, but there are some - `PascalCase` for classes and function names. - `m_camelCase` for member variables. - `g_camelCase` for global variables. +- `p_camelCase` for function parameters. ## Kinds of Contributions From 51ec2c97c60e107b27e15e06b4db8301add35966 Mon Sep 17 00:00:00 2001 From: Mark Langen Date: Tue, 27 Jun 2023 11:44:02 -0700 Subject: [PATCH 2/2] 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~&