commit d4d82fa1d738924d270a81efd0b6948d39d48af7
Author: asterisk727 <59166650+asterisk727@users.noreply.github.com>
Date: Mon Dec 29 19:42:54 2025 +0900
chore: initial commit
diff --git a/.eslintrc.yml b/.eslintrc.yml
new file mode 100644
index 0000000..8039f6e
--- /dev/null
+++ b/.eslintrc.yml
@@ -0,0 +1,9 @@
+plugins:
+ - html
+env:
+ browser: true
+ es2021: true
+overrides: []
+parserOptions:
+ ecmaVersion: latest
+rules: {}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..dfd0e30
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+# Set update schedule for GitHub Actions
+
+version: 2
+updates:
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ # Check for updates to GitHub Actions every week
+ interval: "weekly"
diff --git a/.github/html5validator.yml b/.github/html5validator.yml
new file mode 100644
index 0000000..9108940
--- /dev/null
+++ b/.github/html5validator.yml
@@ -0,0 +1,5 @@
+root: ./
+ignore_re:
+ - 'An "img" element must have an "alt" attribute, except under certain conditions'
+ - 'Element "div" not allowed as child of element "label" in this context'
+ - 'The "center" element is obsolete. Use CSS instead' # initialdzero
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
new file mode 100644
index 0000000..c498f2a
--- /dev/null
+++ b/.github/workflows/check.yml
@@ -0,0 +1,20 @@
+name: Check HTML/JS is valid
+
+on:
+ pull_request:
+ push:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - name: Run HTML5 Validator
+ uses: Cyb3r-Jak3/html5validator-action@v7.2.0
+ with:
+ config: ./.github/html5validator.yml
+ css: true
+ - name: Install modules for eslint
+ run: npm i
+ - name: Run ESLint
+ run: npx eslint . --ext .js,.html
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e50dbd6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+package-lock.json
+/.vs
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..1330df7
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,5 @@
+# Over time, some patchers have changed path due to reorganisation and cleanup.
+# This avoids link rot on mon's server by redirecting the old paths to the new ones
+
+Redirect permanent "/bemanipatcher/chuninewplus.html" "/bemanipatcher/chusannewplus.html"
+Redirect permanent "/bemanipatcher/resort_anthem.html" "/bemanipatcher/resortanthem.html"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..56da3ba
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024-202X two-torial Team & contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d32de0d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,32 @@
+# [TWO-TORIAL](https://two-torial.xyz)'s Web Patcher
+
+### URL: [https://patcher.two-torial.xyz/](https://patcher.two-torial.xyz/)
+
+## About
+
+This repo contains web patches for games incompatible with [spice2x](https://spice2x.github.io/). See our [spice2x patcher](https://github.com/two-torial/sp2xpatcher/) for those.
+We'll be keeping the **repo public** and **url alive** for as long as possible, **feel free to [contibute](#Contributing)** for the community through pull requests!
+
+## Usage
+
+See our guide on [web patching](https://two-torial.xyz/extras/patchweb/) and use the url provided above.
+
+## Contributing
+
+### A new game
+
+- Add the new game html, it is easiest to copy an existing game and modify it.
+- Modify the `
` tag and the `
` tag to the name of the new game.
+- Modify the patcher for the new DLL names/patches.
+- Keep indentation consistent. We will have to fix your PR if it contains poor formatting, which will delay the merging process.
+- Modify `index.html` to add the new game. Sorting: alphabetical by game series, then in release order per game.
+- Add a game image. 128x128px PNG files, please. Any blank space should be either white or transparent.
+- Create a pull request.
+
+### Patches for an existing game
+
+- Open the game's html.
+- If necessary: add a new PatchContainer.
+- If necessary: add a new Patcher.
+- Add your patches following the existing format.
+- Create a pull request.
diff --git a/SDGS125.html b/SDGS125.html
new file mode 100644
index 0000000..e40b97c
--- /dev/null
+++ b/SDGS125.html
@@ -0,0 +1,193 @@
+
+
+
+
+
+ CHUNITHM Intl. SUN PLUS Modder
+
+
+
+
+
+
+
+
+
+
diff --git a/css/file.eot b/css/file.eot
new file mode 100644
index 0000000..32d36be
Binary files /dev/null and b/css/file.eot differ
diff --git a/css/file.svg b/css/file.svg
new file mode 100644
index 0000000..55f885b
--- /dev/null
+++ b/css/file.svg
@@ -0,0 +1,11 @@
+
+
+
\ No newline at end of file
diff --git a/css/file.ttf b/css/file.ttf
new file mode 100644
index 0000000..805576d
Binary files /dev/null and b/css/file.ttf differ
diff --git a/css/file.woff b/css/file.woff
new file mode 100644
index 0000000..c837467
Binary files /dev/null and b/css/file.woff differ
diff --git a/css/style.css b/css/style.css
new file mode 100644
index 0000000..ce3e550
--- /dev/null
+++ b/css/style.css
@@ -0,0 +1,446 @@
+@font-face {
+ font-family: "file";
+ src: url("file.eot?abyff3");
+ src: url("file.eot?abyff3#iefix") format("embedded-opentype"),
+ url("file.ttf?abyff3") format("truetype"),
+ url("file.woff?abyff3") format("woff"),
+ url("file.svg?abyff3#file") format("svg");
+ font-weight: normal;
+ font-style: normal;
+}
+.tagline {
+ font-family: monospace;
+}
+
+.icons,
+.subsection {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, 180px);
+ grid-auto-flow: dense;
+ align-items: stretch;
+ margin: 0 auto;
+ text-align: center;
+ justify-content: center;
+}
+
+.subsection {
+ grid-column: 1 / -1;
+ margin: unset;
+ /* I don't understand anything */
+ margin-right: -15px;
+ padding-right: 15px;
+}
+
+.gameicon,
+.patchContainer {
+ border-radius: 2px;
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
+ color: inherit;
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+ background: #fff;
+}
+
+.gameicon {
+ display: flex;
+ flex-direction: column;
+ text-decoration: none;
+ margin: 15px;
+ width: 144px;
+ padding: 10px;
+ justify-content: center;
+}
+
+label.gameicon {
+ cursor: pointer;
+}
+
+input.sectionToggle,
+input.sectionToggle + div {
+ display: none;
+}
+
+input:checked.sectionToggle + div {
+ display: grid;
+}
+
+.gameicon:hover,
+.dragover {
+ box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
+}
+
+.gameicon img {
+ margin-bottom: 10px;
+ border-radius: 2px;
+ width: 128px;
+ height: 128px;
+ box-shadow: 0 2px 1px rgba(0, 0, 0, 0.24), 0 0px 1px rgba(0, 0, 0, 0.48);
+ align-self: center;
+ margin-top: 5px;
+}
+
+.gameicon > div {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.fileInput {
+ display: none;
+}
+
+.fileLabel {
+ cursor: pointer;
+}
+
+.success {
+ text-align: center;
+ border-radius: 10px;
+}
+
+.success.hidden {
+ display: none;
+}
+
+.patchContainer {
+ padding: 20px;
+ max-width: 650px;
+ margin: 0 auto 2em;
+ position: relative;
+}
+
+.tooltip {
+ visibility: hidden;
+ font-size: 14px;
+ margin-left: 8px;
+ padding: 8px;
+ border-radius: 4px;
+ position: relative;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.tooltip:hover,
+.tooltip:focus,
+.tooltip:active {
+ visibility: visible;
+ color: white;
+ border: none;
+ margin-top: 0px;
+ position: absolute;
+ padding: 3px 16px;
+ white-space: normal;
+ max-width: 300px;
+ z-index: 11;
+}
+
+.tooltip:before {
+ visibility: visible;
+ content: "?";
+ font-size: 18px;
+ margin-right: 8px;
+ border-radius: 50%;
+ padding: 2px 9px;
+ margin-left: -8px;
+ color: white;
+ cursor: pointer;
+ z-index: 10;
+}
+
+.tooltip:hover:before,
+.tooltip:focus:before,
+.tooltip:active:before {
+ color: black;
+ display: none;
+}
+
+.danger:before {
+ content: "!!";
+}
+
+body {
+ margin: 40px auto;
+ max-width: 1300px;
+ line-height: 1.6;
+ font-size: 18px;
+ color: #000;
+ padding: 0 10px;
+ background: #24273a;
+ font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;
+}
+
+h1,
+h2,
+h3 {
+ line-height: 1.2;
+ text-align: center;
+ color: inherit;
+ text-decoration: inherit;
+}
+
+h4,
+h5 {
+ line-height: 1;
+ margin: 0px auto;
+}
+
+button,
+.fileLabel > strong {
+ transition-duration: 0.2s;
+ transition-timing-function: cubic-bezier(0.25, 0.5, 0.5, 1);
+ position: relative;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 20px;
+ font-size: 0.875rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ transition-property: box-shadow, background;
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25), 0px 0px 2px rgba(0, 0, 0, 0.125);
+ cursor: pointer;
+}
+
+.fileLabel > strong {
+ line-height: 3rem;
+}
+
+button:disabled {
+ background-color: rgba(0, 0, 0, 0.12);
+ color: rgba(0, 0, 0, 0.38);
+ box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2),
+ 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12);
+ cursor: default;
+}
+
+button:hover:enabled,
+.fileLabel > strong:hover {
+ box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.25), 0px 0px 4px rgba(0, 0, 0, 0.125);
+}
+
+.matchPercent {
+ font-size: 15px;
+ font-style: italic;
+}
+
+.matchSuccess {
+ font-size: 15px;
+ font-style: italic;
+}
+
+li > button {
+ height: 24px;
+ padding: 0 7px;
+ line-height: 0;
+}
+
+.patches {
+ margin: 1em auto;
+}
+
+input[type="checkbox"],
+input[type="radio"] {
+ vertical-align: middle;
+ position: relative;
+ bottom: 1px;
+}
+
+input[type="radio"] {
+ bottom: 2px;
+}
+
+.patches label {
+ margin-left: 4px;
+}
+
+.dragover > * {
+ filter: blur(5px);
+}
+
+.dragover::before {
+ content: "";
+ display: block;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ text-align: center;
+ z-index: 10;
+ outline: dashed 10px;
+}
+
+.dragover::after {
+ content: "\e900";
+ font-family: "file" !important;
+ font-style: normal;
+ font-weight: normal;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ /* Better Font Rendering =========== */
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ display: block;
+ height: auto;
+ position: absolute;
+ top: calc(50% - 3rem);
+ left: 0;
+ width: 100%;
+ text-align: center;
+ z-index: 11;
+ font-size: 8rem;
+}
+
+.tooltip:not(:hover) {
+ font-size: 0px;
+ padding: 0;
+ margin-left: 16px;
+}
+
+.tooltip {
+ visibility: hidden;
+ font-size: 14px;
+ margin-left: 8px;
+ padding: 8px;
+ border-radius: 4px;
+ position: relative;
+ background: gray;
+ white-space: nowrap;
+ cursor: pointer;
+ display: inline-block;
+}
+.patchPreviewLabel {
+ cursor: pointer;
+}
+.patchPreviewToggle {
+ display: none;
+}
+.patchPreview {
+ display: none;
+}
+input[type="checkbox"]:checked + .patchPreview {
+ display: block;
+}
+input[type="checkbox"] ~ ul > li.patch-off {
+ display: none;
+}
+input[type="checkbox"] ~ ul > li.patch-on {
+ display: list-item;
+}
+input[type="checkbox"]:checked ~ ul > li.patch-on {
+ display: none;
+}
+input[type="checkbox"]:checked ~ ul > li.patch-off {
+ display: list-item;
+}
+
+/* Palette: Catppuccin Macchiato */
+@media (prefers-color-scheme: dark) {
+ body {
+ background: #24273a;
+ color: #cad3f5;
+ }
+ a {
+ color: #c6a0f6;
+ }
+ .gameicon,
+ .patchContainer {
+ background-color: #181926;
+ }
+ label.gameicon {
+ background: #181926;
+ }
+ .subsection {
+ background-color: #363a4f;
+ }
+ .matchPercent,
+ .error {
+ color: #ed8796;
+ }
+ .matchSuccess {
+ color: #a6da95;
+ }
+ .success {
+ background-color: #a6da95;
+ color: #000;
+ }
+ button,
+ .fileLabel > strong {
+ background-color: #c6a0f6;
+ color: #000;
+ }
+ button:disabled {
+ background-color: #ffffff05;
+ color: #ffffff55;
+ }
+ .tooltip,
+ .tooltip:before {
+ background-color: #8087a2;
+ }
+ .danger,
+ .danger:before {
+ background-color: #f5a97f !important;
+ }
+}
+
+/* Palette: Catppuccin Latte */
+@media (prefers-color-scheme: light) {
+ body {
+ background: #eff1f5;
+ color: #4c4f69;
+ }
+ a {
+ color: #8839ef;
+ }
+ .gameicon,
+ .patchContainer {
+ background-color: #dce0e8;
+ }
+ label.gameicon {
+ background: #dce0e8;
+ }
+ .subsection {
+ background-color: #ccd0da;
+ }
+ .matchPercent,
+ .error {
+ color: #d20f39;
+ }
+ .matchSuccess {
+ color: #40a02b;
+ }
+ .success {
+ background-color: #40a02b;
+ color: #fff;
+ }
+ button,
+ .fileLabel > strong {
+ background-color: #8839ef;
+ color: #fff;
+ }
+ button:disabled {
+ background-color: rgba(0, 0, 0, 0.12);
+ color: rgba(0, 0, 0, 0.38);
+ }
+ .tooltip,
+ .tooltip:before {
+ background-color: #9ca0b0;
+ }
+ .danger,
+ .danger:before {
+ background-color: #fe640b;
+ }
+}
+.image-wrapper {
+ position: relative;
+}
+.image-hover {
+ position: absolute;
+ top: 0;
+ left: 1;
+ opacity: 0;
+ transition: opacity 0.5s ease-out;
+}
+.image-hover:hover {
+ opacity: 1;
+}
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..2c1872d
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,8 @@
+# What is this folder?
+
+Did you spend ages getting a hex edit working? Do you want to document your
+process somewhere so you don't forget?
+
+Perhaps you found a really cool way to find new timer-freeze patches for all
+games, or some audio trick that helps identify stuff. Make a .md file for your
+game or technique and put it here so the knowledge is not lost.
diff --git a/docs/chuni_offset_finding.md b/docs/chuni_offset_finding.md
new file mode 100644
index 0000000..0c308d0
--- /dev/null
+++ b/docs/chuni_offset_finding.md
@@ -0,0 +1,53 @@
+# Notes on updating offsets for Crystal Plus
+
+Windows 7 patch is missing.
+
+Steps to determine these values:
+
+1. Take old values from crystal
+1. Use IDA's "Jump" -> "Jump to file offset..." feature to convert the file addresses to virtual memory addresses
+1. Take chunithm crystal plus executable
+1. Use virtual memory addresses from crystal, same spot is likely close to old one
+1. Use some more instructions/bytes around the offsets from crystal to find same patterns on crystal plus
+1. Check if disassembly area is identical regarding the change
+1. At the bottom left of the disassembly text view, you see something like this: `00905500 | 00D06100: sub_D06100 (Synchronized with Hex View-1)`. The first address is the raw file address, the second one is the virtual address when loaded into memory
+1. Take the first address once you found the exact same spot in chunithm crystal
+
+Format below for addresses: crystal file address -> crystal virtual address -> crystal plus virtual address -> crystal plus file address
+
+```
+Allow 127.0.0.1/localhost as the network server",
+0x905500 -> 00D06100 -> 00D97F80 -> 00997380
+0x16CE988 -> 01AD0388 -> 01B17E80 -> 01717080
+
+"Disable shop close lockout",
+0x92D153 -> 00D2DD53 -> 00DBF6D3 -> 009BEAD3
+
+"Force shared audio mode, system audio samplerate must be 48000",
+0xCD470A -> 010D530A -> 010F26BA -> 00CF1ABA
+
+"Force 2 channel audio output",
+0xCD47E1 -> 010D53E1 -> 010F2791 -> 00CF1B91
+
+"Patch for Windows 7",
+0x1A404E8 -> jump failed
+0x1A404F1 -> jump failed
+
+"Disable Song Select Timer",
+0x748FE2 -> 00B49BE2 -> 00B669C2 -> 00765DC2
+
+"Set All Timers to 999",
+0x5FC300 -> 009FCF00 -> 00A14870 -> 00613C70
+
+"Better patch for head-to-head play",
+0x464DB3 -> 008659B3 -> 00879A03 -> 00478E03
+
+Increase max credits to 254",
+0xC204B7 -> 010210B7 -> 01094767 -> 00C93B67
+
+"Free Play",
+0xC208D5 -> 010214D5 -> 01094B85 -> 00C93F85
+
+"Dummy LED",
+0x2499D7 -> 0064A5D7 -> 0064A5D7 -> 002499D7
+```
diff --git a/docs/why_no_n_minus_zero.md b/docs/why_no_n_minus_zero.md
new file mode 100644
index 0000000..6889dfd
--- /dev/null
+++ b/docs/why_no_n_minus_zero.md
@@ -0,0 +1,81 @@
+# What does n-0 mean?
+"n-0" is part of a common nomenclature in the arcade scene referring to
+freshness of data. "n-0" (n minus zero) is the current game release, like a
+mathematical equation. "n-1" is the previous game release. Rarely you will see
+"n+1" where the next, unreleased version of a game is leaked early.
+
+A time can also be added to specify lengths instead of versions. For example,
+"n-6 months" or "n-1 year".
+
+# Why no n-0 in Bemanipatcher?
+
+The original policy of this repo was to accept any patch set that looked sane
+and had consistent style, to be a "global" repository for all games that had
+been released. Sometimes some private release would have hex edits appear as a
+PR, so I accepted them because I didn't want to get into the drama of "why does
+he have data but I don't".
+
+In mid 2021, some groups decided they'd start leaking the latest updates for
+pretty much every game under the sun. In addition to being a hugely
+irresponsible idea (Sega is especially litigious), this produced a lot of noise
+in PRs from people hastily trying to port edits.
+
+I have had several terrible PRs submitted that I have had to revert. These
+include:
+- Broken patches
+- Misattributed sources
+- Totally-fine patch sets where I immediately received a Discord DM saying "I
+ didn't want that to be public, delete it please"
+- PRs opened and closed almost immediately by someone reconsidering
+- PRs opened, merged and then reverted by the original uploader
+
+As such, this repo now has a new rule: No n-0, unless it is more than a year
+old.
+
+# When is n-0 OK? Why is day-old n-1 OK?
+In addition to the obvious frustration I have with all these junk PRs, read on
+for some moral soapboxing as to why you should reconsider public n-0.
+
+**Remember: this is my own opinion.** I like to think it's a fairly common one. You are welcome to disagree with me, but it won't change the policy.
+
+Why the year rule? When a new game comes out, why is the n-1 from yesterday
+suddenly OK? There are two important aspects to my reasoning: Acting in good
+faith, and preservation.
+
+You have to remember that rhythm games are an extremely niche market when
+compared to all other game formats. Arcade rhythm games are even more niche, and
+critically, comprise a different payment model (credits vs gacha/one-time-buy).
+
+Compared to huge MOBAs and FPS games where the majority just pays and plays the
+game, the at-home-arcade community *is* big enough to cause financial problems
+if they all stay at home, which is why we've seen COVID hit Sega hard enough to
+start closing flagship arcades in Akihabara.
+
+I have heard firsthand from arcade operators that as soon as home-data is
+released that matches or exceeds the version in their arcade, attendance drops
+sharply. After all, with no commute and no credits, playing at home on your ASC
+is usually good enough.
+
+So the first point, acting in good faith. By releasing old data, you minimise
+depriving arcade operators of their income. Whether it's in Japan and the coins
+go straight to Konami, or you're in Europe and they fund your local arcade's
+next cab purchase, it's critical to actually financially support the games in
+this niche.
+
+Adding the 1 year specifier is for games like Jubeat and Reflecbeat - games that
+have not seen a new release in years. I believe it's acting in good faith to
+release year old data. If there's been literally no updates in a year... That's
+on Konami for abandoning a property.
+
+The second aspect is preservation. Except for buying an offline cab and hoping
+it has the right version (good luck), there is simply no way to legitimately
+play old releases of these games. This is why n-1 suddenly becomes "good" once
+the next version of the game is released. So much digital history has been lost
+due to bitrot and always-online games becoming unplayable. I like the idea of
+being able to play every game in a series ever released.
+
+# Why did you write all this?
+A lot of people who share my opinion like to loudly flame newcomers thirsty for
+the latest data, chastising them for not innately knowing the correct etiquette.
+This is my attempt to explain where my opinions come from. If it's useful to
+even 1 person, it's done its job.
diff --git a/favicon/android-chrome-192x192.png b/favicon/android-chrome-192x192.png
new file mode 100644
index 0000000..0336bfa
Binary files /dev/null and b/favicon/android-chrome-192x192.png differ
diff --git a/favicon/android-chrome-512x512.png b/favicon/android-chrome-512x512.png
new file mode 100644
index 0000000..2c86b96
Binary files /dev/null and b/favicon/android-chrome-512x512.png differ
diff --git a/favicon/apple-touch-icon.png b/favicon/apple-touch-icon.png
new file mode 100644
index 0000000..d493e65
Binary files /dev/null and b/favicon/apple-touch-icon.png differ
diff --git a/favicon/favicon-16x16.png b/favicon/favicon-16x16.png
new file mode 100644
index 0000000..c65faf4
Binary files /dev/null and b/favicon/favicon-16x16.png differ
diff --git a/favicon/favicon-32x32.png b/favicon/favicon-32x32.png
new file mode 100644
index 0000000..b73ab2d
Binary files /dev/null and b/favicon/favicon-32x32.png differ
diff --git a/favicon/favicon.ico b/favicon/favicon.ico
new file mode 100644
index 0000000..bc842fa
Binary files /dev/null and b/favicon/favicon.ico differ
diff --git a/favicon/site.webmanifest b/favicon/site.webmanifest
new file mode 100644
index 0000000..45dc8a2
--- /dev/null
+++ b/favicon/site.webmanifest
@@ -0,0 +1 @@
+{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
\ No newline at end of file
diff --git a/futuretone.html b/futuretone.html
new file mode 100644
index 0000000..cde614a
--- /dev/null
+++ b/futuretone.html
@@ -0,0 +1,146 @@
+
+
+
+
+ Hatsune Miku: Project DIVA Arcade Future Tone Modder
+
+
+
+
+
+
Hatsune Miku: Project DIVA Arcade Future Tone Modder
+
+
diff --git a/initialdzero.html b/initialdzero.html
new file mode 100644
index 0000000..dc2fd88
--- /dev/null
+++ b/initialdzero.html
@@ -0,0 +1,152 @@
+
+
+
+
+ Initial D Arcade Stage Zero DLL Modder
+
+
+
+
+
+
Initial D Arcade Stage Zero DLL Modder
+
WARNING: This is version 2 of the fullscreen patch, but it needs more testing.
+
+ Pro tip: Using the fullscreen patch and using Rivatuner to lock to 60fps, then setting "FullScreen = 0" and "SYNCMODE = 0" in the InitialD.ini will let you alt-tab out
+ of the game, and still run at locked 60fps.
+
+
Notes about the in-store patch:
+
+ The game will still require an ip in the 192.168.X.X range to boot up. However, everything past that can be freely choosen now, so using something like zerotier or a custom
+ vpn It's technically possible to play in-store battles. You'll need to both have matching ip-ranges in segatools.ini though, and one of you'll have to disable dipsw1, etc.
+
+
+
diff --git a/js/dllpatcher.js b/js/dllpatcher.js
new file mode 100644
index 0000000..e25b106
--- /dev/null
+++ b/js/dllpatcher.js
@@ -0,0 +1,1166 @@
+/*jshint esversion: 6 */
+(function(window, document) {
+"use strict";
+
+// form labels often need unique IDs - this can be used to generate some
+window.Patcher_uniqueid = 0;
+var createID = function() {
+ window.Patcher_uniqueid++;
+ return "dllpatch_" + window.Patcher_uniqueid;
+};
+
+var bytesMatch = function(buffer, offset, bytes) {
+ for(var i = 0; i < bytes.length; i++) {
+ if(buffer[offset+i] != bytes[i])
+ return false;
+ }
+ return true;
+};
+
+var bytesToHex = function(bytes) {
+ var s = ''
+ for(var i = 0; i < bytes.length; i++) {
+ s += bytes[i].toString(16).toUpperCase().padStart(2, '0');
+ }
+ return s;
+}
+
+var hexToBytes = function(hex) {
+ var bytes = [];
+ for(var i = 0; i < hex.length; i += 2) {
+ bytes.push(parseInt(hex.substr(i, 2), 16));
+ }
+ return bytes;
+}
+
+var replace = function(buffer, offset, bytes) {
+ for(var i = 0; i < bytes.length; i++) {
+ buffer[offset+i] = bytes[i];
+ }
+};
+
+var whichBytesMatch = function(buffer, offset, bytesArray) {
+ for(var i = 0; i < bytesArray.length; i++) {
+ if(bytesMatch(buffer, offset, bytesArray[i]))
+ return i;
+ }
+ return -1;
+};
+
+// shorthand functions
+var createElementClass = function(elName, className, textContent, innerHTML) {
+ var el = document.createElement(elName);
+ el.className = className || '';
+ el.textContent = textContent || ''; // optional
+ // overrides textContent with HTML if provided
+ if(innerHTML) {
+ el.innerHTML = innerHTML;
+ }
+ return el;
+};
+
+var createInput = function(type, id, className) {
+ var el = document.createElement('input');
+ el.type = type;
+ el.id = id;
+ el.className = className || '';
+ return el;
+};
+
+var createLabel = function(labelText, htmlFor, className) {
+ var el = document.createElement('label');
+ el.textContent = labelText;
+ el.htmlFor = htmlFor;
+ el.className = className || '';
+ return el;
+};
+
+// Each unique kind of patch should have createUI, validatePatch, applyPatch,
+// updateUI
+
+class StandardPatch {
+ constructor(options) {
+ this.name = options.name;
+ this.patches = options.patches;
+ this.tooltip = options.tooltip;
+ this.danger = options.danger;
+ }
+
+ createUI(parent) {
+ var id = createID();
+ var label = this.name;
+ var patch = createElementClass('div', 'patch');
+ this.checkbox = createInput('checkbox', id);
+ patch.appendChild(this.checkbox);
+ patch.appendChild(createLabel(label, id));
+ if(this.tooltip) {
+ patch.appendChild(createElementClass('div', 'tooltip', this.tooltip));
+ }
+ if(this.danger) {
+ patch.appendChild(createElementClass('div', 'danger tooltip', this.danger));
+ }
+ parent.appendChild(patch);
+ }
+
+ updateUI(file) {
+ this.checkbox.checked = this.checkPatchBytes(file) === "on";
+ }
+
+ validatePatch(file) {
+ var status = this.checkPatchBytes(file);
+ if(status === "on") {
+ console.log('"' + this.name + '"', "is enabled!");
+ } else if(status === "off") {
+ console.log('"' + this.name + '"', "is disabled!");
+ } else {
+ return '"' + this.name + '" is neither on nor off! Have you got the right file?';
+ }
+ }
+
+ applyPatch(file) {
+ this.replaceAll(file, this.checkbox.checked);
+ }
+
+ replaceAll(file, featureOn) {
+ for(var i = 0; i < this.patches.length; i++) {
+ replace(file, this.patches[i].offset,
+ featureOn? this.patches[i].on : this.patches[i].off);
+ }
+ }
+
+ checkPatchBytes(file) {
+ var patchStatus = "";
+ for(var i = 0; i < this.patches.length; i++) {
+ var patch = this.patches[i];
+ if(bytesMatch(file, patch.offset, patch.off)) {
+ if(patchStatus === "") {
+ patchStatus = "off";
+ } else if(patchStatus != "off"){
+ return "on/off mismatch within patch";
+ }
+ } else if(bytesMatch(file, patch.offset, patch.on)) {
+ if(patchStatus === "") {
+ patchStatus = "on";
+ } else if(patchStatus != "on"){
+ return "on/off mismatch within patch";
+ }
+ } else {
+ return "patch neither on nor off";
+ }
+ }
+ return patchStatus;
+ }
+}
+
+class DynamicPatch {
+ constructor(options) {
+ this.name = options.name;
+ this.patches = options.patches;
+ this.tooltip = options.tooltip;
+ this.danger = options.danger;
+ this.mode = options.mode;
+ this.target = options.target;
+ }
+
+ createUI(parent) {
+ var id = createID();
+ var label = this.name;
+ this.ui = createElementClass('div', 'patch');
+ this.checkbox = createInput('checkbox', id);
+ this.ui.appendChild(this.checkbox);
+ this.ui.appendChild(createLabel(label, id));
+ if(this.tooltip) {
+ this.ui.appendChild(createElementClass('div', 'tooltip', this.tooltip));
+ }
+ if(this.danger) {
+ this.ui.appendChild(createElementClass('div', 'danger tooltip', this.danger));
+ }
+ parent.appendChild(this.ui);
+ }
+
+ updateUI(file) {
+ if (this.mode === 'all') {
+ this.checkbox.checked = this.checkPatchAll(file, true) === "on";
+ } else {
+ this.checkbox.checked = this.checkPatch(file, true) === "on";
+ }
+ }
+
+ validatePatch(file) {
+ var status = this.mode === 'all' ? this.checkPatchAll(file) : this.checkPatch(file);
+
+ if(status === "on") {
+ console.log('"' + this.name + '"', "is enabled!");
+ } else if(status === "off") {
+ console.log('"' + this.name + '"', "is disabled!");
+ } else {
+ return '"' + this.name + '" is neither on nor off! Have you got the right file?';
+ }
+ }
+
+ applyPatch(file) {
+ this.replaceAll(file, this.checkbox.checked);
+ }
+
+ replaceAll(file, featureOn) {
+ for(let patch of this.patches) {
+ if (Array.isArray(patch.offset)) {
+ for(const offset of patch.offset) {
+ if (this.target === 'string') {
+ replace(file, offset,
+ new TextEncoder().encode(featureOn? patch.on : patch.off));
+ } else {
+ patch.on = patch.on.map((byte, idx) => byte === 'XX' ? file[offset + idx] : byte);
+ patch.off = patch.off.map((byte, idx) => byte === 'XX' ? file[offset + idx] : byte);
+ replace(file, offset,
+ featureOn? patch.on : patch.off);
+ }
+ }
+ } else {
+ if (this.target === 'string') {
+ replace(file, patch.offset,
+ new TextEncoder().encode(featureOn? patch.on : patch.off));
+ } else {
+ patch.on = patch.on.map((byte, idx) => byte === 'XX' ? file[patch.offset + idx] : byte);
+ patch.off = patch.off.map((byte, idx) => byte === 'XX' ? file[patch.offset + idx] : byte);
+ replace(file, patch.offset,
+ featureOn? patch.on : patch.off);
+ }
+ }
+ }
+ }
+
+ checkPatch(file, updateUiFlag = false) {
+ var patchStatus = "";
+ let listUi;
+ if (updateUiFlag) {
+ listUi = document.createElement('ul');
+ this.ui.appendChild(listUi);
+ }
+ for(var i = 0; i < this.patches.length; i++) {
+ var patch = this.patches[i];
+ var offOffset = this.searchPatchOffset(file, patch.off, i);
+ var onOffset = this.searchPatchOffset(file, patch.on, i);
+ this.patches[i].offset = offOffset === -1 ? onOffset : offOffset;
+ if(offOffset > 0) {
+ if (updateUiFlag) {
+ if (this.target === 'string') {
+ listUi.appendChild(createElementClass('li', 'patch-off', null, '0x' + offOffset.toString(16) + ' ' + patch.off + ' will be replaced with '+ patch.on +''));
+ } else {
+ listUi.appendChild(createElementClass('li', 'patch-off', '0x' + offOffset.toString(16) + ' will be replaced'));
+ }
+ }
+ if(patchStatus === "") {
+ patchStatus = "off";
+ }
+ } else if(onOffset > 0) {
+ if (updateUiFlag) {
+ if (this.target === 'string') {
+ listUi.appendChild(createElementClass('li', 'patch-on', null, '0x' + onOffset.toString(16) + ' ' + patch.on + ' will be replaced with '+ patch.off +''));
+ } else {
+ listUi.appendChild(createElementClass('li', 'patch-on', '0x' + onOffset.toString(16) + ' will be replaced'));
+ }
+ }
+ if(patchStatus === "") {
+ patchStatus = "on";
+ }
+ } else if (this.mode === 'all') {
+ continue;
+ } else {
+ return "patch string not found";
+ }
+ }
+ return patchStatus;
+ }
+
+ checkPatchAll(file, updateUiFlag = false) {
+ var patchStatus = "";
+ let listUi;
+ if (updateUiFlag) {
+ listUi = document.createElement('ul');
+ this.ui.appendChild(listUi);
+ }
+ for(let patch of this.patches) {
+ var offOffset = this.searchPatchOffsetAll(file, patch.off);
+ var onOffset = this.searchPatchOffsetAll(file, patch.on);
+ patch.offset = offOffset.length === 0 ? onOffset : offOffset;
+
+ if(offOffset.length > 0) {
+ if (updateUiFlag) {
+ for(const offset of offOffset) {
+ listUi.appendChild(createElementClass('li', 'patch-off', '0x' + offset.toString(16) + ' will be replaced'));
+ }
+ }
+ if(patchStatus === "") {
+ patchStatus = "off";
+ }
+ } else if(onOffset.length > 0) {
+ if (updateUiFlag) {
+ for(const offset of onOffset) {
+ listUi.appendChild(createElementClass('li', 'patch-on', '0x' + offset.toString(16) + ' will be replaced'));
+ }
+ }
+ if(patchStatus === "") {
+ patchStatus = "on";
+ }
+ } else {
+ return "patch string not found";
+ }
+ }
+ return patchStatus;
+ }
+
+ searchPatchOffset(file, search, offset) {
+ let searchBytes;
+ if (this.target === 'string') {
+ searchBytes = new TextEncoder().encode(search);
+ } else {
+ searchBytes = search;
+ }
+
+ Uint8Array.prototype.indexOfArr = function(searchElements, fromIndex) {
+ fromIndex = fromIndex || 0;
+
+ var index = Array.prototype.indexOf.call(this, searchElements[0], fromIndex);
+ if(searchElements.length === 1 || index === -1) {
+ return {
+ match: false,
+ index: -1,
+ };
+ }
+
+ for(var i = index, j = 0; j < searchElements.length && i < this.length; i++, j++) {
+ if (this.target !== 'string' && searchElements[j] === 'XX') {
+ continue;
+ }
+ if(this[i] !== searchElements[j]) {
+ return {
+ match: false,
+ index,
+ };
+ }
+ }
+ return {
+ match: true,
+ index,
+ };
+ };
+
+ var idx = 0;
+ var foundCount = 0;
+ for (var i = 0; i < file.length; i++) {
+ var result = file.indexOfArr(searchBytes, idx);
+ if (result.match) {
+ if (offset === foundCount) {
+ return result.index;
+ }
+ foundCount++;
+ } else if (result.index === -1) {
+ break;
+ }
+ idx = result.index + 1;
+ }
+ return -1;
+ }
+
+ searchPatchOffsetAll(file, search) {
+ let searchBytes;
+ if (this.target === 'string') {
+ searchBytes = new TextEncoder().encode(search);
+ } else {
+ searchBytes = search;
+ }
+
+ Uint8Array.prototype.indexOfArr = function(searchElements, fromIndex) {
+ fromIndex = fromIndex || 0;
+
+ var index = Array.prototype.indexOf.call(this, searchElements[0], fromIndex);
+ if(searchElements.length === 1 || index === -1) {
+ return {
+ match: false,
+ index: -1,
+ };
+ }
+
+ for(var i = index, j = 0; j < searchElements.length && i < this.length; i++, j++) {
+ if (this.target !== 'string' && searchElements[j] === 'XX') {
+ continue;
+ }
+ if(this[i] !== searchElements[j]) {
+ return {
+ match: false,
+ index,
+ };
+ }
+ }
+
+ return {
+ match: true,
+ index,
+ };
+ };
+
+ var idx = 0;
+ var foundOffsetArray = [];
+ for (var i = 0; i < file.length; i++) {
+ var result = file.indexOfArr(searchBytes, idx);
+ if (result.match) {
+ foundOffsetArray.push(result.index);
+ } else if (result.index === -1) {
+ break;
+ }
+ idx = result.index + 1;
+ }
+ return foundOffsetArray;
+ }
+}
+
+// Each unique kind of patch should have createUI, validatePatch, applyPatch,
+// updateUI
+
+// The DEFAULT state is always the 1st element in the patches array
+class UnionPatch {
+ constructor(options) {
+ this.name = options.name;
+ this.offset = options.offset;
+ this.patches = options.patches;
+ this.tooltip = options.tooltip;
+ this.danger = options.danger;
+ }
+
+ createUI(parent) {
+ this.radios = [];
+ var radio_id = createID();
+
+ var container = createElementClass('div', 'patch-union');
+ container.appendChild(createElementClass('span', 'patch-union-title', this.name + ':'));
+ if(this.tooltip) {
+ container.appendChild(createElementClass('div', 'tooltip', this.tooltip));
+ }
+ if(this.danger) {
+ container.appendChild(createElementClass('div', 'danger tooltip', this.danger));
+ }
+ container.appendChild(document.createElement('span'));
+
+ for(var i = 0; i < this.patches.length; i++) {
+ var patch = this.patches[i];
+ var id = createID();
+ var label = patch.name;
+ var patchDiv = createElementClass('div', 'patch');
+ var radio = createInput('radio', id);
+ radio.name = radio_id;
+ this.radios.push(radio);
+
+ patchDiv.appendChild(radio);
+ patchDiv.appendChild(createLabel(label, id));
+ if(patch.tooltip) {
+ patchDiv.appendChild(createElementClass('div', 'tooltip', patch.tooltip));
+ }
+ if(patch.danger) {
+ patchDiv.appendChild(createElementClass('div', 'danger tooltip', patch.danger));
+ }
+ container.appendChild(patchDiv);
+ }
+ parent.appendChild(container);
+ }
+
+ updateUI(file) {
+ for(var i = 0; i < this.patches.length; i++) {
+ if(bytesMatch(file, this.offset, this.patches[i].patch)) {
+ this.radios[i].checked = true;
+ return;
+ }
+ }
+ // Default fallback
+ this.radios[0].checked = true;
+ }
+
+ validatePatch(file) {
+ for(var i = 0; i < this.patches.length; i++) {
+ if(bytesMatch(file, this.offset, this.patches[i].patch)) {
+ console.log(this.name, "has", this.patches[i].name, "enabled");
+ return;
+ }
+ }
+ return '"' + this.name + '" doesn\'t have a valid patch! Have you got the right file?';
+ }
+
+ applyPatch(file) {
+ var patch = this.getSelected();
+ replace(file, this.offset, patch.patch);
+ }
+
+ getSelected() {
+ for(var i = 0; i < this.patches.length; i++) {
+ if(this.radios[i].checked) {
+ return this.patches[i];
+ }
+ }
+ return null;
+ }
+}
+
+// Each unique kind of patch should have createUI, validatePatch, applyPatch,
+// updateUI
+class NumberPatch {
+ constructor(options) {
+ this.name = options.name;
+ this.tooltip = options.tooltip;
+ this.danger = options.danger;
+
+ this.offset = options.offset;
+ this.size = options.size;
+ this.min = options.min;
+ this.max = options.max;
+ this.littleEndian = options.littleEndian ?? true;
+
+ if (![1, 2, 4].includes(this.size)) {
+ throw new Error(`Unsupported number size: ${this.size}.`);
+ }
+ }
+
+ createUI(parent) {
+ var id = createID();
+ var label = this.name;
+ var patch = createElementClass('div', 'patch');
+
+ patch.appendChild(createLabel(label, id));
+
+ this.number = createInput('number', id);
+ this.number.style.width = "3rem";
+ if (this.min !== null) {
+ this.number.min = this.min;
+ }
+ if (this.max) {
+ this.number.max = this.max;
+ }
+
+ patch.appendChild(this.number);
+
+
+ if (this.tooltip) {
+ patch.appendChild(createElementClass('div', 'tooltip', this.tooltip));
+ }
+ if (this.danger) {
+ patch.appendChild(createElementClass('div', 'danger tooltip', this.danger));
+ }
+ parent.appendChild(patch);
+
+ }
+
+ updateUI(file) {
+ const arr = new Uint8Array(this.size);
+ const view = new DataView(arr.buffer);
+
+ for (var i = 0; i < this.size; i++) {
+ arr[i] = file[this.offset + i];
+ }
+
+ if (this.size === 1) {
+ this.number.value = view.getInt8(0);
+ } else if (this.size === 2) {
+ this.number.value = view.getInt16(0, this.littleEndian);
+ } else if (this.size === 4) {
+ this.number.value = view.getInt32(0, this.littleEndian);
+ }
+ }
+
+ validatePatch(file) {
+ return;
+ }
+
+ applyPatch(file) {
+ // Convert user inputted number to little endian
+ const arr = new Uint8Array(this.size);
+ const view = new DataView(arr.buffer);
+
+ if (this.size === 1) {
+ view.setInt8(0, this.number.value);
+ } else if (this.size === 2) {
+ view.setInt16(0, this.number.value, this.littleEndian);
+ } else if (this.size === 4) {
+ view.setInt32(0, this.number.value, this.littleEndian);
+ }
+
+ for (var i = 0; i < this.size; i++) {
+ file[this.offset + i] = arr[i];
+ }
+ }
+}
+
+// Each unique kind of patch should have createUI, validatePatch, applyPatch,
+// updateUI
+class HexPatch {
+ constructor(options) {
+ this.name = options.name;
+ this.tooltip = options.tooltip;
+ this.danger = options.danger;
+ this.offset = options.offset;
+
+ this.off = options.off;
+ }
+
+ createUI(parent) {
+ this.radios = [];
+ var radio_id = createID();
+
+ // Title of the radio option.
+ var container = createElementClass('div', 'patch-union');
+ container.appendChild(createElementClass('span', 'patch-union-title', this.name + ':'));
+ if(this.tooltip) {
+ container.appendChild(createElementClass('div', 'tooltip', this.tooltip));
+ }
+ if(this.danger) {
+ container.appendChild(createElementClass('div', 'danger tooltip', this.danger));
+ }
+ container.appendChild(document.createElement('span'));
+
+ // Default option; tooltip shows default hex value.
+ var id = createID();
+ var patchDiv = createElementClass('div', 'patch');
+ var radio = createInput('radio', id);
+ radio.name = radio_id;
+ this.radios.push(radio);
+
+ patchDiv.appendChild(radio);
+ patchDiv.appendChild(createLabel('Default', id));
+ patchDiv.appendChild(createElementClass('div', 'tooltip', 'Value ' + bytesToHex(this.off)));
+ container.appendChild(patchDiv);
+
+ // Custom option.
+ id = createID();
+ patchDiv = createElementClass('div', 'patch');
+ radio = createInput('radio', id);
+ radio.name = radio_id;
+ this.radios.push(radio);
+
+ patchDiv.appendChild(radio);
+ patchDiv.appendChild(createLabel('Custom ' + this.off.length + '-byte hex value: ', id));
+ this.valueHex = document.createElement('input');
+ this.valueHex.type = 'text';
+ this.valueHex.id = id;
+ patchDiv.appendChild(this.valueHex);
+
+ patchDiv.appendChild(createElementClass('div', 'danger tooltip', 'Invalid values will not be applied.'));
+ container.appendChild(patchDiv);
+
+ parent.appendChild(container);
+
+ }
+
+ updateUI(file) {
+ if(bytesMatch(file, this.offset, this.off)) {
+ this.radios[0].checked = true;
+ return;
+ }
+ this.valueHex.value = bytesToHex(file.slice(this.offset, this.offset + this.off.length));
+ this.radios[1].checked = true;
+ }
+
+ validatePatch(file) {
+ if(bytesMatch(file, this.offset, this.off)) {
+ console.log(this.name, "has default hex value");
+ return;
+ }
+ console.log(this.name, "has custom hex value");
+ }
+
+ applyPatch(file) {
+ if(this.radios[0].checked) {
+ replace(file, this.offset, this.off);
+ return;
+ }
+ if(this.radios[1].checked) {
+ if(!this.valueHex.value.match(/^[0-9a-fA-F]+$/)) {
+ alert('Patch "' + this.name + '" not applied - invalid hex!');
+ return;
+ }
+ if(this.valueHex.value.length != this.off.length * 2) {
+ alert('Patch "' + this.name + '" not applied - invalid length!!');
+ return;
+ }
+ replace(file, this.offset, hexToBytes(this.valueHex.value));
+ return;
+ }
+ }
+}
+
+var loadPatch = function(_this, self, patcher) {
+ patcher.loadPatchUI();
+ patcher.updatePatchUI();
+ patcher.container.style.display = '';
+ var successStr = patcher.filename;
+ if (typeof _this.description === "string") {
+ successStr += "(" + patcher.description + ")";
+ }
+ self.successDiv.innerHTML = successStr + " loaded successfully!";
+};
+
+/**
+ * Check if the input is a PE executable, and if it is, parses the PE
+ * just enough to see if it's packed with HyperTech CrackProof, by
+ * checking if the first DLL import is "KeRnEl32.dLl".
+ *
+ * If there was any error parsing the executable or if it was packed,
+ * this function returns a string. If there were no errors, this returns
+ * undefined.
+ *
+ * @param {Uint8Array} dllFile
+ * @returns {string | undefined}
+ */
+var checkUnpackedExecutable = function(dllFile) {
+ // Note to anyone looking: all offsets are absolute file offsets.
+
+ const DOS_EXECUTABLE_MAGIC = [0x4D, 0x5A];
+ const PE_EXECUTABLE_MAGIC = [0x50, 0x45, 0x00, 0x00];
+ const PE_ROM_MAGIC = 0x107;
+ const PE32_MAGIC = 0x10B;
+ const PE32_PLUS_MAGIC = 0x20B;
+ const OFFSET_IMAGE_OPTIONAL_HEADER32_NUMBER_OF_RVAS = 0x5C;
+ const OFFSET_IMAGE_OPTIONAL_HEADER64_NUMBER_OF_RVAS = 0x6C;
+ const SIZEOF_IMAGE_IMPORT_DESCRIPTOR = 0x28;
+ const CRACKPROOF_KERNEL32_DLL = [
+ 0x4B, 0x65, 0x52, 0x6E, 0x45, 0x6C, 0x33, 0x32, 0x2E, 0x64, 0x4C, 0x6C, 0x00
+ ]; // "KeRnEl32.dLl\0"
+
+ // We do have patchers for other types of files, like chu.acf,
+ // so skip the check if we're not looking at a PE executable.
+ if (!bytesMatch(dllFile, 0x00, DOS_EXECUTABLE_MAGIC)) {
+ return undefined;
+ }
+
+ const dllFileView = new DataView(dllFile.buffer);
+ const peHeaderOffset = dllFileView.getUint32(0x3C, true); // e_lfanew
+
+ if (!bytesMatch(dllFile, peHeaderOffset, PE_EXECUTABLE_MAGIC)) {
+ // Probably a file that accidentally looks like a DOS executable...
+ return undefined;
+ }
+
+ const numberOfSections = dllFileView.getUint16(peHeaderOffset + 0x06, true);
+ const sizeOfOptionalHeader = dllFileView.getUint16(peHeaderOffset + 0x14, true);
+ const startOfOptionalHeader = peHeaderOffset + 0x18;
+ const startOfSections = startOfOptionalHeader + sizeOfOptionalHeader;
+
+ const rva2offset = (rva) => {
+ for (let i = 0; i < numberOfSections; i++) {
+ const sectionHeaderOffset = startOfSections + 0x28 * i;
+ const virtualSize = dllFileView.getUint32(sectionHeaderOffset + 0x08, true);
+ const virtualAddress = dllFileView.getUint32(sectionHeaderOffset + 0x0C, true);
+ const pointerToRawData = dllFileView.getUint32(sectionHeaderOffset + 0x14, true);
+
+ if (virtualAddress <= rva && (virtualAddress + virtualSize) > rva) {
+ return rva - virtualAddress + pointerToRawData;
+ }
+ }
+
+ return -1;
+ };
+
+ const magic = dllFileView.getUint16(startOfOptionalHeader, true);
+
+ if (![PE_ROM_MAGIC, PE32_MAGIC, PE32_PLUS_MAGIC].includes(magic)) {
+ return `Unknown PE format ${magic}.`;
+ }
+
+ const numberOfRvaAndSizesOffset = startOfOptionalHeader + (
+ magic === PE32_PLUS_MAGIC
+ ? OFFSET_IMAGE_OPTIONAL_HEADER64_NUMBER_OF_RVAS
+ : OFFSET_IMAGE_OPTIONAL_HEADER32_NUMBER_OF_RVAS
+ );
+ const numberOfRvaAndSizes = dllFileView.getUint32(numberOfRvaAndSizesOffset, true);
+
+ // No import directory, apparently. Not our problem then.
+ if (numberOfRvaAndSizes < 2) {
+ return undefined;
+ }
+
+ const startOfDataDirectories = numberOfRvaAndSizesOffset + 0x04;
+
+ // The import directory is the second directory (index 1), as can be seen in winnt.h:
+ // #define IMAGE_DIRECTORY_ENTRY_IMPORT 1
+ const importDirectoryRva = dllFileView.getUint32(startOfDataDirectories + 0x08, true);
+ const importDirectorySize = dllFileView.getUint32(startOfDataDirectories + 0x0C, true);
+
+ // No imports, also not our problem.
+ if (importDirectorySize < SIZEOF_IMAGE_IMPORT_DESCRIPTOR) {
+ return undefined;
+ }
+
+ const importDirectoryOffset = rva2offset(importDirectoryRva);
+
+ if (importDirectoryOffset === -1) {
+ return "Could not convert import directory RVA to offset.";
+ }
+
+ const importDllNameRva = dllFileView.getUint32(importDirectoryOffset + 12, true);
+ const importDllNameOffset = rva2offset(importDllNameRva);
+
+ if (importDllNameOffset === -1) {
+ return "Could not convert import DLL name RVA to offset.";
+ }
+
+ if (bytesMatch(dllFile, importDllNameOffset, CRACKPROOF_KERNEL32_DLL)) {
+ return "This executable is packed with CrackProof. Please obtain an unpacked executable.";
+ }
+
+ return undefined;
+}
+
+class PatchContainer {
+ constructor(patchers) {
+ this.patchers = patchers;
+ this.createUI();
+ }
+
+ getSupportedDLLs() {
+ var dlls = [];
+ for (var i = 0; i < this.patchers.length; i++) {
+ var name = this.patchers[i].filename;
+ if (dlls.indexOf(name) === -1) {
+ dlls.push(name);
+ }
+ }
+ return dlls;
+ }
+
+ createUI() {
+ var self = this;
+ var container = createElementClass('div', 'patchContainer');
+ var header = this.getSupportedDLLs().join(", ");
+ container.innerHTML = "
" + header + "
";
+
+ var supportedDlls = document.createElement('ul');
+ this.forceLoadTexts = [];
+ this.forceLoadButtons = [];
+ this.matchSuccessText = [];
+ for (var i = 0; i < this.patchers.length; i++) {
+ var checkboxId = createID();
+
+ var listItem = document.createElement('li');
+ listItem.appendChild(createLabel(this.patchers[i].description, checkboxId, 'patchPreviewLabel'));
+ var matchPercent = createElementClass('span', 'matchPercent');
+ this.forceLoadTexts.push(matchPercent);
+ listItem.appendChild(matchPercent);
+ var matchSuccess = createElementClass('span', 'matchSuccess');
+ this.matchSuccessText.push(matchSuccess);
+ listItem.appendChild(matchSuccess);
+ var forceButton = createElementClass('button', '', 'Force load?');
+ forceButton.style.display = 'none';
+ this.forceLoadButtons.push(forceButton);
+ listItem.appendChild(forceButton);
+
+ var input = createInput('checkbox', checkboxId, 'patchPreviewToggle');
+ listItem.appendChild(input);
+ var patchPreviews = createElementClass('ul', 'patchPreview');
+ for (var j = 0; j < this.patchers[i].mods.length; j++) {
+ var patchName = this.patchers[i].mods[j].name;
+ patchPreviews.appendChild(createElementClass('li', null, patchName));
+ }
+ listItem.appendChild(patchPreviews);
+
+ supportedDlls.appendChild(listItem);
+ }
+
+ ["dragover", "dragenter"].forEach(function(n){
+ document.documentElement.addEventListener(n,function (e) {
+ container.classList.add("dragover");
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ });
+ ["dragleave", "dragend", "drop"].forEach(function(n){
+ document.documentElement.addEventListener(n,function (e) {
+ container.classList.remove("dragover");
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ });
+
+ container.addEventListener("drop", function (e) {
+ var files = e.dataTransfer.files;
+ if (files && files.length > 0)
+ self.loadFile(files[0]);
+ });
+
+ var filepickerId = createID();
+ this.fileInput = createInput('file', filepickerId, 'fileInput');
+ var label = createLabel('', filepickerId, 'fileLabel');
+ label.innerHTML = "Choose a file or drag and drop.";
+
+ this.fileInput.addEventListener("change", function (e) {
+ if (this.files && this.files.length > 0)
+ self.loadFile(this.files[0]);
+ });
+
+ this.successDiv = createElementClass('div', 'success');
+ this.errorDiv = createElementClass('div', 'error');
+
+ container.appendChild(this.fileInput);
+ container.appendChild(label);
+
+ container.appendChild(createElementClass('h4', null, 'Supported Versions:'));
+ container.appendChild(createElementClass('h5', null, 'Click name to preview patches'));
+ container.appendChild(supportedDlls);
+ container.appendChild(this.successDiv);
+ container.appendChild(this.errorDiv);
+ document.body.appendChild(container);
+ }
+
+ loadFile(file) {
+ var reader = new FileReader();
+ var self = this;
+
+ reader.onload = function (e) {
+ var found = false;
+ // clear logs
+ self.errorDiv.textContent = '';
+ self.successDiv.textContent = '';
+
+ const error = checkUnpackedExecutable(new Uint8Array(e.target.result));
+
+ if (error) {
+ self.errorDiv.innerHTML = error;
+ return;
+ }
+
+ for (var i = 0; i < self.patchers.length; i++) {
+ // reset text and buttons
+ self.forceLoadButtons[i].style.display = 'none';
+ self.forceLoadTexts[i].textContent = '';
+ self.matchSuccessText[i].textContent = '';
+ var patcher = self.patchers[i];
+ // remove the previous UI to clear the page
+ patcher.destroyUI();
+ // patcher UI elements have to exist to load the file
+ patcher.createUI();
+ patcher.container.style.display = 'none';
+ patcher.loadBuffer(e.target.result);
+ if (patcher.validatePatches()) {
+ found = true;
+ loadPatch(this, self, patcher);
+ // show patches matched for 100% - helps identify which version is loaded
+ var valid = patcher.validPatches;
+ self.matchSuccessText[i].textContent = ' ' + valid + ' of ' + valid + ' patches matched (100%) ';
+ }
+ }
+
+ if (!found) {
+ // let the user force a match
+ for (let i = 0; i < self.patchers.length; i++) {
+ const patcher = self.patchers[i];
+
+ const valid = patcher.validPatches;
+ const percent = (valid / patcher.totalPatches * 100).toFixed(1);
+
+ self.forceLoadTexts[i].textContent = ' ' + valid + ' of ' + patcher.totalPatches + ' patches matched (' + percent + '%) ';
+ self.forceLoadButtons[i].style.display = '';
+ self.forceLoadButtons[i].onclick = function(i) {
+ // reset old text
+ for(var j = 0; j < self.patchers.length; j++) {
+ self.forceLoadButtons[j].style.display = 'none';
+ self.forceLoadTexts[j].textContent = '';
+ }
+
+
+ loadPatch(this, self, self.patchers[i]);
+ }.bind(this, i);
+ }
+ self.errorDiv.innerHTML = "No patch set was a 100% match.";
+ }
+ };
+
+ reader.readAsArrayBuffer(file);
+ }
+}
+
+class Patcher {
+ constructor(fname, description, args) {
+ this.mods = [];
+ for(var i = 0; i < args.length; i++) {
+ var mod = args[i];
+ if(mod.type) {
+ if(mod.type === "union") {
+ this.mods.push(new UnionPatch(mod));
+ }
+ if(mod.type === "number") {
+ this.mods.push(new NumberPatch(mod));
+ }
+ if(mod.type === "dynamic") {
+ this.mods.push(new DynamicPatch(mod));
+ }
+ if(mod.type === "hex") {
+ this.mods.push(new HexPatch(mod));
+ }
+ } else { // standard patch
+ this.mods.push(new StandardPatch(mod));
+ }
+ }
+
+ this.filename = fname;
+ this.description = description;
+ this.multiPatcher = true;
+
+ if (!this.description) {
+ // old style patcher, use the old method to generate the UI
+ this.multiPatcher = false;
+ this.createUI();
+ this.loadPatchUI();
+ }
+ }
+
+ createUI() {
+ var self = this;
+ this.container = createElementClass('div', 'patchContainer');
+ var header = this.filename;
+ if(this.description === "string") {
+ header += ' (' + this.description + ')';
+ }
+ this.container.innerHTML = '