Gagnetar Posted Saturday at 04:10 AM Posted Saturday at 04:10 AM Psi-Ops is a short experience with an irrelevant story that I think would be sweet to make quick character mods for, especially since the game comes with unlockable outfits and characters for campaign use with mechanical changes compared to the base character. However, from what I understand it was built using a modified UE2 engine and no one really knows a whole lot about the formats. The game was actually released for free on PC, and after its parent company also ceased to be. Thus it should be accessible without any potential legal issue here https://www.myabandonware.com/game/psi-ops-the-mindgate-conspiracy-dvc Watto's Game Extractor can seemingly extract data from the .w32 archives (which appear to contain the desired character data), and I asked Watto himself to take a look at "MAINNICK.w32" in the included attachment and he had this to say, Quote When looking at the MESH files, for example, we can see what appears to be a list of face indexes, but there's no vertexes in the file. We couldn't find a matching file where the vertexes were located. For the TEX files, they are really small and look like they just contain metadata for an image (width, height, format, ...). We assume maybe the texture data is stored in another file like the TBX file, however we couldn't find anywhere in the TEX that tells us where the image data is stored in the TBX. This game looks like it has lots of small pieces that need to fit together, and we weren't able to really figure out some of these fundamental issues, so think it might be better with someone else. The best option is probably to ask at https://reshax.com/ as there are people there who can help with all kinds of files (especially texture and model files). Thus, I am now here! Inside of the "MAINNICK.w32" appears to be: .smsh (skinned mesh) .skel (skeleton) .tbx (texture) //guessing it's only a diffuse texture, game probably doesn't have more than that lol .tex (material) .strs (???) .vbx (???) //referred to as a "global vertex buffer" I also included MAINPK, as she's one of the unlockable skins, just incase there's potential variation as the aformentioned character is the base skin, which allows cutscenes and hitgrunts. In addition, i've also included what appears to be the games general containers (divided by language given the final character) which includes .mesh files, which I'm assuming is static mesh. Let me know if anyone is willing to make .bms and/or noesis import/export scripts (anything that would allow me to make character replacer mods) I'd be super grateful! Psi-Ops W32s.zip
Engineers shak-otay Posted Saturday at 06:50 PM Engineers Posted Saturday at 06:50 PM Using hex2obj - mesh format appears to be simple (for one sub mesh at least):
Gagnetar Posted Saturday at 10:33 PM Author Posted Saturday at 10:33 PM (edited) Oooh! Thank you for taking a look at this! Ahh that looks right. The game has no face posing, so nothing to worry about in that department. I'm guessing it's separated by mesh groups? fascinating. Then workflow will then consist of finding the location of the segments, maybe? Are the vertex skins just embedded in the mesh as normal? I'm super inexperienced with RE tools and I'm a 3D artist rather than a programmer, but is there anything I can do to aid the process? Edited Saturday at 11:03 PM by Gagnetar reduced keyword density
Engineers shak-otay Posted Sunday at 05:31 AM Engineers Posted Sunday at 05:31 AM (edited) Didn't check for "mesh groups", just the next face index block (FI block). Goes like so: mainnick.w32 (see advice in appended picture) As a start you'll need to load said w32 into hex2obj and enter the parameters from the picture in my previous post. Then proceed as follows: Quote check address from picture in previous post, lower left window 0x522f0 endOf first FI block (04A00100, some signature?) next FI block: 0x52300 to 533C6, delta 0x10C6 -> 4294 dec. div 2= 2147 (word FIs), FI count 2C4803 (some signature?) Edited Sunday at 05:45 AM by shak-otay
Engineers h3x3r Posted Sunday at 04:16 PM Engineers Posted Sunday at 04:16 PM (edited) Here's Noesis script for textures. from inc_noesis import * import noesis import rapi import os def registerNoesisTypes(): handle = noesis.register("Psi Ops - Texture", ".w32") noesis.setHandlerTypeCheck(handle, noepyCheckType) noesis.setHandlerLoadRGBA(handle, noepyLoadRGBA) noesis.logPopup() return 1 def noepyCheckType(data): bs = NoeBitStream(data) if len(data) < 20: return 0 return 1 def noepyLoadRGBA(data, texList): bs = NoeBitStream(data) BaseName = rapi.getExtensionlessName(rapi.getLocalFileName(rapi.getInputName())) bs.read(20) ResourceTableOffset = bs.readUInt() bs.read(12) StringTableOffset = bs.readUInt() bs.seek(ResourceTableOffset, NOESEEK_ABS) ResourceCount = bs.readUInt() for i in range(0, ResourceCount): Extension = bs.readUInt() Unknown_0 = bs.readUInt() ResourceSize = bs.readUInt() ResourceNameOffset = bs.readUInt() cPos_0 = bs.tell() bs.seek(StringTableOffset + ResourceNameOffset, NOESEEK_ABS) ResourceName = bs.readString() bs.seek(cPos_0, NOESEEK_ABS) ResourceOffset = bs.readUInt() Unknown_1 = bs.readUInt() cPos_1 = bs.tell() if Extension == 544761204: bs.seek(ResourceOffset, NOESEEK_ABS) TextureWidth = bs.readUInt() TextureHeight = bs.readUInt() RawDataSize = bs.readUInt() -20 Unknown_0 = bs.readUInt() BufferInfoOffset = bs.readUInt() Unknown_1 = bs.readUInt() MipMap = bs.readUInt() Unknown_2 = bs.readUInt() PixelFormat = bs.readUInt() Unknown_3 = bs.readUInt() RawDataOffset = bs.readUInt() bs.seek(RawDataOffset, NOESEEK_ABS) TextureBuffer = bs.read(RawDataSize) bs.seek(cPos_1, NOESEEK_ABS) if PixelFormat == 19: print("Pixel Format > R8") elif PixelFormat == 12: print("Pixel Format > DXT1") elif PixelFormat == 14: print("Pixel Format > DXT3") elif PixelFormat == 15: print("Pixel Format > DXT5") elif PixelFormat == 18: print("Pixel Format > RGBA8") else: print("Unknown Pixel Format > ",PixelFormat) if PixelFormat == 12: texFmt = noesis.NOESISTEX_DXT1 elif PixelFormat == 14: texFmt = noesis.NOESISTEX_DXT3 elif PixelFormat == 15: texFmt = noesis.NOESISTEX_DXT5 elif PixelFormat == 18: texFmt = noesis.NOESISTEX_RGBA32 elif PixelFormat == 19: TextureBuffer = rapi.imageDecodeRaw(TextureBuffer, TextureWidth, TextureHeight, "b0 g0 r8 a0") texFmt = noesis.NOESISTEX_RGBA32 texList.append(NoeTexture(ResourceName, TextureWidth, TextureHeight, TextureBuffer, texFmt)) return 1 Edited Sunday at 04:17 PM by h3x3r 2
Gagnetar Posted Monday at 02:03 AM Author Posted Monday at 02:03 AM 9 hours ago, h3x3r said: Here's Noesis script for textures. from inc_noesis import * import noesis import rapi import os def registerNoesisTypes(): handle = noesis.register("Psi Ops - Texture", ".w32") noesis.setHandlerTypeCheck(handle, noepyCheckType) noesis.setHandlerLoadRGBA(handle, noepyLoadRGBA) noesis.logPopup() return 1 def noepyCheckType(data): bs = NoeBitStream(data) if len(data) < 20: return 0 return 1 def noepyLoadRGBA(data, texList): bs = NoeBitStream(data) BaseName = rapi.getExtensionlessName(rapi.getLocalFileName(rapi.getInputName())) bs.read(20) ResourceTableOffset = bs.readUInt() bs.read(12) StringTableOffset = bs.readUInt() bs.seek(ResourceTableOffset, NOESEEK_ABS) ResourceCount = bs.readUInt() for i in range(0, ResourceCount): Extension = bs.readUInt() Unknown_0 = bs.readUInt() ResourceSize = bs.readUInt() ResourceNameOffset = bs.readUInt() cPos_0 = bs.tell() bs.seek(StringTableOffset + ResourceNameOffset, NOESEEK_ABS) ResourceName = bs.readString() bs.seek(cPos_0, NOESEEK_ABS) ResourceOffset = bs.readUInt() Unknown_1 = bs.readUInt() cPos_1 = bs.tell() if Extension == 544761204: bs.seek(ResourceOffset, NOESEEK_ABS) TextureWidth = bs.readUInt() TextureHeight = bs.readUInt() RawDataSize = bs.readUInt() -20 Unknown_0 = bs.readUInt() BufferInfoOffset = bs.readUInt() Unknown_1 = bs.readUInt() MipMap = bs.readUInt() Unknown_2 = bs.readUInt() PixelFormat = bs.readUInt() Unknown_3 = bs.readUInt() RawDataOffset = bs.readUInt() bs.seek(RawDataOffset, NOESEEK_ABS) TextureBuffer = bs.read(RawDataSize) bs.seek(cPos_1, NOESEEK_ABS) if PixelFormat == 19: print("Pixel Format > R8") elif PixelFormat == 12: print("Pixel Format > DXT1") elif PixelFormat == 14: print("Pixel Format > DXT3") elif PixelFormat == 15: print("Pixel Format > DXT5") elif PixelFormat == 18: print("Pixel Format > RGBA8") else: print("Unknown Pixel Format > ",PixelFormat) if PixelFormat == 12: texFmt = noesis.NOESISTEX_DXT1 elif PixelFormat == 14: texFmt = noesis.NOESISTEX_DXT3 elif PixelFormat == 15: texFmt = noesis.NOESISTEX_DXT5 elif PixelFormat == 18: texFmt = noesis.NOESISTEX_RGBA32 elif PixelFormat == 19: TextureBuffer = rapi.imageDecodeRaw(TextureBuffer, TextureWidth, TextureHeight, "b0 g0 r8 a0") texFmt = noesis.NOESISTEX_RGBA32 texList.append(NoeTexture(ResourceName, TextureWidth, TextureHeight, TextureBuffer, texFmt)) return 1 Ooh! textures! I was really stoked to see this. I'm guessing the script probably needs some additional pointer information? It spits out this error tried to export the texture and gives a similar error trying to open it.
Engineers h3x3r Posted Monday at 05:54 AM Engineers Posted Monday at 05:54 AM I tested it on all files you provided and it works. So it must be the Noesis. Try to update it. 1
Gagnetar Posted Monday at 04:00 PM Author Posted Monday at 04:00 PM 9 hours ago, h3x3r said: I tested it on all files you provided and it works. So it must be the Noesis. Try to update it. Thanks! that was in fact the issue. Apologies for my ignorance. I know you guys do this stuff regularly but I'm always super impressed seeing how these little scripts turn the seemingly complete gibberish of a an obscure game format into plainly readable files. Do you know how make something to dump the .w32 and create another?
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now