├── 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 |
--------------------------------------------------------------------------------