|
2 | 2 | <html lang="en"> |
3 | 3 | <head> |
4 | 4 | <meta charset="UTF-8"> |
5 | | -<title>Godtool (Web) • v1.0</title> |
| 5 | +<title>Godtool (Web) • v1.1</title> |
6 | 6 | <style> |
7 | 7 | :root { |
8 | 8 | --bg-main: #101014; |
|
146 | 146 | width: 70px; |
147 | 147 | } |
148 | 148 | .dwc-field, |
| 149 | + .dwc-salt-field, |
149 | 150 | .fileinfo-field, |
150 | 151 | .friends-field, |
151 | 152 | .penalty-field, |
|
796 | 797 | <body> |
797 | 798 | <header> |
798 | 799 | <div class="row"> |
799 | | - <span class="logo">Godtool (Web) • v1.0</span> |
| 800 | + <span class="logo">Godtool (Web) • v1.1</span> |
800 | 801 | <input type="file" id="fileInput" accept=".dat,.ikw,.rkp"> |
801 | 802 | <button id="downloadBtn" disabled>Download DAT</button> |
802 | 803 | <span id="status">No file loaded</span> |
|
2433 | 2434 | return false; |
2434 | 2435 | } |
2435 | 2436 | if (choice === "apply" && typeof currentDirtyTab.applyFn === "function") { |
2436 | | - currentDirtyTab.applyFn(); |
| 2437 | + const res = currentDirtyTab.applyFn(); |
| 2438 | + if (res && typeof res.then === "function") { |
| 2439 | + await res; |
| 2440 | + } |
| 2441 | + if (currentDirtyTab && currentDirtyTab.dirty) { |
| 2442 | + return false; |
| 2443 | + } |
2437 | 2444 | } else if (choice === "discard") { |
2438 | 2445 | clearDirtyTab(true); |
2439 | 2446 | } |
|
3838 | 3845 | } |
3839 | 3846 |
|
3840 | 3847 |
|
| 3848 | +function formatGameIdSalt(bytes) { |
| 3849 | + if (!bytes || bytes.length < 4) return "????"; |
| 3850 | + let allZero = true; |
| 3851 | + let printable = true; |
| 3852 | + let ascii = ""; |
| 3853 | + for (let i = 0; i < 4; i++) { |
| 3854 | + const b = bytes[i]; |
| 3855 | + if (b !== 0) allZero = false; |
| 3856 | + if (b < 0x20 || b > 0x7E) printable = false; |
| 3857 | + ascii += String.fromCharCode(b); |
| 3858 | + } |
| 3859 | + if (allZero) return "0000"; |
| 3860 | + if (printable) return ascii; |
| 3861 | + let hex = ""; |
| 3862 | + for (let i = 0; i < 4; i++) { |
| 3863 | + hex += bytes[i].toString(16).padStart(2, "0"); |
| 3864 | + } |
| 3865 | + return hex.toUpperCase(); |
| 3866 | +} |
| 3867 | + |
| 3868 | +function parseGameIdSaltInput(raw) { |
| 3869 | + const s = String(raw || "").trim(); |
| 3870 | + if (!s) return null; |
| 3871 | + if (s === "0000") return new Uint8Array([0, 0, 0, 0]); |
| 3872 | + if (/^0x[0-9a-fA-F]{8}$/.test(s)) { |
| 3873 | + const hex = s.slice(2); |
| 3874 | + const out = new Uint8Array(4); |
| 3875 | + for (let i = 0; i < 4; i++) { |
| 3876 | + out[i] = parseInt(hex.substr(i * 2, 2), 16) & 0xFF; |
| 3877 | + } |
| 3878 | + return out; |
| 3879 | + } |
| 3880 | + if (/^[0-9a-fA-F]{8}$/.test(s)) { |
| 3881 | + const out = new Uint8Array(4); |
| 3882 | + for (let i = 0; i < 4; i++) { |
| 3883 | + out[i] = parseInt(s.substr(i * 2, 2), 16) & 0xFF; |
| 3884 | + } |
| 3885 | + return out; |
| 3886 | + } |
| 3887 | + if (s.length === 4) { |
| 3888 | + const out = new Uint8Array(4); |
| 3889 | + for (let i = 0; i < 4; i++) { |
| 3890 | + const code = s.charCodeAt(i); |
| 3891 | + if (code < 0x20 || code > 0x7E) return null; |
| 3892 | + out[i] = code & 0xFF; |
| 3893 | + } |
| 3894 | + return out; |
| 3895 | + } |
| 3896 | + return null; |
| 3897 | +} |
| 3898 | + |
| 3899 | +function gameIdSaltFormatLabel(raw) { |
| 3900 | + const s = String(raw || "").trim(); |
| 3901 | + if (!s) return ""; |
| 3902 | + if (/^0x[0-9a-fA-F]{8}$/.test(s) || /^[0-9a-fA-F]{8}$/.test(s) || s === "0000") { |
| 3903 | + return "(Hex)"; |
| 3904 | + } |
| 3905 | + if (s.length === 4) { |
| 3906 | + let printable = true; |
| 3907 | + for (let i = 0; i < 4; i++) { |
| 3908 | + const code = s.charCodeAt(i); |
| 3909 | + if (code < 0x20 || code > 0x7E) { |
| 3910 | + printable = false; |
| 3911 | + break; |
| 3912 | + } |
| 3913 | + } |
| 3914 | + if (printable) return "(ASCII)"; |
| 3915 | + } |
| 3916 | + const parsed = parseGameIdSaltInput(s); |
| 3917 | + if (parsed && s.length === 4) return "(ASCII)"; |
| 3918 | + if (parsed) return "(Hex)"; |
| 3919 | + return ""; |
| 3920 | +} |
| 3921 | + |
3841 | 3922 | function renderDwcTab(lic) { |
3842 | 3923 | const base = licBase(lic) + DWC_OFFSET; |
| 3924 | + const gameIdBytes = content.slice(base + 0x24, base + 0x28); |
| 3925 | + const gameIdText = formatGameIdSalt(gameIdBytes); |
| 3926 | + const gameIdFormat = gameIdSaltFormatLabel(gameIdText); |
3843 | 3927 | let html = `<div class="group-box"> |
3844 | 3928 | <div class="group-title">DWC User Data</div> |
3845 | 3929 | <div class="grid-2col">`; |
| 3930 | + html += ` |
| 3931 | + <label>Game ID salt</label> |
| 3932 | + <span class="dwc-input-wrap" id="dwcGameIdSaltWrap"> |
| 3933 | + <input type="text" class="small-input dwc-salt-field" id="dwcGameIdSalt" value="${gameIdText}" maxlength="8"> |
| 3934 | + <span id="dwcGameIdSaltFormat" style="font-size:11px; color: var(--text-muted);">${gameIdFormat}</span> |
| 3935 | + </span> |
| 3936 | + `; |
3846 | 3937 | for (const [label, off] of DWC_FIELDS) { |
3847 | 3938 | const val = view.getUint32(base + off, false) >>> 0; |
3848 | 3939 | html += ` |
|
3892 | 3983 | markDwcWarnings(lic); |
3893 | 3984 | }); |
3894 | 3985 | }); |
| 3986 | + const gameIdInput = document.getElementById("dwcGameIdSalt"); |
| 3987 | + if (gameIdInput) { |
| 3988 | + gameIdInput.addEventListener("input", () => { |
| 3989 | + const fmtEl = document.getElementById("dwcGameIdSaltFormat"); |
| 3990 | + if (fmtEl) fmtEl.textContent = gameIdSaltFormatLabel(gameIdInput.value); |
| 3991 | + markDirtyNow(); |
| 3992 | + markDwcWarnings(lic); |
| 3993 | + }); |
| 3994 | + } |
3895 | 3995 |
|
3896 | 3996 | markDwcWarnings(lic); |
3897 | 3997 |
|
|
4021 | 4121 | document.getElementById("dwcApplyBtn").onclick = () => { |
4022 | 4122 | if (!ensureLoaded()) return; |
4023 | 4123 | const inputs = tabContent.querySelectorAll(".dwc-field"); |
| 4124 | + const gameIdInput = document.getElementById("dwcGameIdSalt"); |
| 4125 | + let gameIdBytes = null; |
| 4126 | + if (gameIdInput) { |
| 4127 | + gameIdBytes = parseGameIdSaltInput(gameIdInput.value); |
| 4128 | + if (!gameIdBytes) { |
| 4129 | + alert("Game ID salt must be 4 ASCII characters or 8 hex digits."); |
| 4130 | + return; |
| 4131 | + } |
| 4132 | + } |
4024 | 4133 |
|
4025 | 4134 | // First pass: validate all |
4026 | 4135 | for (const inp of inputs) { |
|
4047 | 4156 | const v = res.value >>> 0; |
4048 | 4157 | view.setUint32(base + off, v, false); |
4049 | 4158 | } |
| 4159 | + if (gameIdBytes) { |
| 4160 | + content.set(gameIdBytes, base + 0x24); |
| 4161 | + } |
4050 | 4162 |
|
4051 | 4163 | const crc = reversedWordsCRC32(base, DWC_DATA_LEN); |
4052 | 4164 | view.setUint32(base + DWC_DATA_LEN, crc >>> 0, false); |
|
4213 | 4325 |
|
4214 | 4326 | const currentEmpty = isDwcEmptyLicense(currentLic); |
4215 | 4327 |
|
| 4328 | + if (!currentEmpty) { |
| 4329 | + const gameIdWrap = document.getElementById("dwcGameIdSaltWrap"); |
| 4330 | + let gameIdBytes = null; |
| 4331 | + const gameIdInput = document.getElementById("dwcGameIdSalt"); |
| 4332 | + if (gameIdInput) { |
| 4333 | + const parsed = parseGameIdSaltInput(gameIdInput.value); |
| 4334 | + gameIdBytes = parsed ? parsed : new Uint8Array(0); |
| 4335 | + } else { |
| 4336 | + gameIdBytes = content.slice( |
| 4337 | + licBase(currentLic) + DWC_OFFSET + 0x24, |
| 4338 | + licBase(currentLic) + DWC_OFFSET + 0x28 |
| 4339 | + ); |
| 4340 | + } |
| 4341 | + const isRmcj = |
| 4342 | + gameIdBytes.length === 4 && |
| 4343 | + gameIdBytes[0] === 0x52 && |
| 4344 | + gameIdBytes[1] === 0x4D && |
| 4345 | + gameIdBytes[2] === 0x43 && |
| 4346 | + gameIdBytes[3] === 0x4A; |
| 4347 | + if (!isRmcj) { |
| 4348 | + addDwcWarning( |
| 4349 | + gameIdWrap, |
| 4350 | + "warn-red", |
| 4351 | + "The game salt for Mario Kart Wii's friend code calculation is RMCJ. Other values indicate that the save file has been tampered with and friend codes may not work or display properly." |
| 4352 | + ); |
| 4353 | + } |
| 4354 | + } |
| 4355 | + |
4216 | 4356 | // Authentic User ID #2 across all licenses (ignore empty licenses) |
4217 | 4357 | const auth2Values = []; |
4218 | 4358 | for (let lic = 0; lic < 4; lic++) { |
|
0 commit comments