Skip to content

Commit fefd9b7

Browse files
authored
Add files via upload
1 parent 3b5f3d0 commit fefd9b7

File tree

1 file changed

+245
-0
lines changed

1 file changed

+245
-0
lines changed

docs/index.html

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6+
<title>Halo Content Converter</title>
7+
<style>
8+
body {
9+
font-family: system-ui, sans-serif;
10+
padding: 2em;
11+
background-color: #121212;
12+
color: #e0e0e0;
13+
}
14+
h1, h2 {
15+
color: #ffffff;
16+
}
17+
.drop-zone {
18+
border: 2px dashed #555;
19+
padding: 2em;
20+
text-align: center;
21+
color: #ccc;
22+
margin-bottom: 1em;
23+
background-color: #1e1e1e;
24+
}
25+
.output {
26+
white-space: pre-wrap;
27+
background: #1e1e1e;
28+
padding: 1em;
29+
margin-top: 1em;
30+
max-height: 300px;
31+
overflow-y: auto;
32+
border: 1px solid #333;
33+
border-radius: 6px;
34+
}
35+
button {
36+
margin-top: 1em;
37+
padding: 0.6em 1.2em;
38+
font-size: 1em;
39+
border: none;
40+
border-radius: 4px;
41+
background-color: #2d89ef;
42+
color: white;
43+
cursor: pointer;
44+
}
45+
button:disabled {
46+
background-color: #444;
47+
cursor: not-allowed;
48+
}
49+
input[type="file"] {
50+
color: #ccc;
51+
}
52+
.faq {
53+
margin-top: 3em;
54+
background-color: #1e1e1e;
55+
padding: 1.5em;
56+
border-radius: 6px;
57+
border: 1px solid #333;
58+
}
59+
.faq h2 {
60+
margin-top: 0;
61+
}
62+
.faq-item {
63+
margin-bottom: 1.5em;
64+
}
65+
.faq-item h3 {
66+
margin-bottom: 0.2em;
67+
color: #89dceb;
68+
}
69+
.faq-item p {
70+
margin: 0;
71+
color: #ccc;
72+
}
73+
</style>
74+
</head>
75+
<body>
76+
<h1>Halo Content Converter</h1>
77+
<div class="drop-zone" id="drop-zone">
78+
Drag & drop files here or <input type="file" multiple id="file-input" />
79+
</div>
80+
<button id="convert-btn" disabled>Convert Files</button>
81+
<div class="output" id="output"></div>
82+
83+
<!-- FAQ Section -->
84+
<div class="faq" id="faq">
85+
<h2>FAQ</h2>
86+
<div class="faq-item">
87+
<h3>Where can I find my Halo Xbox 360 container files?</h3>
88+
<p>After copying all the files you want to convert to a USB:</p>
89+
<p> - Halo 3: "Content/E0000.../4D5307E6/00000001"</p>
90+
<p> - Halo 3 ODST: "Content/E0000.../4D530877/00000001"</p>
91+
<p> - Halo Reach: "Content/E0000.../4D53085B/00000001"</p>
92+
<p> - Halo 4: "Content/E0000.../4D530919/00000001"</p>
93+
<p>The files within those folders are the 'container' files!</p>
94+
</div>
95+
<div class="faq-item">
96+
<h3>How do I get these into MCC?</h3>
97+
<p>Drop the .bin files into the 'game_variants' folder & .mvar files into the 'map_variants' folder for their respective games.</p>
98+
</div>
99+
<div class="faq-item">
100+
<h3>Is my file data stored or sent anywhere?</h3>
101+
<p>All file processing happens locally in the browser. No server interaction.</p>
102+
</div>
103+
</div>
104+
105+
<script>
106+
const fileInput = document.getElementById("file-input");
107+
const dropZone = document.getElementById("drop-zone");
108+
const output = document.getElementById("output");
109+
const convertBtn = document.getElementById("convert-btn");
110+
111+
let selectedFiles = [];
112+
113+
const getString = (bytes, offset, length) => {
114+
return new TextDecoder("ascii").decode(bytes.slice(offset, offset + length)).replace(/\0/g, '').trim();
115+
};
116+
117+
const findIndexOfString = (bytes, str, start = 0) => {
118+
const target = new TextEncoder().encode(str);
119+
for (let i = start; i < bytes.length - target.length; i++) {
120+
let match = true;
121+
for (let j = 0; j < target.length; j++) {
122+
if (bytes[i + j] !== target[j]) {
123+
match = false;
124+
break;
125+
}
126+
}
127+
if (match) return i;
128+
}
129+
return -1;
130+
};
131+
132+
const findIndexOfBytes = (bytes, pattern, start = 0) => {
133+
for (let i = start; i <= bytes.length - pattern.length; i++) {
134+
let match = true;
135+
for (let j = 0; j < pattern.length; j++) {
136+
if (bytes[i + j] !== pattern[j]) {
137+
match = false;
138+
break;
139+
}
140+
}
141+
if (match) return i;
142+
}
143+
return -1;
144+
};
145+
146+
const sanitizeFilename = (name) => {
147+
return name.replace(/[\\/:*?"<>|]/g, "_");
148+
};
149+
150+
const triggerDownload = (blob, filename) => {
151+
const a = document.createElement("a");
152+
const url = URL.createObjectURL(blob);
153+
a.href = url;
154+
a.download = filename;
155+
document.body.appendChild(a);
156+
a.click();
157+
document.body.removeChild(a);
158+
URL.revokeObjectURL(url);
159+
};
160+
161+
const processFile = async (file) => {
162+
try {
163+
const buffer = await file.arrayBuffer();
164+
const bytes = new Uint8Array(buffer);
165+
166+
const fileContent = new TextDecoder("ascii").decode(bytes);
167+
const blfIndex = fileContent.indexOf("_blf");
168+
if (blfIndex === -1) return;
169+
170+
const content = getString(bytes, 0xD0C0, 0x7F);
171+
const sanitizedContent = sanitizeFilename(content);
172+
const creatorName = getString(bytes, 0xD088, 0x0F);
173+
const creatorXUID = Array.from(bytes.slice(0xD080, 0xD088)).map(b => b.toString(16).padStart(2, '0')).join('');
174+
const modifierName = getString(bytes, 0xD0AC, 0x0F);
175+
const modifierXUID = Array.from(bytes.slice(0xD0A4, 0xD0AC)).map(b => b.toString(16).padStart(2, '0')).join('');
176+
const description = getString(bytes, 0xD1C0, 0xFF);
177+
const headerType = getString(bytes, 0xD2F0, 0x04);
178+
179+
const typeMap = {
180+
"mpvr": { type: "Gametypes", ext: ".bin" },
181+
"mvar": { type: "Map variants", ext: ".mvar" },
182+
"athr": { type: "Theater films", ext: ".film" },
183+
"scnc": { type: "Screenshots", ext: ".jpg" }
184+
};
185+
186+
if (!(headerType in typeMap)) return;
187+
188+
output.innerText += `\nFile: ${file.name}
189+
Creator: ${creatorName} (XUID: ${creatorXUID})
190+
Modifier: ${modifierName} (XUID: ${modifierXUID})
191+
Content: ${content}
192+
Description: ${description}
193+
Header Type: ${headerType}\n`;
194+
195+
if (headerType === "scnc") {
196+
const jpgStartMarker = [0xFF, 0xD8, 0xFF, 0xE0];
197+
const jpgEndMarker = [0xFF, 0xD9];
198+
199+
const jpgStart = findIndexOfBytes(bytes, jpgStartMarker, blfIndex);
200+
const jpgEnd = findIndexOfBytes(bytes, jpgEndMarker, jpgStart);
201+
if (jpgStart === -1 || jpgEnd === -1) return;
202+
203+
const jpgBytes = bytes.slice(jpgStart, jpgEnd + 2);
204+
const blob = new Blob([jpgBytes], { type: "image/jpeg" });
205+
triggerDownload(blob, sanitizedContent + ".jpg");
206+
return;
207+
}
208+
209+
const stopIndex = findIndexOfString(bytes, "_eof", blfIndex);
210+
if (stopIndex === -1) return;
211+
212+
const extracted = bytes.slice(blfIndex, stopIndex + 4 + 0x0D);
213+
const { ext } = typeMap[headerType];
214+
const blob = new Blob([extracted], { type: "application/octet-stream" });
215+
triggerDownload(blob, sanitizedContent + ext);
216+
} catch (e) {
217+
output.innerText += `Error processing file ${file.name}: ${e.message}\n`;
218+
}
219+
};
220+
221+
const handleFiles = (files) => {
222+
selectedFiles = Array.from(files);
223+
output.innerText = `${selectedFiles.length} file(s) ready to convert.\nClick "Convert Files" to proceed.`;
224+
convertBtn.disabled = false;
225+
};
226+
227+
fileInput.addEventListener("change", () => handleFiles(fileInput.files));
228+
dropZone.addEventListener("dragover", e => e.preventDefault());
229+
dropZone.addEventListener("drop", e => {
230+
e.preventDefault();
231+
handleFiles(e.dataTransfer.files);
232+
});
233+
234+
convertBtn.addEventListener("click", async () => {
235+
output.innerText += "\nProcessing files...\n";
236+
for (const file of selectedFiles) {
237+
await processFile(file);
238+
await new Promise(resolve => setTimeout(resolve, 150));
239+
}
240+
output.innerText += "\nAll files processed.";
241+
convertBtn.disabled = true;
242+
});
243+
</script>
244+
</body>
245+
</html>

0 commit comments

Comments
 (0)