├── Docs_Resources_SDK ├── 102089-semc_java_me_cldc_sdk_2_5_0_3.txt ├── 3ds_max_dlls.rar ├── Code sample – Rotation around an arbitrary axis in Mascot Capsule (April 7, 2005) │ └── 71222-Micro3DRotation.zip ├── Mascot Capsule v3 race game code example (November 11, 2005) │ └── 76107-Race.zip ├── MascotWorld application example (July 28, 2005) │ └── 74056-MascotWorld.zip ├── Micro3D Master the basic techniques (February 17, 2005) │ └── 70517-Micro3DExamples.zip ├── Transparency using Mascot Capsule Micro3D API (February 17, 2005) │ └── 70515-Micro3D Ghost.zip ├── Working with Mascot Capsule v3 and Mobile 3D Graphics (April 24, 2007) │ └── 94465-tutorial_java_3d_programming_r1a.zip ├── bactraexporter_max.zip ├── com_mc_javadoc_se.zip ├── data_format_bac6_2_1.zip ├── data_format_tra4_2_1.zip ├── emuforstar2_0_1_01.txt ├── jdk-6u45-windows-i586.txt ├── m3dconverter_v5_1.zip ├── mcv3_usersman1_1.zip ├── midp_v3_tutorial_1_0_1_e.pdf ├── pac_v3_1.zip ├── pgman_com_mc_v3_102.zip ├── pvmicro_v5_0.zip ├── trafilegenerator_v1_0.zip ├── tu_mcdev_gen_pdf_en.zip └── tutorial_v3_midp.zip ├── Examples └── .keep ├── Format_Descriptions ├── .keep └── MBAC.md ├── README.md ├── bactra-blender ├── bac6_dataformat_2_1_en.pdf ├── bac6_document_j_2_0_070330.pdf ├── bac6_document_j_2_1_070528.pdf ├── bactraexporter_blender_beta.zip ├── tra4_dataformat_2_1_en.pdf ├── tra4_document_j_2_0_070330.pdf └── tra4_document_j_2_1_070607.pdf └── tools ├── analyze_mbac.py ├── blender_importscript.py ├── fishlabs_obfuscation.py ├── mascotcapsule.py ├── mbac2obj.py ├── mtratool.py ├── render_obj.py └── stats.py /Docs_Resources_SDK/102089-semc_java_me_cldc_sdk_2_5_0_3.txt: -------------------------------------------------------------------------------- 1 | https://archive.org/details/102089-semc_java_me_cldc_sdk_2_5_0_3.zip 2 | -------------------------------------------------------------------------------- /Docs_Resources_SDK/3ds_max_dlls.rar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/3ds_max_dlls.rar -------------------------------------------------------------------------------- /Docs_Resources_SDK/Code sample – Rotation around an arbitrary axis in Mascot Capsule (April 7, 2005)/71222-Micro3DRotation.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/Code sample – Rotation around an arbitrary axis in Mascot Capsule (April 7, 2005)/71222-Micro3DRotation.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/Mascot Capsule v3 race game code example (November 11, 2005)/76107-Race.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/Mascot Capsule v3 race game code example (November 11, 2005)/76107-Race.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/MascotWorld application example (July 28, 2005)/74056-MascotWorld.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/MascotWorld application example (July 28, 2005)/74056-MascotWorld.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/Micro3D Master the basic techniques (February 17, 2005)/70517-Micro3DExamples.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/Micro3D Master the basic techniques (February 17, 2005)/70517-Micro3DExamples.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/Transparency using Mascot Capsule Micro3D API (February 17, 2005)/70515-Micro3D Ghost.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/Transparency using Mascot Capsule Micro3D API (February 17, 2005)/70515-Micro3D Ghost.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/Working with Mascot Capsule v3 and Mobile 3D Graphics (April 24, 2007)/94465-tutorial_java_3d_programming_r1a.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/Working with Mascot Capsule v3 and Mobile 3D Graphics (April 24, 2007)/94465-tutorial_java_3d_programming_r1a.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/bactraexporter_max.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/bactraexporter_max.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/com_mc_javadoc_se.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/com_mc_javadoc_se.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/data_format_bac6_2_1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/data_format_bac6_2_1.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/data_format_tra4_2_1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/data_format_tra4_2_1.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/emuforstar2_0_1_01.txt: -------------------------------------------------------------------------------- 1 | https://archive.org/details/emuforstar2_0_1_01 2 | -------------------------------------------------------------------------------- /Docs_Resources_SDK/jdk-6u45-windows-i586.txt: -------------------------------------------------------------------------------- 1 | https://mega.nz/#!HcxzwKSa!rUJgcWTVpHOANfqaMiK1buQc7b_k1OG0MxGH-9lgNLw 2 | -------------------------------------------------------------------------------- /Docs_Resources_SDK/m3dconverter_v5_1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/m3dconverter_v5_1.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/mcv3_usersman1_1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/mcv3_usersman1_1.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/midp_v3_tutorial_1_0_1_e.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/midp_v3_tutorial_1_0_1_e.pdf -------------------------------------------------------------------------------- /Docs_Resources_SDK/pac_v3_1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/pac_v3_1.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/pgman_com_mc_v3_102.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/pgman_com_mc_v3_102.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/pvmicro_v5_0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/pvmicro_v5_0.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/trafilegenerator_v1_0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/trafilegenerator_v1_0.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/tu_mcdev_gen_pdf_en.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/tu_mcdev_gen_pdf_en.zip -------------------------------------------------------------------------------- /Docs_Resources_SDK/tutorial_v3_midp.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Docs_Resources_SDK/tutorial_v3_midp.zip -------------------------------------------------------------------------------- /Examples/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Examples/.keep -------------------------------------------------------------------------------- /Format_Descriptions/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/Format_Descriptions/.keep -------------------------------------------------------------------------------- /Format_Descriptions/MBAC.md: -------------------------------------------------------------------------------- 1 | MBAC file format 2 | ================ 3 | 4 | ## Abstract 5 | 6 | The MBAC format contains geometry data, referred to in M3D as Figures. No official documentation for the format is available. It is a binary serialization of the documented BAC format which is used in the content pipeline. A MBAC file is often accompanied by a MTRA file which contains animation data. 7 | 8 | MBAC files don't store any texture names, but they have a notion of texture indices. 9 | 10 | ## Known implementations 11 | 12 | All known implementations are in native code. 13 | 14 | - MascotCapsule SDK (M3DConverter, PVMicro) 15 | - Sony Ericsson WTK emulator (in zayitlib.dll) 16 | - official implementation used in cell phones (we don't have this code) 17 | 18 | Known NON-implementations (as of 2017/12): 19 | 20 | - KEmulator (only implements parts of the API) 21 | - FreeJ2ME (stubbed) 22 | - J2ME-Loader (doesn't implement API) 23 | - phoneME (doesn't implement API) 24 | 25 | ## Overall structure 26 | 27 | - Header(s) 28 | - Vertices 29 | - Normals (if present) 30 | - Faces (types: T3, T4, F3, F4) 31 | - Bones 32 | - 20-byte encrypted trailer 33 | 34 | ## Format versions 35 | 36 | Known versions of the format, along with allowed data encodings, are listed below. 37 | 38 | | Version | vertexformats | normalformats | polygonformats | boneformats(?) | Seen in | 39 | |----------------|---------------|---------------|----------------|-------------------|---------| 40 | | 3 | 1 | 0 (= N/A) | 1 | 1 | 41 | | 4 | 1, 2 | 0, 1, 2 | 1, 2 | 1 | 42 | | 5 | 1, 2 | 0, 1, 2 | 1, 2, 3 | 1 | Burning Tires, GoF, Deep, Blades & Magic, GoF 2 | 43 | 44 | Some data: https://docs.google.com/spreadsheets/d/1KYqJr-XSWoTbdRDF-JLuCdAbYaKsaRQLp_yNlxpCmp8/edit?usp=sharing 45 | 46 | ## Format description 47 | 48 | All fields are in little-endian. 49 | 50 | The file starts with this common header: 51 | 52 | char magic[2] = {'M', 'B'}; 53 | uint16_t formatversion; 54 | 55 | if (formatversion > 3) { 56 | uint8_t vertexformat; 57 | uint8_t normalformat; 58 | uint8_t polygonformat; 59 | uint8_t boneformat?; 60 | } 61 | 62 | uint16_t num_vertices; 63 | uint16_t num_polyT3; 64 | uint16_t num_polyT4; 65 | uint16_t num_bones; 66 | 67 | For polygonformat >= 3, more header data follows. 68 | 69 | uint16_t num_polyF3; 70 | uint16_t num_polyF4; 71 | uint16_t matcnt0C; // something like number of materials 72 | uint16_t max0E; // unknown, but related to some polygon attributes 73 | uint16_t num_color; 74 | 75 | // meaning of the following is unknown; likely to be related to materials and/or textures 76 | repeat(max0E) { 77 | uint16_t unk1; 78 | uint16_t unk2; 79 | 80 | repeat(matcnt0E) { 81 | uint16_t unk3; 82 | uint16_t unk4; 83 | } 84 | } 85 | 86 | ### Vertices (vertexformat=2) 87 | 88 | Vertices are packed in blocks of up to 64 each. These blocks form a bitstream and are not necessarily byte-aligned. The entire bitstream does start on byte boundary and, if necessary, is padded to also end on one. 89 | 90 | align uint8_t; 91 | 92 | bitstream { 93 | while (have_vertices < num_vertices) { 94 | uint(6) count_minus_one; 95 | uint(2) range; 96 | 97 | num_vertices_in_block := count_minus_one + 1; 98 | 99 | switch (range) { 100 | case 0: 101 | repeat(num_vertices_in_block) { 102 | sint(8) x; 103 | sint(8) y; 104 | sint(8) z; 105 | } 106 | break; 107 | case 1: 108 | repeat(num_vertices_in_block) { 109 | sint(10) x; 110 | sint(10) y; 111 | sint(10) z; 112 | } 113 | break; 114 | case 2: 115 | repeat(num_vertices_in_block) { 116 | sint(13) x; 117 | sint(13) y; 118 | sint(13) z; 119 | } 120 | break; 121 | case 3: 122 | repeat(num_vertices_in_block) { 123 | sint(16) x; 124 | sint(16) y; 125 | sint(16) z; 126 | } 127 | break; 128 | } 129 | 130 | have_vertices := have_vertices + num_vertices_in_block; 131 | } 132 | } 133 | 134 | align uint8_t; 135 | 136 | ### Normals (normalformat=2) 137 | 138 | If normalformat=0, there is no data for normals. 139 | 140 | Normals are packed in blocks of up to 64 each. These blocks form a bitstream and are not necessarily byte-aligned. The entire bitstream does start on byte boundary and, if necessary, is padded to also end on one. 141 | 142 | bitstream { 143 | repeat (num_vertices) { 144 | sint(7) x; 145 | 146 | if (x == -64) { 147 | uint(3) direction; 148 | } 149 | else { 150 | sint(7) y; 151 | sint(1) z_sign; 152 | } 153 | } 154 | } 155 | 156 | TODO: describe `direction` 157 | 158 | If XYZ format is used, Z can be calculated as `+/- sqrt(1 - x^2 - y^2)`. `z_sign` is -1 if Z is negative. 159 | 160 | Some files I've examined have x^2 + y^2 > 1, not sure what that means. 161 | 162 | ### Faces (polygonformat=3) 163 | 164 | F-polygons: not documented for now. 165 | 166 | T-polygons: 167 | 168 | uint(8) unknown_bits; 169 | uint(8) vertex_index_bits; 170 | uint(8) uv_bits; 171 | uint(8) somedata; // meaning unknown 172 | 173 | repeat (num_polyt3) { 174 | uint(unknown_bits) unknown; // could be polygon flags 175 | // not material index, but maybe material flags baked into polygons 176 | 177 | repeat(3) { 178 | uint(vertex_index_bits) vertex_index; 179 | } 180 | 181 | repeat(3) { 182 | uint(uv_bits) u; 183 | uint(uv_bits) v; 184 | } 185 | } 186 | 187 | repeat (num_polyt4) { 188 | uint(unknown_bits) unknown; 189 | 190 | repeat(4) { 191 | uint(vertex_index_bits) vertex_index; 192 | } 193 | 194 | repeat(4) { 195 | uint(uv_bits) u; 196 | uint(uv_bits) v; 197 | } 198 | } 199 | 200 | UV coordinates seem to be simply in pixels. Unknown if `uv_bits` can be 0. 201 | 202 | Note that face->material mapping is not stored here. 203 | 204 | ### Bones (boneformat=1) 205 | 206 | repeat (num_bones) { 207 | uint(16) seg_vertices; 208 | int(16) parent; 209 | 210 | int(16) matrix[3][4]; 211 | } 212 | 213 | `seg_vertices` specifies the number of vertices in this bone (starting at wherever the previous bone left off) 214 | 215 | `parent` should be "-1" for exactly one bone (root of the skeleton), otherwise it is the non-negative index of parent bone. 216 | 217 | `matrix` is 3 rows (outer loop) by 4 columns; columns 0 through 2 are pre-multiplied by 4096 before conversion to int16; in other words, they use 4.12 signed format. Usually, the matrix will specify rotation + translation, but always unity scale. 218 | 219 | ### 20-byte encrypted trailer 220 | 221 | Format: 222 | 223 | uint(16) key1; 224 | uint(8) data1[8]; 225 | uint(16) key2; 226 | uint(8) data2[8]; 227 | 228 | Data is encrypted using a simple XOR cipher with the corresponding keys. After decryption, `data1` should == `data2`. For MBAC files generated with stock M3DConverter5.1, decrypted data reads as `HI000000` (HI standing for HI CORP.). Abyss engine games have `SE000000` (SE = Sony Ericsson???). 229 | 230 | This might have been used for copyright protection, as hinted in `man_pg_comdot_v3_e_102.pdf`. 231 | 232 | During conversion, keys are chosen using an MT PRNG (seeded with `time()`) and some scene metadata. The plaintext comes from the BAC struct, so it might be possible to specify it in the source BAC file. 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MascotCapsule Archaeology 2 | 3 | - [Javadoc for com.mascotcapsule.micro3d.v3](https://j2me-preservation.github.io/MascotCapsule/javadoc/) 4 | - [List of known games](https://github.com/j2me-preservation/MascotCapsule/wiki/List-of-known-games) 5 | -------------------------------------------------------------------------------- /bactra-blender/bac6_dataformat_2_1_en.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/bactra-blender/bac6_dataformat_2_1_en.pdf -------------------------------------------------------------------------------- /bactra-blender/bac6_document_j_2_0_070330.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/bactra-blender/bac6_document_j_2_0_070330.pdf -------------------------------------------------------------------------------- /bactra-blender/bac6_document_j_2_1_070528.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/bactra-blender/bac6_document_j_2_1_070528.pdf -------------------------------------------------------------------------------- /bactra-blender/bactraexporter_blender_beta.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/bactra-blender/bactraexporter_blender_beta.zip -------------------------------------------------------------------------------- /bactra-blender/tra4_dataformat_2_1_en.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/bactra-blender/tra4_dataformat_2_1_en.pdf -------------------------------------------------------------------------------- /bactra-blender/tra4_document_j_2_0_070330.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/bactra-blender/tra4_document_j_2_0_070330.pdf -------------------------------------------------------------------------------- /bactra-blender/tra4_document_j_2_1_070607.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j2me-preservation/MascotCapsule/22aafaa22484cf4b29aa5a0e700588917aed56d4/bactra-blender/tra4_document_j_2_1_070607.pdf -------------------------------------------------------------------------------- /tools/analyze_mbac.py: -------------------------------------------------------------------------------- 1 | # analyze all MBAC files in a directory, print out stats 2 | # can also do rendering, but that has been vastly improved in https://github.com/minexew/mbac-gallery 3 | 4 | import os, struct, sys 5 | 6 | import mbac2obj 7 | import render_obj 8 | 9 | from fishlabs_obfuscation import deobfuscate 10 | 11 | DIR = sys.argv[1] 12 | 13 | def render_MBAC(path, filedata): 14 | with open('tmp.mbac', 'wb+') as f: 15 | #f = io.BytesIO(filedata) 16 | f.write(filedata) 17 | f.seek(0) 18 | 19 | print(path, file=sys.stderr) 20 | 21 | TMPOBJ = path + '.obj' 22 | with open(TMPOBJ, 'wt') as obj: 23 | mbac2obj.MBAC_to_obj(f, obj) 24 | 25 | render_obj.render_obj(TMPOBJ, 26 | os.path.join('outputs', path.replace('/', '_').replace('\\', '_'))) 27 | os.remove(TMPOBJ) 28 | 29 | def analyze_filedata(path, filedata, render): 30 | size = len(filedata) 31 | 32 | (magic, version) = struct.unpack('HH', filedata[0:4]) 33 | 34 | if magic != 0x424D: 35 | raise Exception('Not a MBAC file') 36 | if version != 5: 37 | raise Exception('Unsupported MBAC version') 38 | 39 | (vertexformat, normalformat, polygonformat, segmentformat) = struct.unpack('BBBB', filedata[4:8]) 40 | (num_vertices, num_polyt3, num_polyt4, num_segments) = struct.unpack('HHHH', filedata[8:16]) 41 | 42 | (num_polyf3, num_polyf4, matcnt, unk21, num_color) = struct.unpack('HHHHH', filedata[16:26]) 43 | 44 | print('%s,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d' % ( 45 | path, size, version, vertexformat, normalformat, polygonformat, segmentformat, 46 | num_vertices, num_polyt3, num_polyt4, num_segments, 47 | num_polyf3, num_polyf4, matcnt, unk21, num_color)) 48 | 49 | if render: 50 | render_MBAC(path, filedata) 51 | 52 | def analyze_file(path, render): 53 | with open(path, 'rb') as f: 54 | filedata = f.read() 55 | 56 | if filedata[0:2] == b'MB': 57 | analyze_filedata(path, filedata, render) 58 | elif filedata[len(filedata)-2:] == b'BM': 59 | # File is obfuscated (GoF 1, Deep) 60 | analyze_filedata(path, deobfuscate(filedata), render) 61 | else: 62 | raise Exception('Invalid format of ' + path) 63 | 64 | print('Path,Size,formatversion,vertexformat,normalformat,polygonformat,segmentformat,num_vertices,num_polyt3,num_polyt4,num_segments,num_polyf3,num_polyf4,matcnt,unk21,num_color') 65 | 66 | render = True 67 | 68 | for file in os.listdir(DIR): 69 | if file.endswith('.mbac'): 70 | analyze_file(os.path.join(DIR, file), render) 71 | -------------------------------------------------------------------------------- /tools/blender_importscript.py: -------------------------------------------------------------------------------- 1 | # helper for offline rendering using blender 2 | 3 | import bpy 4 | import bmesh 5 | import sys 6 | 7 | argv = sys.argv 8 | argv = argv[argv.index("--") + 1:] # get all args after "--" 9 | 10 | OBJFILE = argv[0] 11 | TEXTURE = argv[1] 12 | RESOLUTION_X = int(argv[2]) 13 | RESOLUTION_Y = int(argv[3]) 14 | AXIS_FORWARD, AXIS_UP = argv[4:6] 15 | TEXTURE_INTERPOLATION = argv[6] 16 | 17 | bpy.ops.object.delete() 18 | bpy.ops.import_scene.obj(filepath=OBJFILE, axis_forward=AXIS_FORWARD, axis_up=AXIS_UP) 19 | 20 | obj, = bpy.context.selected_objects 21 | 22 | if TEXTURE: 23 | mat = bpy.data.materials["Default OBJ"] 24 | bsdf = mat.node_tree.nodes["Principled BSDF"] 25 | 26 | texImage = mat.node_tree.nodes.new('ShaderNodeTexImage') 27 | texImage.image = bpy.data.images.load(TEXTURE) 28 | texImage.interpolation = TEXTURE_INTERPOLATION 29 | mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) 30 | bsdf.inputs['Specular'].default_value = 0 31 | 32 | # scale UVs from pixel coordinates to normalized coordinates 33 | # cheers to https://blender.stackexchange.com/a/111307 34 | def transform_uv(obj, scale, translate): 35 | bpy.context.view_layer.objects.active = obj 36 | bpy.ops.object.mode_set(mode = 'EDIT') 37 | 38 | me = obj.data 39 | bm = bmesh.from_edit_mesh(me) 40 | 41 | uv_layer = bm.loops.layers.uv.verify() 42 | 43 | # adjust uv coordinates 44 | for face in bm.faces: 45 | for loop in face.loops: 46 | loop_uv = loop[uv_layer] 47 | loop_uv.uv = loop_uv.uv[0] * scale[0] + translate[0], loop_uv.uv[1] * scale[1] + translate[1] 48 | 49 | bmesh.update_edit_mesh(me) 50 | bpy.ops.object.mode_set(mode = 'OBJECT') 51 | 52 | w, h = texImage.image.size 53 | 54 | if w > 0 and h > 0: 55 | transform_uv(obj, (1 / w, -1 / h), (0, 1)) 56 | 57 | # scale object down to be lit fully 58 | dim = max(obj.dimensions.x, obj.dimensions.y, obj.dimensions.z) 59 | SCALE = 5 / dim 60 | bpy.ops.transform.resize(value=(SCALE, SCALE, SCALE)) 61 | bpy.ops.view3d.camera_to_view_selected() 62 | 63 | bpy.context.scene.cycles.samples = 16 # reduce rendering time 64 | bpy.context.scene.render.film_transparent = True 65 | bpy.context.scene.render.resolution_x = RESOLUTION_X 66 | bpy.context.scene.render.resolution_y = RESOLUTION_Y 67 | -------------------------------------------------------------------------------- /tools/fishlabs_obfuscation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # implements Fishlabs asset (de)obfuscation 4 | 5 | import sys 6 | 7 | def deobfuscate(data): 8 | length = len(data) 9 | 10 | data = bytearray(data) 11 | 12 | if length < 100: 13 | var5 = 10 + length % 10 14 | elif length < 200: 15 | var5 = 50 + length % 20 16 | elif length < 300: 17 | var5 = 80 + length % 20 18 | else: 19 | var5 = 100 + length % 50 20 | 21 | for i in range(var5): 22 | var7 = data[i] 23 | data[i] = data[length - i - 1] 24 | data[length - i - 1] = var7 25 | 26 | return bytes(data) 27 | 28 | # deobfuscate if needed 29 | def normalize(data, ext): 30 | if is_obfuscated(data, ext): 31 | return deobfuscate(data) 32 | else: 33 | return data 34 | 35 | def is_obfuscated(data, ext): 36 | if ext.upper() == '.BMP': 37 | if data[-2:] == b'MB': 38 | return True 39 | elif data[0:2] == b'BM': 40 | return False 41 | elif ext.upper() == '.MBAC': 42 | if data[-2:] == b'BM': 43 | return True 44 | elif data[0:2] == b'MB': 45 | return False 46 | elif ext.upper() == '.PNG': 47 | if data[-4:] == b'GNP\x89': 48 | return True 49 | elif data[0:4] == b'\x89PNG': 50 | return False 51 | 52 | return None 53 | 54 | if __name__ == "__main__": 55 | import argparse 56 | 57 | parser = argparse.ArgumentParser(description='(de)obfuscate Fishlabs game assets') 58 | parser.add_argument('file', nargs='+') 59 | args = parser.parse_args() 60 | 61 | for filename in args.file: 62 | with open(filename, 'rb+') as f: 63 | data = f.read() 64 | f.seek(0) 65 | f.write(deobfuscate(data)) 66 | -------------------------------------------------------------------------------- /tools/mascotcapsule.py: -------------------------------------------------------------------------------- 1 | import math 2 | import struct 3 | 4 | MAGNITUDE_8BIT = 0 5 | MAGNITUDE_10BIT = 1 6 | MAGNITUDE_13BIT = 2 7 | MAGNITUDE_16BIT = 3 8 | 9 | class Unpacker: 10 | def __init__(self, f): 11 | self.f = f 12 | self.havebits = 0 13 | self.data = 0 14 | 15 | def unpackbits(self, nbits): 16 | while nbits > self.havebits: 17 | self.addbits() 18 | 19 | bits = self.data & ((1 << nbits) - 1) 20 | self.havebits -= nbits 21 | self.data >>= nbits 22 | return bits 23 | 24 | def unpackbits_signed(self, nbits): 25 | value = self.unpackbits(nbits) 26 | sign = value & (1 << (nbits - 1)) 27 | 28 | if sign: 29 | value -= (1 << nbits) 30 | 31 | return value 32 | 33 | def addbits(self): 34 | self.data |= ord(self.f.read(1)) << self.havebits 35 | self.havebits += 8 36 | 37 | def unpackvertices_f2(unp): 38 | header = unp.unpackbits(8) 39 | magnitude = header >> 6 40 | count = (header & 0x3F) + 1 41 | 42 | #print('packed vertices: magnitude=%d, count=%d' % (magnitude, count)) 43 | 44 | print_vertices = False 45 | vertices = [] 46 | 47 | if magnitude == MAGNITUDE_8BIT: 48 | for i in range(count): 49 | x = unp.unpackbits_signed(8) 50 | y = unp.unpackbits_signed(8) 51 | z = unp.unpackbits_signed(8) 52 | if print_vertices: print(' vert', x, y, z) 53 | 54 | vertices += [(x, y, z)] 55 | elif magnitude == MAGNITUDE_10BIT: 56 | for i in range(count): 57 | x = unp.unpackbits_signed(10) 58 | y = unp.unpackbits_signed(10) 59 | z = unp.unpackbits_signed(10) 60 | if print_vertices: print(' vert', x, y, z) 61 | 62 | vertices += [(x, y, z)] 63 | elif magnitude == MAGNITUDE_13BIT: 64 | for i in range(count): 65 | x = unp.unpackbits_signed(13) 66 | y = unp.unpackbits_signed(13) 67 | z = unp.unpackbits_signed(13) 68 | if print_vertices: print(' vert', x, y, z) 69 | 70 | vertices += [(x, y, z)] 71 | elif magnitude == MAGNITUDE_16BIT: 72 | for i in range(count): 73 | x = unp.unpackbits_signed(16) 74 | y = unp.unpackbits_signed(16) 75 | z = unp.unpackbits_signed(16) 76 | if print_vertices: print(' vert', x, y, z) 77 | 78 | vertices += [(x, y, z)] 79 | else: 80 | raise Exception('invalid magnitude') 81 | 82 | return vertices 83 | 84 | def unpacknormals_f2(unp): 85 | x = unp.unpackbits_signed(7) 86 | 87 | print_normals = False 88 | 89 | if x == -64: 90 | direction = unp.unpackbits(3) 91 | if print_normals: print(' direction', direction) 92 | else: 93 | x = x / 64 94 | y = unp.unpackbits_signed(7) / 64 95 | z_negative = unp.unpackbits(1) 96 | #print(x, y, z_negative, 1 - x * x - y * y) 97 | 98 | if 1 - x * x - y * y >= 0: 99 | z = math.sqrt(1 - x * x - y * y) * (-1 if z_negative else 1) 100 | else: 101 | # what 102 | z = 0 103 | 104 | if print_normals: print(' norm', x, y, z) 105 | 106 | return 1 107 | 108 | class Figure: 109 | def __init__(self): 110 | self.vertices = [] 111 | self.faces = [] 112 | self.bones = [] 113 | 114 | @staticmethod 115 | def fromfile(*args, **kwargs): 116 | figure = Figure() 117 | figure.load(*args, **kwargs) 118 | return figure 119 | 120 | def load(self, f, verbose=False): 121 | (magic, version) = struct.unpack('HH', f.read(4)) 122 | 123 | if magic != 0x424D: 124 | raise Exception('Not a MBAC file') 125 | if version != 5: 126 | raise Exception('Unsupported MBAC version') 127 | 128 | (vertexformat, normalformat, polygonformat, boneformat) = struct.unpack('BBBB', f.read(4)) 129 | print('magic=%04Xh version=%d vertexformat=%d normalformat=%d polygonformat=%d boneformat=%d' % ( 130 | magic, version, vertexformat, normalformat, polygonformat, boneformat)) 131 | 132 | (num_vertices, num_polyt3, num_polyt4, num_bones) = struct.unpack('HHHH', f.read(8)) 133 | print('num_vertices=%d num_polyt3=%d num_polyt4=%d num_bones=%d' % ( 134 | num_vertices, num_polyt3, num_polyt4, num_bones)) 135 | 136 | 137 | # unsure about matcnt 138 | if polygonformat != 3: 139 | raise Exception('Unsupported polygonformat. Please report this bug.') 140 | 141 | (num_polyf3, num_polyf4, matcnt, unk21, num_color) = struct.unpack('HHHHH', f.read(10)) 142 | print('num_polyf3=%d num_polyf4=%d matcnt=%d unk21=%d num_color=%d' % ( 143 | num_polyf3, num_polyf4, matcnt, unk21, num_color)) 144 | 145 | for i in range(unk21): 146 | (unk1, unk2) = struct.unpack('HH', f.read(4)) 147 | print('#%d unk1=%d unk2=%d' % (i, unk1, unk2)) 148 | 149 | for j in range(matcnt): 150 | (unk3, unk4) = struct.unpack('HH', f.read(4)) 151 | print('\tunk3=%d unk4=%d' % (unk3, unk4)) 152 | 153 | # decode vertices 154 | if vertexformat != 2: 155 | raise Exception('Unsupported vertexformat. Please report this bug.') 156 | 157 | unp = Unpacker(f) 158 | 159 | while len(self.vertices) < num_vertices: 160 | self.vertices += unpackvertices_f2(unp) 161 | 162 | if len(self.vertices) != num_vertices: 163 | print('WARNING: unpacked', len(self.vertices), 'vertices') 164 | 165 | #print('tell:', f.tell()) 166 | 167 | # decode normals 168 | if normalformat: 169 | if normalformat != 2: 170 | raise Exception('Unsupported normalformat. Please report this bug.') 171 | 172 | unp = Unpacker(f) 173 | 174 | have_normals = 0 175 | while have_normals < num_vertices: 176 | have_normals += unpacknormals_f2(unp) 177 | 178 | # decode polygons 179 | if polygonformat != 3: 180 | raise Exception('Unsupported polygonformat. Please report this bug.') 181 | 182 | #print('tell:', f.tell()) 183 | 184 | unp = Unpacker(f) 185 | 186 | # expect 0x07 now 187 | #if f.read(1) != b'\x07': 188 | # raise Exception('Unexpected encoding mode. Please report this bug.') 189 | 190 | if num_polyf3 + num_polyf4 > 0: 191 | unknown_bits = unp.unpackbits(8) 192 | vertex_index_bits = unp.unpackbits(8) 193 | color_bits = unp.unpackbits(8) 194 | color_id_bits = unp.unpackbits(8) 195 | unp.unpackbits(8) 196 | 197 | for i in range(num_color): 198 | r = unp.unpackbits(color_bits) 199 | g = unp.unpackbits(color_bits) 200 | b = unp.unpackbits(color_bits) 201 | 202 | for i in range(num_polyf3): 203 | unknown = unp.unpackbits(unknown_bits) 204 | a = unp.unpackbits(vertex_index_bits) 205 | b = unp.unpackbits(vertex_index_bits) 206 | c = unp.unpackbits(vertex_index_bits) 207 | 208 | color_id = unp.unpackbits(color_id_bits) 209 | 210 | if verbose: print('unknown=%d a=%d b=%d c=%d color_id=%d' % ( 211 | unknown, a, b, c, color_id)) 212 | self.faces.append((a, b, c)) 213 | 214 | for i in range(num_polyf4): 215 | unknown = unp.unpackbits(unknown_bits) 216 | a = unp.unpackbits(vertex_index_bits) 217 | b = unp.unpackbits(vertex_index_bits) 218 | c = unp.unpackbits(vertex_index_bits) 219 | d = unp.unpackbits(vertex_index_bits) 220 | 221 | color_id = unp.unpackbits(color_id_bits) 222 | 223 | if verbose: print('unknown=%d a=%d b=%d c=%d d=%d color_id=%d' % ( 224 | unknown, a, b, c, d, color_id)) 225 | self.faces.append((a, b, c, d)) 226 | 227 | if num_polyt3 + num_polyt4 > 0: 228 | unknown_bits = unp.unpackbits(8) 229 | vertex_index_bits = unp.unpackbits(8) 230 | uv_bits = unp.unpackbits(8) 231 | somedata = unp.unpackbits(8) 232 | 233 | if verbose: print('POLYGONFORMAT 3: unknown_bits=%d vertex_index_bits=%d uv_bits=%d somedata=%d' % ( 234 | unknown_bits, vertex_index_bits, uv_bits, somedata)) 235 | 236 | # Highest value seen in the wild (Devil May Cry 3D.jar) 237 | if unknown_bits > 10: 238 | raise Exception('Format error. Please report this bug.') 239 | 240 | #max_index = 0 241 | for i in range(num_polyt3): 242 | unknown = unp.unpackbits(unknown_bits) 243 | a = unp.unpackbits(vertex_index_bits) 244 | b = unp.unpackbits(vertex_index_bits) 245 | c = unp.unpackbits(vertex_index_bits) 246 | 247 | u1 = unp.unpackbits(uv_bits) 248 | v1 = unp.unpackbits(uv_bits) 249 | u2 = unp.unpackbits(uv_bits) 250 | v2 = unp.unpackbits(uv_bits) 251 | u3 = unp.unpackbits(uv_bits) 252 | v3 = unp.unpackbits(uv_bits) 253 | 254 | if verbose: print('unknown=%d a=%d b=%d c=%d (%d %d) (%d %d) (%d %d)' % ( 255 | unknown, a, b, c, u1, v1, u2, v2, u3, v3)) 256 | #max_index = max(max_index, a, b, c) 257 | 258 | self.faces.append((a, b, c, u1, v1, u2, v2, u3, v3)) 259 | 260 | #max_index = 0 261 | for i in range(num_polyt4): 262 | unknown = unp.unpackbits(unknown_bits) 263 | a = unp.unpackbits(vertex_index_bits) 264 | b = unp.unpackbits(vertex_index_bits) 265 | c = unp.unpackbits(vertex_index_bits) 266 | d = unp.unpackbits(vertex_index_bits) 267 | 268 | u1 = unp.unpackbits(uv_bits) 269 | v1 = unp.unpackbits(uv_bits) 270 | u2 = unp.unpackbits(uv_bits) 271 | v2 = unp.unpackbits(uv_bits) 272 | u3 = unp.unpackbits(uv_bits) 273 | v3 = unp.unpackbits(uv_bits) 274 | u4 = unp.unpackbits(uv_bits) 275 | v4 = unp.unpackbits(uv_bits) 276 | 277 | if verbose: print('unknown=%d a=%d b=%d c=%d d=%d (%d %d) (%d %d) (%d %d) (%d %d)' % ( 278 | unknown, a, b, c, d, u1, v1, u2, v2, u3, v3, u4, v4)) 279 | #max_index = max(max_index, a, b, c, d) 280 | 281 | self.faces.append((a, b, c, d, u1, v1, u2, v2, u3, v3, u4, v4)) 282 | 283 | # decode bones 284 | bone_vertices_sum = 0 285 | have_root = False 286 | 287 | for i in range(num_bones): 288 | if verbose: print('bone #', i) 289 | (bone_vertices, parent) = struct.unpack('Hh', f.read(4)) 290 | if verbose: print('\tbone_vertices=%d parent=%d' % (bone_vertices, parent)) 291 | 292 | (m00, m01, m02, m03) = struct.unpack('hhhh', f.read(8)) 293 | (m10, m11, m12, m13) = struct.unpack('hhhh', f.read(8)) 294 | (m20, m21, m22, m23) = struct.unpack('hhhh', f.read(8)) 295 | 296 | if verbose: 297 | print('\tmtx = [%d\t%d\t%d\t%d]' % (m00, m01, m02, m03)) 298 | print('\t [%d\t%d\t%d\t%d]' % (m10, m11, m12, m13)) 299 | print('\t [%d\t%d\t%d\t%d]' % (m20, m21, m22, m23)) 300 | 301 | if parent == -1: 302 | if have_root: 303 | raise Exception('Format error (multiple roots). Please report this bug.') 304 | elif parent < 0: 305 | raise Exception('Format error (negative parent). Please report this bug.') 306 | 307 | mtx = (m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23) 308 | mtx_parent = self.get_parent_matrix(parent) 309 | mtx_res = self.mult_matrix(mtx_parent, mtx) 310 | self.bones.append((parent, mtx_res, bone_vertices_sum, bone_vertices_sum + bone_vertices)) 311 | 312 | bone_vertices_sum += bone_vertices 313 | 314 | if bone_vertices_sum != num_vertices: 315 | raise Exception('Format error (bone_vertices_sum). Please report this bug.') 316 | 317 | # apply bone transformations 318 | for i, (parent, mtx, start, end) in enumerate(self.bones): 319 | # apply transformation to vertices 320 | for i in range(start, end): 321 | x, y, z = self.vertices[i] 322 | m = mtx 323 | self.vertices[i] = ((m[ 0] * x + m[ 1] * y + m[ 2] * z) // 4096 + m[ 3], 324 | (m[ 4] * x + m[ 5] * y + m[ 6] * z) // 4096 + m[ 7], 325 | (m[ 8] * x + m[ 9] * y + m[10] * z) // 4096 + m[11]) 326 | 327 | # MT trailer 328 | for i in range(2): 329 | key = struct.unpack('BB', f.read(2)) 330 | 331 | if verbose: print('key: %02X%02X' % key) 332 | 333 | for word_index in range(4): 334 | word = [None, None] 335 | 336 | for byte_index in range(2): 337 | encrypted_byte = ord(f.read(1)) 338 | word[byte_index] = ((encrypted_byte ^ key[byte_index]) + 127) & 0xff 339 | 340 | if verbose: print('%02X%02X "%c%c"' % tuple(word + word)) 341 | 342 | end_at = f.tell() 343 | f.seek(0,2) 344 | file_end = f.tell() 345 | 346 | if file_end > end_at: 347 | print('WARNING: %d uninterpreted bytes in file' % (file_end - end_at)) 348 | 349 | def get_parent_matrix(self, parent): 350 | if parent < 0: 351 | parent_mtx = (4096, 0, 0, 0, 352 | 0, 4096, 0, 0, 353 | 0, 0, 4096, 0) 354 | else: 355 | assert parent < len(self.bones) 356 | parent_mtx = self.bones[parent][1] 357 | 358 | return parent_mtx 359 | 360 | def mult_matrix(self, a, b): 361 | m00 = (a[0] * b[0] + a[1] * b[4] + a[2] * b[8]) / 4096 362 | m01 = (a[0] * b[1] + a[1] * b[5] + a[2] * b[9]) / 4096 363 | m02 = (a[0] * b[2] + a[1] * b[6] + a[2] * b[10]) / 4096 364 | m03 = (a[0] * b[3] + a[1] * b[7] + a[2] * b[11]) / 4096 + a[3] 365 | 366 | m10 = (a[4] * b[0] + a[5] * b[4] + a[6] * b[8]) / 4096 367 | m11 = (a[4] * b[1] + a[5] * b[5] + a[6] * b[9]) / 4096 368 | m12 = (a[4] * b[2] + a[5] * b[6] + a[6] * b[10]) / 4096 369 | m13 = (a[4] * b[3] + a[5] * b[7] + a[6] * b[11]) / 4096 + a[7] 370 | 371 | m20 = (a[8] * b[0] + a[9] * b[4] + a[10] * b[8]) / 4096 372 | m21 = (a[8] * b[1] + a[9] * b[5] + a[10] * b[9]) / 4096 373 | m22 = (a[8] * b[2] + a[9] * b[6] + a[10] * b[10]) / 4096 374 | m23 = (a[8] * b[3] + a[9] * b[7] + a[10] * b[11]) / 4096 + a[11] 375 | 376 | return (m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23) 377 | -------------------------------------------------------------------------------- /tools/mbac2obj.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # MBAC to OBJ converter (stand-alone / importable) 4 | # todo: do we really want to use this garbage format? PLY is a thing. 5 | 6 | from mascotcapsule import Figure 7 | 8 | def fixup_index(index): 9 | if index >= 0: 10 | return 1 + index 11 | else: 12 | return index 13 | 14 | class VertexSink: 15 | def __init__(self, f): 16 | self.f = f 17 | 18 | def sink(self, x, y, z): 19 | print('v %d %d %d' % (x, y, z), file=self.f) 20 | 21 | def normal(self, x, y, z): 22 | print('n %d %d %d' % (x, t, z), file=self.f) 23 | 24 | def texcoord(self, u, v): 25 | print(f'vt {u} {v}', file=self.f) 26 | 27 | def quad_vt(self, v1, v2, v3, v4, vt1, vt2, vt3, vt4): 28 | v1, v2, v3, v4, vt1, vt2, vt3, vt4 = [fixup_index(i) for i in (v1, v2, v3, v4, vt1, vt2, vt3, vt4)] 29 | print(f'f {v1}/{vt1} {v2}/{vt2} {v3}/{vt3} {v4}/{vt4}', file=self.f) 30 | 31 | def triangle_vt(self, v1, v2, v3, vt1, vt2, vt3): 32 | v1, v2, v3, vt1, vt2, vt3 = [fixup_index(i) for i in (v1, v2, v3, vt1, vt2, vt3)] 33 | print(f'f {v1}/{vt1} {v2}/{vt2} {v3}/{vt3}', file=self.f) 34 | 35 | def MBAC_to_obj(f, obj, verbose=False): 36 | figure = Figure.fromfile(f, verbose) 37 | 38 | vs = VertexSink(obj) 39 | 40 | for x, y, z in figure.vertices: 41 | vs.sink(x, y, z) 42 | 43 | for face in figure.faces: 44 | if len(face) == 3: 45 | a, b, c = face 46 | 47 | vs.triangle_vt(a, b, c, -3, -2, -1) 48 | elif len(face) == 4: 49 | a, b, c, d = face 50 | 51 | vs.quad_vt(a, b, d, c, -4, -3, -1, -2) 52 | elif len(face) == 9: 53 | a, b, c, u1, v1, u2, v2, u3, v3 = face 54 | 55 | vs.texcoord(u1, v1) 56 | vs.texcoord(u2, v2) 57 | vs.texcoord(u3, v3) 58 | 59 | vs.triangle_vt(a, b, c, -3, -2, -1) 60 | else: 61 | a, b, c, d, u1, v1, u2, v2, u3, v3, u4, v4 = face 62 | 63 | vs.texcoord(u1, v1) 64 | vs.texcoord(u2, v2) 65 | vs.texcoord(u3, v3) 66 | vs.texcoord(u4, v4) 67 | 68 | # last 2 vertices need to be swapped to render correctly 69 | vs.quad_vt(a, b, d, c, -4, -3, -1, -2) 70 | 71 | if __name__ == "__main__": 72 | import argparse 73 | 74 | parser = argparse.ArgumentParser(description='convert MBAC models to OBJ') 75 | parser.add_argument('mbacfile') 76 | parser.add_argument('objfile') 77 | parser.add_argument("-v", dest="verbose", action="store_true") 78 | args = parser.parse_args() 79 | 80 | with open(args.mbacfile, 'rb') as f: 81 | with open(args.objfile, 'wt') as obj: 82 | MBAC_to_obj(f, obj, args.verbose) 83 | -------------------------------------------------------------------------------- /tools/mtratool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import struct 4 | 5 | def MTRA(f, verbose=False): 6 | (magic, version) = struct.unpack('2sH', f.read(4)) 7 | 8 | if magic != b'MT': 9 | raise Exception('Not a MTRA file. Perhaps it needs to be deobfuscated first?') 10 | if version != 5: 11 | raise Exception('Unsupported MTRA version') 12 | 13 | num_actions, num_segments, tra_unk3, tra_unk4 = struct.unpack(' end_at: 96 | print('WARNING: %d uninterpreted bytes in file' % (file_end - end_at)) 97 | 98 | if __name__ == "__main__": 99 | import argparse 100 | 101 | parser = argparse.ArgumentParser(description='dump MTRA file') 102 | parser.add_argument('mtrafile') 103 | args = parser.parse_args() 104 | 105 | with open(args.mtrafile, 'rb') as f: 106 | MTRA(f, True) 107 | -------------------------------------------------------------------------------- /tools/render_obj.py: -------------------------------------------------------------------------------- 1 | # helper to render OBJ files (together with blender_importscript.py) 2 | 3 | from pathlib import Path 4 | import os 5 | import subprocess 6 | import time 7 | 8 | def render_obj(objfile, pngfile, texture="", resolution=(1290, 1080), format="PNG", axis_forward='-Z', axis_up='Y', 9 | texture_interpolation="Linear"): 10 | script_path = Path(__file__).resolve().parent / "blender_importscript.py" 11 | subprocess.check_call([ 12 | "blender", 13 | '--background', 14 | "--engine", "CYCLES", # only Cycles rendering works without OpenGL 15 | "-noaudio", # reduce stderr clutter (we don't need no sound) 16 | '--python', str(script_path), 17 | '--render-output', '//' + pngfile, 18 | '--render-format', format, 19 | '--use-extension', '1', 20 | '-f', '0', 21 | '--', objfile, texture if texture else "", str(resolution[0]), str(resolution[1]), 22 | axis_forward, 23 | axis_up, 24 | texture_interpolation, # https://docs.blender.org/api/current/bpy.types.ShaderNodeTexImage.html#bpy.types.ShaderNodeTexImage.interpolation 25 | ], stdout=subprocess.DEVNULL) 26 | 27 | # TODO: is this really needed, and why? 28 | time.sleep(1) 29 | -------------------------------------------------------------------------------- /tools/stats.py: -------------------------------------------------------------------------------- 1 | # look at all JAR files in a directory and print out information 2 | # to help us understand the different versions of the game 3 | 4 | import hashlib, os, sys, zipfile 5 | 6 | def file_hash(path): 7 | h = hashlib.sha256() 8 | 9 | with open(path, 'rb', buffering=0) as f: 10 | for b in iter(lambda : f.read(128*1024), b''): 11 | h.update(b) 12 | 13 | return h.hexdigest() 14 | 15 | def read_manifest(z): 16 | manifest = {} 17 | 18 | with z.open('META-INF/MANIFEST.MF', 'r') as manifest_file: 19 | for line in manifest_file: 20 | line = line.decode().strip() 21 | if not line: continue 22 | [key, value] = line.split(':') 23 | manifest[key.strip()] = value.strip() 24 | 25 | return manifest 26 | 27 | all_bins = [] 28 | 29 | for name in os.listdir(sys.argv[1]): 30 | if not name.endswith('.jar'): continue 31 | #print(name) 32 | path = os.path.join(sys.argv[1], name) 33 | size = os.stat(path).st_size 34 | hash = file_hash(path) 35 | 36 | with zipfile.ZipFile(path, mode='r') as z: 37 | manifest = read_manifest(z) 38 | 39 | contents_size = 0 40 | h = hashlib.sha256() 41 | 42 | for info in z.infolist(): 43 | contents_size += info.file_size 44 | 45 | with z.open(info.filename, 'r') as f: 46 | for b in iter(lambda : f.read(128*1024), b''): 47 | h.update(b) 48 | 49 | contents_hash = h.hexdigest() 50 | 51 | if hash == '03f5b6f9e8e16617db0ffdaa7db0c3abd8c0d50ddf31fe935f78143cf55bf697': 52 | note = 'GoF 2 v1.0.3 RU (Fan Patch?)' 53 | elif hash == '235c6960c6cc714a538eca61b92b453473eb7dd8e8344e1c5b23fed5d6c2fc0a': 54 | note = 'GoF 1.1.2 RU' 55 | tested = 'minexew_w595, some graphics stretched' 56 | elif hash == '271b69a582e60b4c1bb09a53bb0756e6842fdd17ca3808538336b19881fc7e56': 57 | note = 'GoF v1.0.3 DE (Retail?)' 58 | tested = 'minexew_w595, some graphics stretched' 59 | elif hash == '29d07ade596b5bc27e79e8c8604d01e283df205595dbb7b6a142ffcbf4229e83': 60 | note = 'GoF v1.4.8 CZ Fan Translation' 61 | tested = 'minexew_w595, runs well' 62 | elif hash == '3eaf078773b7886c5cc1abd2159f93e06af87cdd96258ed29afcae71bc13f11a': 63 | note = 'GoF v1.2 CZ Fan Translation' 64 | tested = 'minexew_w595, some graphics stretched' 65 | elif hash == '4c039e7584dd99a2f91f22b6812e53d93ba03cde601052dd1249769772559bc6': 66 | note = 'GoF v1.2 or newer, EN, with adware' 67 | tested = 'minexew_w595, some graphics stretched' 68 | elif hash == '667f81069891da5bda4929c8a068945bc4f9c099341e11eb57f570d8c6eaa3ca': 69 | note = 'GoF 2 v1.0.3 RU (Fan Patch?)' 70 | elif hash == '7b9dd10e64267cd758a9e8ffd52c488c86822d4a236aa32a825365a43f8732ce': 71 | note = 'GoF v1.0.3 CZ Fan Translation' 72 | elif hash == '9d8e7d5fcc795c92be34c09ebe10480827d055248d114f975a91000bb0901382': 73 | note = 'GoF 2 v1.0.3 SK Fan Translation' 74 | elif hash == 'a5ac630affe0ef222a37abcb67980421dabced79c60d618d2c578a3574cbc08f': 75 | note = 'Deep v1.0.3 EN (Retail?)' 76 | tested = 'minexew_w595' 77 | elif hash == 'aab2276684609839c541b818b290fb4e8087405407ce273dd9256e2dc5386791': 78 | note = 'GoF 2 v1.0.3 EN (Retail?)' 79 | elif hash == 'c2b47c4a6eebe2fd36d40cc4300103c0012c62b60748a185a8e4e6eddd0b4d41': 80 | note = 'Deep v1.0.8 EN (Hacked?)' 81 | tested = 'minexew_w595, laggy & graphical errors' 82 | elif hash == 'ca467a324e310bf44ca5733c457ceadcb1ef33d73449bd586c199919172f2157': 83 | note = 'GoF v1.2.9 EN (hacked version)' 84 | tested = 'minexew_w595, runs well' 85 | elif hash == 'd8013fad92f69418ee73ffd01e99bcf44ef108bad7c830367bd35095badcca8e': 86 | note = 'Deep v1.0.2 EN (Retail?)' 87 | tested = 'minexew_w595' 88 | elif hash == 'ddd6503d2a047cf5fc02987e446ef3e64ec7a04d8ad908850c873cb322ebebca': 89 | note = 'GoF v1.2.9 EN (M3G API version)' 90 | tested = 'minexew_w595, hangs pre-menu' 91 | elif hash == 'ea9dce67fad4c0f139ef7d6b616593981216942d592988a47d5e918d938fd0d5': 92 | note = 'GoF v1.1.2 EN (176x208 version?)' 93 | tested = 'minexew_w595, some graphics stretched' 94 | elif hash == 'f40d40f3efd8d2f34e12752a550770429770a7fa651b9e39adcbeece28ad17c9': 95 | note = 'GoF v1.2.9 EN (M3G API version)' 96 | tested = ['minexew_w595, hangs pre-menu', 'minexew_kemulator, runs'] 97 | elif hash == 'f8944d71919230dc9e9c4343b8a7fdab7706665e578ed572f4c0ec65315c54bf': 98 | note = 'GoF 2 v1.0.3 RU (Fan Patch?)' 99 | elif hash == 'ff74b33a1eabe5bf37528ee633328cde08ceac00a810f8f41ff3e6faa6ff5c09': 100 | node = 'GoF v1.1.2 EN (176x208 version?)' 101 | tested = 'minexew_w595, some graphics stretched' 102 | else: 103 | note = None 104 | 105 | all_bins.append(dict(path=path, name=name, size=size, hash=hash, 106 | contents_size=contents_size, contents_hash=contents_hash, 107 | manifest=manifest, note=note)) 108 | 109 | all_bins = sorted(all_bins, key=lambda d: d['contents_size'], reverse=True) 110 | 111 | print('Name,Size,SHA-256,Contents_Size,Contents_Hash,MIDlet-Name,MIDlet-Version,MIDlet-Vendor,Note') 112 | 113 | for d in all_bins: 114 | print('%s,%d,%s,%d,%s,%s,%s,%s,"%s"' % ( 115 | d['name'], 116 | d['size'], 117 | d['hash'], 118 | d['contents_size'], 119 | d['contents_hash'], 120 | d['manifest']['MIDlet-Name'], 121 | d['manifest']['MIDlet-Version'], 122 | d['manifest']['MIDlet-Vendor'], 123 | d['note'] or '', 124 | )) 125 | --------------------------------------------------------------------------------