├── LICENSE ├── README.md ├── favicon.ico ├── mc3dslib.py └── mc3dslib_updater.py /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Development for mc3dslib has ceased. 2 | ### Please try [mc3dslib2]() for modification to files and other things. 3 | 4 | # mc3dslib 5 | - **A python Library for Minecraft 3DS, allowing for easy Modification of the SaveGames, and romfs Files.** 6 | - **An online installer/Updater was just released alongside version v0.1.0-beta🎉.** 7 | - **Lastest Release: v0.1.3.** 8 | 9 | ## Note: 10 | - **Documentation is Extremely barebones currently. A more in-depth documentation will be added soon.** 11 | - **I've made a Documentation on both MC3DS's [Options.txt](https://github.com/Cracko298/MC3DS-Options-Documentation), and [ARGB .3DST](https://github.com/Cracko298/MC-3DST-Documentation), if you want a more "in-depth explantaion of things."** 12 | 13 | ## Read the Wiki: 14 | - **[mc3dslib](https://github.com/Cracko298/mc3dslib/wiki) Documentation.** 15 | 16 | ## Download(s): 17 | - **Download The Updater/Installer [Here](https://github.com/Cracko298/mc3dslib/releases/download/v0.1.0-beta/mc3dslib_updater.py).** 18 | 19 | 20 | # mc3dslib Overview: 21 | 22 | - **Extract Bytes: `extract_bytes(filename, arg1, arg2)`** 23 | - **Convert Bytes: `convert_bytes(bytestring,order)`** 24 | - **Extract Color: `extract_colors(image_path)`** 25 | - **Invert Colors: `invertclrs(image_path)`** 26 | - **Set Green Hue: `greenify(image_path)`** 27 | - **Set Orange Hue: `orangify(image_path)`** 28 | - **Set Blue Hue: `bluify(image_path)`** 29 | - **Set Red Hue `redify(image_path)`** 30 | - **Grab Meta Data: `meta_grab(image_path)`** 31 | - **Material To Json: `mat2json(file_path)`** 32 | - **Convert Options: `convert_options(file_path,output_file_path)`** 33 | - **Revert Options: `revert_options(file_path,output_file_path`** 34 | - **Blang To Json: `toJson(blang_file)`** 35 | - **Json To Blang: `fromJson(json_file)`** 36 | - **Extract Head: `extract_head(image_path)`** 37 | - **Convert To PNG: `image_convert(image_path)`** 38 | - **Create .r3dst: `create_r3dst(image_path)`** 39 | - **Copy Lines: `copy_lines(filename, line_number, mode)`** 40 | - **Convert CDB To LDB: `console2bedrock_cdb(folder_path, optional_offset)`** 41 | - **Convert VDB To Log: `console2bedrock_vdb(folder_path)`** 42 | - **Copy World Information: `console2bedrock_cdb(folder_path, optional_offset)`** 43 | - **Convert Full World: `convert_save(folder_path, world_icon_path)`** 44 | - **Create Converted World Lockage: `convert_lockage(file_path)`** 45 | - **Convert Stuff into .mcworld: `zip_convert_contents`** 46 | - **Convert Images to 3DST: `convert_2_etc2(image_path)`** 47 | - **Convert 3DST to Images: `convert_2_img(etc2_path)`** 48 | - **Get .3DST Image Demensions: `get_3dst_demensions(etc2_path)`** 49 | - **Get Image Image Demensions: `get_img_demensions(image_path)`** 50 | 51 | ## Importing the Module(s): 52 | ### Defualt Importing: 53 | ```py 54 | import mc3dslib 55 | from mc3dslib import BlangFile 56 | from mc3dslib import * 57 | import mc3dslib as mc3ds 58 | ``` 59 | 60 | ## Blang Conversion(s): 61 | ### Initializing the File: 62 | ```py 63 | import mc3dslib 64 | 65 | file = mc3dslib.BlangFile().open("en_GB.json") # Initialzation of Example File 66 | ``` 67 | 68 | ### JSON TO BLANG 69 | ```py 70 | import mc3dslib 71 | 72 | input_file_path = ".\\" ## Any Valid JSON file can go here 73 | blang_file = mc3dslib.BlangFile().fromJson(input_file_path) 74 | ``` 75 | ### BLANG TO JSON 76 | ```py 77 | import mc3dslib 78 | 79 | blang_file = mc3dslib.BlangFile().open("en_GB.json") 80 | 81 | output_path = ".\\" # Any Valid Path can go here 82 | blang_file.toJson(output_path) 83 | ``` 84 | 85 | # Panning Additions: 86 | - **Convert Achievements** 87 | - **Revert Achievements** 88 | - **Extract Arms** 89 | - **Extract Legs** 90 | - **Extract Body** 91 | 92 | ## Credit(s): 93 | - **[@Wolfyxon](https://github.com/Wolfyxon) - Few of the Functions in the Code.** 94 | - **[@STBrian](https://github.com/STBrian) - MC3DS Blang Format Conversion Code.** 95 | - **[@Cracko298](https://github.com/Cracko298) - Developer of Most Functions in the Code.** 96 | - **[@YT-Toaster](https://github.com/YT-Toaster) - Few of the Functions in the Code.** 97 | - **[olverimcDISC]() - His map was used as a test to conversion methods from 3DS to Bedrock** 98 | ``` 99 | Oliver's Map (LoCity - https://www.minecraft3ds.net/maps/locity) 100 | ``` 101 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cracko298/mc3dslib/fdf42f62fd457f27ae07075489a1c377211273dd/favicon.ico -------------------------------------------------------------------------------- /mc3dslib.py: -------------------------------------------------------------------------------- 1 | import os, json, hashlib, shutil, zipfile, struct 2 | from pathlib import Path 3 | from PIL import Image 4 | 5 | class make_mcworld_struct: 6 | # Cracko298 7 | def make_dirs(self): 8 | os.makedirs("mcworld_files", exist_ok=True) 9 | os.makedirs(os.path.join("mcworld_files", "db"), exist_ok=True) 10 | os.makedirs(os.path.join("mcworld_files", "resource_packs"), exist_ok=True) 11 | os.makedirs(os.path.join("mcworld_files", "behavior_packs"), exist_ok=True) 12 | 13 | open(os.path.join("mcworld_files", "behavior_packs", "#KEEP_FOLDER"), "wb+") 14 | open(os.path.join("mcworld_files", "resource_packs", "#KEEP_FOLDER"), "wb+") 15 | 16 | class MC3DSBlangException(Exception): 17 | def __init__(self, message): 18 | super().__init__(message) 19 | 20 | class BlangFile: 21 | # STBUniverse (STBrian) 22 | def __init__(self): 23 | return 24 | 25 | def open(self, path: str = None): 26 | if path == None: 27 | raise MC3DSBlangException("path is empty") 28 | if type(path) is not str: 29 | raise MC3DSBlangException("path must be a 'str'") 30 | 31 | self.filename = Path(path).stem 32 | 33 | with open(path, "rb") as f: 34 | file_content = list(f.read()) 35 | 36 | # Obtener longitud 37 | long = [] 38 | for i in range(0, 4): 39 | long.append(file_content[i]) 40 | long = int.from_bytes(bytearray(long), "little") 41 | 42 | # Obtener los elementos del indice 43 | idx = 4 44 | data = [] 45 | for i in range(0, long): 46 | join = [] 47 | for j in range(0, 4): 48 | join.append(file_content[idx]) 49 | idx += 1 50 | data.append(join) 51 | idx += 4 52 | 53 | # Longitud de los textos 54 | textlong = [] 55 | for i in range(idx, idx + 4): 56 | textlong.append(file_content[i]) 57 | textlong = int.from_bytes(bytearray(textlong), "little") 58 | 59 | # Obtener los textos 60 | idx += 4 61 | texts = [] 62 | for i in range(0, long): 63 | join = [] 64 | while file_content[idx] != 0: 65 | join.append(file_content[idx]) 66 | idx += 1 67 | texts.append(bytearray(join).decode("utf-8")) 68 | idx += 1 69 | 70 | self.data = data 71 | self.texts = texts 72 | return self 73 | 74 | def getData(self): 75 | return self.data 76 | 77 | def getTexts(self): 78 | return self.texts 79 | 80 | def replace(self, text: str, newtext: str): 81 | if type(text) is not str: 82 | raise MC3DSBlangException("text must be a 'str'") 83 | if type(newtext) is not str: 84 | raise MC3DSBlangException("newtext must be a 'str'") 85 | 86 | if text in self.texts: 87 | if newtext != "": 88 | self.texts[self.texts.index(text)] = newtext 89 | else: 90 | self.texts[self.texts.index(text)] = " " 91 | return 92 | 93 | def export(self, path: str): 94 | if type(path) is not str: 95 | raise MC3DSBlangException("path must be a 'str'") 96 | 97 | long = len(self.data) 98 | indexLong = list(long.to_bytes(4, "little")) 99 | 100 | indexData = [] 101 | textData = [] 102 | for i in range(0, long): 103 | # Copiar los primeros datos del elemento 104 | indexData.extend(self.data[i]) 105 | 106 | # Posición de texto 107 | indexData.extend(list(len(textData).to_bytes(4, "little"))) 108 | 109 | # Agregar texto 110 | textData.extend(list(self.texts[i].encode("utf-8"))) 111 | 112 | # Separador/terminador 113 | textData.append(0) 114 | 115 | textsLong = list(len(textData).to_bytes(4, "little")) 116 | 117 | # Junta todo en una sola lista 118 | self.exportData = [] 119 | self.exportData.extend(indexLong) 120 | self.exportData.extend(indexData) 121 | self.exportData.extend(textsLong) 122 | self.exportData.extend(textData) 123 | 124 | self.exportData = bytearray(self.exportData) 125 | 126 | with open(os.path.join(path, f"{self.filename}.blang"), "wb") as f: 127 | f.write(self.exportData) 128 | return 129 | 130 | def toJson(self, path: str): 131 | long = len(self.data) 132 | dataDictionary = {} 133 | for i in range(0, long): 134 | item = self.data[i] 135 | identifier = [] 136 | for j in range(0, 4): 137 | identifier.append(item[j]) 138 | identifier = bytearray(identifier) 139 | identifier = int.from_bytes(identifier, "little") 140 | identifier = str(identifier) 141 | 142 | dataDictionary[identifier] = {} 143 | dataDictionary[identifier]["order"] = i + 1 144 | dataDictionary[identifier]["text"] = self.texts[i] 145 | 146 | outFile = open(os.path.join(path, f"{self.filename}.json"), "w", encoding="utf-8") 147 | json.dump(dataDictionary, outFile, indent=4, ensure_ascii=False) 148 | outFile.close() 149 | return 150 | 151 | def fromJson(self, path: str): 152 | if type(path) is not str: 153 | raise MC3DSBlangException("path must be a 'str'") 154 | 155 | data = [] 156 | texts = [] 157 | 158 | with open(path, "r", encoding="utf-8") as jsonData: 159 | dataDictionary = json.load(jsonData) 160 | 161 | self.filename = Path(path).stem 162 | 163 | idx = 1 164 | while idx <= len(dataDictionary): 165 | for key in dataDictionary: 166 | if dataDictionary[key]["order"] == idx: 167 | data.append(list(int(key).to_bytes(4, "little"))) 168 | texts.append(dataDictionary[key]["text"]) 169 | idx += 1 170 | break 171 | 172 | self.data = data 173 | self.texts = texts 174 | return self 175 | 176 | def extract_bytes(filename, arg1, arg2): 177 | # Cracko298 178 | with open(filename, "rb+") as file: 179 | try: 180 | file.seek(arg1) 181 | extracted_bytes = file.read(arg2 - arg1) 182 | return extracted_bytes 183 | 184 | except Exception as e: 185 | return f"Error extracting bytes: {e}" 186 | 187 | def convert_bytes(bytestring,order=""): 188 | # Cracko298 189 | if not isinstance(bytestring, (bytes, bytearray)): 190 | return "Invalid input. Please provide a valid bytearray or bytes object." 191 | 192 | if order == "r" or order == "R": 193 | bytestring[::-1] 194 | 195 | elif order == "f" or order == "F" or order == "": 196 | pass 197 | 198 | else: 199 | return "Invlid Order Options." 200 | 201 | byte_data = int.from_bytes(bytestring) 202 | return byte_data 203 | 204 | def extract_colors(image_path): 205 | # Cracko298 206 | tmp0 = image_path.replace('.3dst','') 207 | output_path = f"colors_{tmp0}.txt" 208 | 209 | existing_colors = set() 210 | 211 | try: 212 | with open(output_path, "r") as existing_file: 213 | existing_colors = {line.strip() for line in existing_file} 214 | except FileNotFoundError: 215 | pass 216 | 217 | with open(image_path, "rb") as image_file: 218 | image_file.seek(0x20) 219 | argb_data = image_file.read() 220 | 221 | rgb_hex_values = [] 222 | for i in range(0, len(argb_data), 4): 223 | b, g, r, _ = argb_data[i:i+4] 224 | rgb_hex = "#{:02X}{:02X}{:02X}".format(r, g, b) 225 | 226 | if rgb_hex not in existing_colors: 227 | existing_colors.add(rgb_hex) 228 | rgb_hex_values.append(rgb_hex) 229 | 230 | with open(output_path, "a") as output_file: 231 | for hex_value in rgb_hex_values: 232 | output_file.write(hex_value + "\n") 233 | 234 | def green_filter(rgb_bytes): 235 | red = rgb_bytes[0] 236 | green = rgb_bytes[1] 237 | blue = rgb_bytes[2] 238 | 239 | if red >= 0x10: 240 | red -= 0x10 241 | 242 | if blue >= 0x10: 243 | blue -= 0x10 244 | 245 | green = 0xB0 246 | 247 | green_filtered_rgb = bytearray([red, green, blue]) 248 | return green_filtered_rgb 249 | 250 | def greenify(image_path): 251 | # Cracko298 252 | with open(image_path, "rb+") as file: 253 | while True: 254 | offset = file.tell() 255 | byte = file.read(1) 256 | 257 | if not byte: 258 | break 259 | 260 | if offset >= 0x20 and byte == b'\xFF': 261 | rgb_bytes = file.read(3) 262 | if len(rgb_bytes) == 3: 263 | green_filtered_rgb = green_filter(rgb_bytes) 264 | file.seek(-3, 1) 265 | file.write(green_filtered_rgb) 266 | 267 | return f"Set Green Hue To: '{image_path}'." 268 | 269 | def invert_color(rgb_bytes): 270 | inverted_rgb = bytearray([255 - byte for byte in rgb_bytes]) 271 | return inverted_rgb 272 | 273 | def invertclrs(image_path): 274 | # Cracko298 275 | with open(image_path, "rb+") as file: 276 | while True: 277 | offset = file.tell() 278 | byte = file.read(1) 279 | 280 | if not byte: 281 | break 282 | 283 | if offset >= 0x20 and byte == b'\xFF': 284 | rgb_bytes = file.read(3) 285 | if len(rgb_bytes) == 3: 286 | inverted_rgb = invert_color(rgb_bytes) 287 | file.seek(-3, 1) 288 | file.write(inverted_rgb) 289 | 290 | return f"Inverted Color of: '{image_path}'." 291 | 292 | def red_filter(rgb_bytes): 293 | red = rgb_bytes[0] 294 | green = rgb_bytes[1] 295 | blue = rgb_bytes[2] 296 | 297 | if red >= 0x10: 298 | red -= 0x10 299 | 300 | blue = 0xA0 301 | 302 | if green >= 0x10: 303 | green -= 0x10 304 | 305 | red_filter_bytes = bytearray([red, green, blue]) 306 | return red_filter_bytes 307 | 308 | def redify(image_path): 309 | # Cracko298 310 | with open(image_path, "rb+") as file: 311 | while True: 312 | offset = file.tell() 313 | byte = file.read(1) 314 | 315 | if not byte: 316 | break 317 | 318 | if offset >= 0x20 and byte == b'\xFF': 319 | rgb_bytes = file.read(3) 320 | if len(rgb_bytes) == 3: 321 | red_filter_bytes = red_filter(rgb_bytes) 322 | file.seek(-3, 1) 323 | file.write(red_filter_bytes) 324 | 325 | print(f"Set Red Hue To: '{image_path}'.") 326 | 327 | def orange_filter(rgb_bytes): 328 | red = rgb_bytes[0] 329 | green = rgb_bytes[1] 330 | blue = rgb_bytes[2] 331 | 332 | red = 0x10 333 | 334 | blue = 0xF0 335 | 336 | orange_filter_rgb = bytearray([red, green, blue]) 337 | return orange_filter_rgb 338 | 339 | def orangify(image_path): 340 | # Cracko298 341 | with open(image_path, "rb+") as file: 342 | while True: 343 | offset = file.tell() 344 | byte = file.read(1) 345 | 346 | if not byte: 347 | break 348 | 349 | if offset >= 0x20 and byte == b'\xFF': 350 | rgb_bytes = file.read(3) 351 | if len(rgb_bytes) == 3: 352 | orange_filter_rgb = orange_filter(rgb_bytes) 353 | file.seek(-3, 1) 354 | file.write(orange_filter_rgb) 355 | 356 | print(f"Set Orange/Yellow Hue To: '{image_path}'.") 357 | 358 | def blue_filter(rgb_bytes): 359 | red = rgb_bytes[0] 360 | green = rgb_bytes[1] 361 | blue = rgb_bytes[2] 362 | 363 | red = 0xDF 364 | 365 | green = 0xFF 366 | 367 | blue_filter_rgb = bytearray([red, green, blue]) 368 | return blue_filter_rgb 369 | 370 | def bluify(image_path): 371 | # Cracko298 372 | with open(image_path, "rb+") as file: 373 | while True: 374 | offset = file.tell() 375 | byte = file.read(1) 376 | 377 | if not byte: 378 | break 379 | 380 | if offset >= 0x20 and byte == b'\xFF': 381 | rgb_bytes = file.read(3) 382 | if len(rgb_bytes) == 3: 383 | blue_filter_rgb = blue_filter(rgb_bytes) 384 | file.seek(-3, 1) 385 | file.write(blue_filter_rgb) 386 | 387 | print(f"Set Blue Hue To: '{image_path}'.") 388 | 389 | 390 | def meta_grab(image_path): 391 | # Cracko298 392 | tempdata = image_path.replace('.3dst','') 393 | output_path = f"{tempdata}_metadata.txt" 394 | with open(image_path, "rb") as f, open(output_path, 'a') as of: 395 | data0 = f.read(0x4) 396 | f.seek(0x4) 397 | data1 = f.read(0x01) 398 | f.seek(0xC) 399 | data2 = f.read(0x01) 400 | f.seek(0x10) 401 | data3 = f.read(0x01) 402 | f.seek(0x14) 403 | data4 = f.read(0x01) 404 | f.seek(0x18) 405 | data5 = f.read(0x01) 406 | 407 | int_data = int.from_bytes(data1) 408 | print(f"Texture Mode: {int_data}") 409 | of.write(f"Texture Mode: {int_data}\n") 410 | int_data = int.from_bytes(data2) 411 | print(f"Skin Width: {int_data}") 412 | of.write(f"Skin Width: {int_data}\n") 413 | int_data = int.from_bytes(data3) 414 | print(f"Skin Height: {int_data}") 415 | of.write(f"Skin Height: {int_data}\n") 416 | 417 | int_data = data0.decode("ascii") 418 | print(f"Header Name: {int_data}") 419 | of.write(f"Header Name: {int_data}\n") 420 | print(f'MIP Value: 1') 421 | of.write(f"MIP Value: 1\n") 422 | print("Img Format: RGBA8") 423 | of.write(f"Image Format: RGBA8\n") 424 | print("Bit Depth: 8") 425 | of.write(f"Bit Depth: 8\n") 426 | 427 | int_data = int.from_bytes(data4) 428 | print(f"Width Checksum: {int_data}") 429 | of.write(f"Width Checksum: {int_data}\n") 430 | int_data = int.from_bytes(data5) 431 | print(f"Height Checksum: {int_data}") 432 | of.write(f"Height Checksum: {int_data}\n") 433 | print(" ") 434 | return f"Saved MetaData As: '{output_path}'." 435 | 436 | def mat2json(file_path): 437 | # Cracko298 438 | files = ["material", "material3DS", "images", "bak", "dat"] 439 | filestypes = ["*.material", "*.material3DS", "*.mat", "*.bak", "*.dat"] 440 | 441 | filename = os.path.basename(file_path) 442 | f0, f1 = filename.split('.') 443 | 444 | if f1 not in files: 445 | print(f"WARNING: If you proceed with this file, it may cause an error that could result in a Corrupted File.") 446 | print(f"File extensions that are allowed/expected are as follows: {files}.\n") 447 | print(f"Press the 'Enter Key' to close the Application.") 448 | print(f"Press the '0 Key' then press the 'Enter Key' to continue with the selected file.\n") 449 | exit(1) 450 | else: 451 | pass 452 | 453 | parts = filename.split('.') 454 | out_file = '.'.join(parts[:-1]) + ".json" 455 | 456 | with open(file_path, "rb+") as f: 457 | with open(out_file, "wb") as o: 458 | dats = f.read() 459 | data = dats[::-1] 460 | writable_d = data[::-1] 461 | 462 | o.write(writable_d) 463 | 464 | def convert_options(file_path,output_file_path): 465 | # Cracko298 and Wolfyxon 466 | target_bytes = bytes([0xD8, 0x05, 0x20, 0x20, 0x6D, 0x70]) 467 | with open(file_path, "rb") as file: 468 | content = file.read() 469 | 470 | if content.startswith(target_bytes): 471 | modified_content = content.replace(b'\x20', b'\x00') 472 | 473 | with open(output_file_path, "wb") as modified_file: 474 | modified_file.write(modified_content) 475 | print("Modification successful.") 476 | else: 477 | print("Target bytes not found, no modification needed.") 478 | 479 | def reverse_four_bytes(data): 480 | reversed_four_bytes = data[:4][::-1] 481 | return reversed_four_bytes 482 | 483 | def reverse_three_bytes(data): 484 | reversed_three_bytes = data[:3][::-1] 485 | reversed_three_bytes += data[3:4] 486 | return reversed_three_bytes 487 | 488 | def create_r3dst(image_path): 489 | with open(f"{image_path}_converted.r3dst", "wb+") as f: 490 | with open(image_path, "rb") as file: 491 | file.seek(0x20, 1) 492 | for i in range(0x01, 0x4001): 493 | data = file.read(0x04) 494 | data = reverse_four_bytes(data) 495 | data = reverse_three_bytes(data) 496 | 497 | f.write(data) 498 | file.seek(0x04 * i) 499 | 500 | def extract_head(image_path, output_path): 501 | offset = 0x20 502 | with open(image_path, "rb") as f, open(output_path, "wb+") as outpf: 503 | header = f.read(offset) 504 | f.seek(0x3020) 505 | data = f.read(0x4020-0x3020) 506 | 507 | outpf.write(header) 508 | outpf.seek(offset) 509 | outpf.write(data) 510 | exit(1) 511 | 512 | def revert_options(file_path,output_file_path): 513 | # Cracko298 and Wolfyxon 514 | target_bytes = bytes([0xD8, 0x05, 0x00, 0x00, 0x6D, 0x70]) 515 | with open(file_path, "rb") as file: 516 | content = file.read() 517 | 518 | if content.startswith(target_bytes): 519 | modified_content = content.replace(b'\x00', b'\x20') 520 | 521 | with open(output_file_path, "wb") as modified_file: 522 | modified_file.write(modified_content) 523 | print("Modification successful.") 524 | else: 525 | print("Target bytes not found, no modification needed.") 526 | 527 | def image_convert(image_path): 528 | # Cracko298 529 | def extract_blocks(img): 530 | block_size = 0x100 531 | total_size = 0x4000 532 | offset = 0x20 533 | 534 | with open(img, "rb") as f: 535 | header = f.read(offset) 536 | 537 | for i in range(1, total_size // block_size + 1): 538 | with open(os.path.join("out", f"out_{i}.3dst"), "wb") as o: 539 | o.write(header) 540 | blocks = f.read(block_size) 541 | o.write(blocks) 542 | 543 | def extract_lines(start_offset, output_folder): 544 | block_size = 0x100 545 | total_size = 0x4000 546 | for i in range(1, total_size // block_size + 1): 547 | with open("out", f"out_{i}.3dst", "rb+") as f: 548 | f.seek(start_offset) 549 | one = f.read(0x08) 550 | f.seek(start_offset + 0x10) 551 | two = f.read(0x08) 552 | f.seek(start_offset + 0x40) 553 | three = f.read(0x08) 554 | f.seek(start_offset + 0x50) 555 | four = f.read(0x08) 556 | 557 | with open(os.path.join("out", "lines", f"{output_folder}_out_{i}.3dst"), "wb+") as o: 558 | o.write(one) 559 | o.write(two) 560 | o.write(three) 561 | o.write(four) 562 | 563 | def sort_and_concatenate_binary_files(input_directory, output_directory): 564 | files_dict = {} 565 | 566 | for filename in os.listdir(input_directory): 567 | if filename.endswith(".3dst"): 568 | last_number = int(filename.split('_')[-1].split('.')[0]) 569 | 570 | files_dict.setdefault(last_number, []).append(filename) 571 | 572 | os.makedirs(output_directory, exist_ok=True) 573 | 574 | for last_number in sorted(files_dict.keys()): 575 | with open(os.path.join(output_directory, f"compiled_lines_{last_number}.3dst"), "wb") as output_file: 576 | for filename in sorted(files_dict[last_number]): 577 | input_file_path = os.path.join(input_directory, filename) 578 | with open(input_file_path, "rb") as input_file: 579 | output_file.write(input_file.read()) 580 | 581 | 582 | os.makedirs("out", exist_ok=True) 583 | os.makedirs(os.path.join("out", "lines"), exist_ok=True) 584 | os.makedirs(os.path.join("out", "compiled_lines"), exist_ok=True) 585 | 586 | extract_blocks(image_path) 587 | extract_lines(0x20, "1") 588 | extract_lines(0x28, "2") 589 | extract_lines(0x40, "3") 590 | extract_lines(0x48, "4") 591 | extract_lines(0xA0, "5") 592 | extract_lines(0xA8, "6") 593 | extract_lines(0xC0, "7") 594 | extract_lines(0xC8, "8") 595 | sort_and_concatenate_binary_files(os.path.join("out", "lines"), os.path.join("out", "compiled_lines")) 596 | 597 | def copy_lines(filename, line_number, mode=1): 598 | # YT-Toaster 599 | line_number -= 1 600 | try: 601 | with open(filename, "rb") as file: 602 | lines = file.readlines() 603 | 604 | if 0 <= line_number < len(lines): 605 | if mode == 0: 606 | lines_to_copy = lines[line_number:] 607 | elif mode == 1: 608 | lines_to_copy = [lines[line_number]] 609 | elif mode == 2: 610 | lines_to_copy = [lines[line_number]] 611 | elif mode == 3: 612 | line = lines[line_number] 613 | hash_value = hashlib.sha256(line).hexdigest() 614 | return f"SHA256 Hash of Line {line_number + 1}: {hash_value}" 615 | 616 | new_filename = f"{filename}_copied.bin" 617 | with open(new_filename, "wb") as new_file: 618 | for line_to_copy in lines_to_copy: 619 | new_file.write(line_to_copy) 620 | 621 | return f"Text(s) copied to {new_filename}" 622 | else: 623 | return "Invalid line number" 624 | 625 | except FileNotFoundError: 626 | return f"File '{filename}' not found" 627 | 628 | def console2bedrock_cdb(folder_path, truncate_offset=0x84): 629 | # Cracko298 630 | make_mcworld_struct.make_dirs(make_mcworld_struct) 631 | 632 | file_prefix = "slt" 633 | output_file_name = "000001.ldb" 634 | 635 | def extract_number(file_name): 636 | try: 637 | return int(file_name[len(file_prefix):file_name.index(".cdb")]) 638 | except ValueError: 639 | return 0 640 | 641 | files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))] 642 | 643 | filtered_files = [(file, extract_number(file)) for file in files if file.startswith(file_prefix)] 644 | 645 | if filtered_files: 646 | max_file = max(filtered_files, key=lambda x: x[1]) 647 | max_file_name, max_file_number = max_file 648 | 649 | with open(os.path.join(folder_path, max_file_name), "rb+") as file: 650 | file_content = file.read() 651 | file.seek(truncate_offset) 652 | truncated_content = file.read() 653 | 654 | with open(os.path.join("mcworld_files", "db", output_file_name), "wb") as new_file: 655 | new_file.write(truncated_content) 656 | 657 | return f"Converted most Recent Slot to: '{output_file_name}'." 658 | else: 659 | return "No files found with the specified format." 660 | 661 | def console2bedrock_vdb(folder_path): 662 | # Cracko298 663 | make_mcworld_struct.make_dirs(make_mcworld_struct) 664 | 665 | offset=0x8014 666 | file_prefix="slt" 667 | output_file_name="000002.log" 668 | 669 | if not os.path.exists(folder_path): 670 | return f"Error: '{folder_path}' is not a valid path." 671 | 672 | def extract_number(file_name): 673 | try: 674 | return int(file_name[len(file_prefix):file_name.index(".vdb")]) 675 | except ValueError: 676 | return 0 677 | 678 | vdb_files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f)) and f.startswith(file_prefix)] 679 | 680 | if vdb_files: 681 | filtered_files = [(file, extract_number(file)) for file in vdb_files] 682 | max_file = max(filtered_files, key=lambda x: x[1]) 683 | max_file_name, max_file_number = max_file 684 | 685 | with open(os.path.join(folder_path, max_file_name), "rb") as file: 686 | content_at_offset = file.read() 687 | file.seek(offset) 688 | check_file = file.read(0x01) 689 | 690 | with open(os.path.join("mcworld_files", "db", output_file_name), "wb") as new_file: 691 | new_file.write(content_at_offset) 692 | 693 | return f"Converted most recent VDB file to: '{output_file_name}'." 694 | else: 695 | return "No VDB files found with the specified format." 696 | 697 | def console2bedrock_meta(level_dat=0, level_dat_old=0, levelname_txt=0, world_icon=None): 698 | # Cracko298 699 | make_mcworld_struct.make_dirs(make_mcworld_struct) 700 | checksum = 0x00 701 | 702 | if world_icon == None: 703 | checksum += 0x7A 704 | 705 | shutil.copy2(level_dat, "mcworld_files") 706 | shutil.copy2(level_dat_old, "mcworld_files") 707 | shutil.copy2(levelname_txt, "mcworld_files") 708 | 709 | if checksum >= 0x50: 710 | pass 711 | else: 712 | shutil.copy2(world_icon, "mcworld_files") 713 | 714 | def convert_lockage(file_path): 715 | # Cracko298 716 | make_mcworld_struct.make_dirs(make_mcworld_struct) 717 | with open(file_path, "rb+") as f0: 718 | lockage_data = f0.read() 719 | 720 | with open(os.path.join("mcworld_files", "db", "MANIFEST-000001"), "wb+") as f1: 721 | f1.write(lockage_data) 722 | 723 | with open(os.path.join("mcworld_files", "db", "CURRENT"), "w+") as f2: 724 | f2.write("MANIFEST-000001\n") 725 | 726 | f3 = open(os.path.join("mcworld_files", "db", "LOCK"), "wb+") 727 | f4 = open(os.path.join("mcworld_files", "db", "LOG"), "wb+") 728 | f3.close() 729 | f4.close() 730 | 731 | def zip_convert_contents(folder_path): 732 | # Cracko298 733 | zip_name = os.path.basename(folder_path) 734 | 735 | with zipfile.ZipFile(zip_name, "w", zipfile.ZIP_DEFLATED) as zipf: 736 | for root, dirs, files in os.walk("mcworld_files"): 737 | for file in files: 738 | zipf.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), "mcworld_files")) 739 | 740 | zip_name = zip_name.replace(".zip", "") 741 | 742 | os.rename(zip_name, f"{zip_name}_Converted.mcworld") 743 | 744 | def convert_save(folder_path, world_icon_path=None): 745 | # YT-Toaster & Cracko298 746 | make_mcworld_struct.make_dirs(make_mcworld_struct) 747 | 748 | toast0 = os.path.exists(os.path.join(folder_path, "db", "vdb")) 749 | toast1 = os.path.exists(os.path.join(folder_path, "db", "cdb")) 750 | 751 | if toast0 == True and toast1 == True: 752 | print("Valid Path Recieved.") 753 | else: 754 | return f"Path Recieved is Not Valid 3DS World Save.\n\nPath Provided: '{folder_path}'." 755 | 756 | console2bedrock_cdb(os.path.join(folder_path, "db", "cdb")) 757 | console2bedrock_vdb(os.path.join(folder_path, "db", "vdb")) 758 | 759 | chk0, chk1, chk2 = (".png", ".jpeg", ".jpg") 760 | 761 | if chk0 in folder_path or chk1 in folder_path or chk2 in folder_path: 762 | png_check = True 763 | else: 764 | png_check = False 765 | 766 | if world_icon_path == None: 767 | console2bedrock_meta(os.path.join(folder_path, "level.dat"), os.path.join(folder_path, "level.dat_old"), os.path.join(folder_path, "levelname.txt")) 768 | elif world_icon_path and png_check == True: 769 | console2bedrock_meta(os.path.join(folder_path, "level.dat"), os.path.join(folder_path, "level.dat_old"), os.path.join(folder_path, "levelname.txt"), world_icon_path) 770 | 771 | convert_lockage(os.path.join(folder_path, "db", "vdb", "newindex.vdb")) 772 | zip_convert_contents(folder_path) 773 | 774 | def get_png_demesions(png_path: str): 775 | with Image.open(png_path) as image: 776 | width,height = image.size 777 | return width, height 778 | 779 | def get_3dst_demensions(etc2_path: str): 780 | with open(etc2_path, "rb+") as of: 781 | of.seek(0x0C) 782 | width_b = of.read(0x04) 783 | of.seek(0x10) 784 | height_b = of.read(0x04) 785 | 786 | width = int.from_bytes(width_b, byteorder='little') 787 | height = int.from_bytes(height_b, byteorder='little') 788 | of.close() 789 | return width, height 790 | 791 | def convert_2_img(etc2_file_path: str,show_flag=False): 792 | outname = os.path.basename(etc2_file_path) 793 | extension = os.path.splitext(etc2_file_path)[1] 794 | outname = outname.replace(extension,'.png') 795 | 796 | width, height = get_3dst_demensions(etc2_file_path) 797 | 798 | with open(etc2_file_path, "rb") as f: 799 | f.seek(0x20) 800 | etc2_data = f.read() 801 | 802 | image = Image.new('RGBA', (width, height)) 803 | block_size = 8 804 | offset = 0 805 | for y in range(0, height, block_size): 806 | for x in range(0, width, block_size): 807 | for block_y in range(block_size): 808 | for block_x in range(block_size): 809 | if offset + 4 <= len(etc2_data): 810 | a, b, g, r = struct.unpack_from('BBBB', etc2_data, offset) 811 | if a == 0: 812 | r,g,b = 0,0,0 813 | image.putpixel((x + block_x, y + block_y), (r, g, b, a)) 814 | offset += 4 815 | 816 | if show_flag == True: 817 | image.show() 818 | 819 | image.save(outname) 820 | return image 821 | 822 | def convert_2_etc2(png_file_path: str): 823 | outname = os.path.basename(png_file_path) 824 | extension = os.path.splitext(png_file_path)[1] 825 | outname = outname.replace(extension,'.3dst') 826 | 827 | width,height = get_png_demesions(png_file_path) 828 | w = width.to_bytes(4, byteorder='little') 829 | h = height.to_bytes(4, byteorder='little') 830 | 831 | with Image.open(png_file_path) as image: 832 | if image.mode != 'RGBA': 833 | image = image.convert('RGBA') 834 | 835 | width, height = image.size 836 | etc2_data = bytearray() 837 | for y in range(0, height, 8): 838 | for x in range(0, width, 8): 839 | block_data = b'' 840 | for block_y in range(8): 841 | for block_x in range(8): 842 | pixel = image.getpixel((x + block_x, y + block_y)) 843 | r, g, b, a = pixel 844 | block_data += struct.pack('BBBB', a, b, g, r) 845 | 846 | etc2_data += block_data 847 | 848 | with open(outname, "wb+") as f: 849 | f.write(b'3DST\x03\x00\x00\x00\x00\x00\x00\x00'),f.write(w),f.write(h),f.write(w),f.write(h),f.write(b'\x01\x00\x00\x00') 850 | f.write(etc2_data) 851 | 852 | return bytes(etc2_data) 853 | -------------------------------------------------------------------------------- /mc3dslib_updater.py: -------------------------------------------------------------------------------- 1 | import os, site, requests 2 | from time import sleep 3 | 4 | def get_python_site_packages_dir(): 5 | return site.getsitepackages()[0] 6 | 7 | import requests 8 | 9 | def download_latest_release(repo_owner, repo_name, file_name, save_path): 10 | release_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" 11 | response = requests.get(release_url) 12 | release_info = response.json() 13 | print("Getting Latest mc3dslib Version...") 14 | 15 | for asset in release_info['assets']: 16 | if asset['name'] == file_name: 17 | asset_url = asset['browser_download_url'] 18 | print(f"Most Recent Version: {asset_url}") 19 | break 20 | else: 21 | raise ValueError(f"No asset named '{file_name}' found in the latest release.") 22 | 23 | response = requests.get(asset_url) 24 | print("Downloading Library...") 25 | with open(save_path, "wb") as f: 26 | f.write(response.content) 27 | 28 | if __name__ == "__main__": 29 | python_site_packages_dir = get_python_site_packages_dir() 30 | if python_site_packages_dir == None: 31 | exit(1) 32 | 33 | mc3ds_install_dir = os.path.join(python_site_packages_dir, "mc3dslib") 34 | chk0 = os.path.exists(os.path.join(mc3ds_install_dir, "__init__.py")) 35 | 36 | if chk0 == True: 37 | os.remove(os.path.join(mc3ds_install_dir, "__init__.py")) 38 | 39 | os.makedirs(mc3ds_install_dir, exist_ok=True) 40 | 41 | download_latest_release("Cracko298", "mc3dslib", "mc3dslib.py", os.path.join(mc3ds_install_dir, "__init__.py")) 42 | 43 | if chk0 == True: 44 | print("\nUpdated mc3dslib Successfully.") 45 | else: 46 | print("\nInstalled mc3dslib Successfully.") 47 | 48 | sleep(5) 49 | --------------------------------------------------------------------------------