From f529f7482345f31bce69bce5e1a6161b0a921e40 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 8 Jun 2026 16:25:52 +0200 Subject: [PATCH] A lot of things. --- app/Auth/XenForoGuard.php | 5 + app/Auth/XenForoUser.php | 32 +- app/Helpers/EntryHelpers.php | 2 + .../Controllers/DynamicLoadController.php | 1 + app/Http/Controllers/EntryController.php | 16 + .../Controllers/ModCP/AuthorController.php | 72 + app/Http/Controllers/ModCP/GameController.php | 75 + .../Controllers/ModCP/GenreController.php | 68 + .../Controllers/ModCP/LanguageController.php | 67 + .../Controllers/ModCP/PlatformController.php | 68 + app/Http/Controllers/ModCPController.php | 78 + app/Http/Controllers/SubmissionController.php | 25 +- app/Http/Controllers/ToolsController.php | 22 + app/Http/Requests/StoreDraftRequest.php | 26 + app/Http/Requests/StoreEntryRequest.php | 2 + app/Jobs/DeleteXenForoCommentsThread.php | 34 + app/Jobs/RestoreXenForoCommentsThread.php | 34 + app/Livewire/Database.php | 34 +- app/Livewire/XfUserSelector.php | 2 +- app/Models/Author.php | 18 + app/Models/EntryFile.php | 2 +- app/Models/Game.php | 5 + app/Models/Genre.php | 7 + app/Models/Language.php | 7 + app/Models/Platform.php | 10 + app/Policies/EntryPolicy.php | 2 +- app/Providers/AppServiceProvider.php | 9 + .../Filament/ManagePanelProvider.php | 2 +- app/Services/SubmissionsService.php | 99 +- app/Services/XenforoApiService.php | 23 + app/Traits/ModCPSearch.php | 23 + app/View/Components/ModCPSearch.php | 29 + app/helpers.php | 37 + bootstrap/app.php | 5 + config/menu.php | 11 + ...6_make_entries_fields_draft_compatible.php | 28 + ...235_add_online_patcher_fields_to_files.php | 30 + extra.less | 528 +++- public/ZELDA.ips | Bin 0 -> 1571 bytes public/rom-patcher-js/RomPatcher.js | 412 ++++ public/rom-patcher-js/RomPatcher.webapp.js | 2167 +++++++++++++++++ .../RomPatcher.webworker.apply.js | 83 + .../RomPatcher.webworker.crc.js | 26 + .../RomPatcher.webworker.create.js | 37 + public/rom-patcher-js/modules/BinFile.js | 483 ++++ .../rom-patcher-js/modules/HashCalculator.js | 179 ++ .../modules/RomPatcher.format.aps_gba.js | 114 + .../modules/RomPatcher.format.aps_n64.js | 211 ++ .../modules/RomPatcher.format.bdf.js | 90 + .../modules/RomPatcher.format.bps.js | 466 ++++ .../modules/RomPatcher.format.ips.js | 282 +++ .../modules/RomPatcher.format.pmsr.js | 97 + .../modules/RomPatcher.format.ppf.js | 269 ++ .../modules/RomPatcher.format.rup.js | 396 +++ .../modules/RomPatcher.format.ups.js | 224 ++ .../modules/RomPatcher.format.vcdiff.js | 382 +++ public/rom-patcher-js/modules/bz2/LICENSE | 19 + public/rom-patcher-js/modules/bz2/bz2.js | 1 + public/rom-patcher-js/modules/zip.js/LICENSE | 28 + .../rom-patcher-js/modules/zip.js/inflate.js | 36 + .../rom-patcher-js/modules/zip.js/z-worker.js | 2 + .../rom-patcher-js/modules/zip.js/zip.min.js | 28 + resources/css/app.css | 3 + resources/css/components/common.css | 6 + resources/css/components/drafts.css | 160 ++ resources/css/components/modcp.css | 320 +++ resources/css/components/tools.css | 83 + resources/css/layout/content.css | 36 +- resources/css/layout/entry.css | 7 +- resources/js/RomPatcher.js | 114 + resources/js/SubmissionsClass/FSFileData.js | 10 + resources/js/app.js | 4 + resources/js/submissions.js | 6 + .../views/components/hovercard.blade.php | 4 +- resources/views/components/menu.blade.php | 10 +- .../views/components/modcp-search.blade.php | 6 + .../components/submit-entry-status.blade.php | 53 + resources/views/components/topbar.blade.php | 20 +- resources/views/entries/draft_item.blade.php | 54 + resources/views/entries/drafts.blade.php | 25 + resources/views/entries/show.blade.php | 20 +- resources/views/layouts/modcp.blade.php | 95 + resources/views/livewire/database.blade.php | 3 + resources/views/modcp/authors.blade.php | 89 + resources/views/modcp/deleted.blade.php | 61 + resources/views/modcp/entries.blade.php | 52 + resources/views/modcp/games.blade.php | 103 + resources/views/modcp/index.blade.php | 91 + resources/views/modcp/resources.blade.php | 76 + resources/views/submissions/form.blade.php | 14 +- .../views/submissions/fs-upload.blade.php | 62 +- resources/views/tools/patcher.blade.php | 92 + routes/web.php | 32 + vite.config.js | 4 +- 94 files changed, 9178 insertions(+), 107 deletions(-) create mode 100644 app/Http/Controllers/ModCP/AuthorController.php create mode 100644 app/Http/Controllers/ModCP/GameController.php create mode 100644 app/Http/Controllers/ModCP/GenreController.php create mode 100644 app/Http/Controllers/ModCP/LanguageController.php create mode 100644 app/Http/Controllers/ModCP/PlatformController.php create mode 100644 app/Http/Controllers/ModCPController.php create mode 100644 app/Http/Controllers/ToolsController.php create mode 100644 app/Http/Requests/StoreDraftRequest.php create mode 100644 app/Jobs/DeleteXenForoCommentsThread.php create mode 100644 app/Jobs/RestoreXenForoCommentsThread.php create mode 100644 app/Traits/ModCPSearch.php create mode 100644 app/View/Components/ModCPSearch.php create mode 100644 database/migrations/2026_06_04_083346_make_entries_fields_draft_compatible.php create mode 100644 database/migrations/2026_06_05_163235_add_online_patcher_fields_to_files.php create mode 100644 public/ZELDA.ips create mode 100644 public/rom-patcher-js/RomPatcher.js create mode 100644 public/rom-patcher-js/RomPatcher.webapp.js create mode 100644 public/rom-patcher-js/RomPatcher.webworker.apply.js create mode 100644 public/rom-patcher-js/RomPatcher.webworker.crc.js create mode 100644 public/rom-patcher-js/RomPatcher.webworker.create.js create mode 100644 public/rom-patcher-js/modules/BinFile.js create mode 100644 public/rom-patcher-js/modules/HashCalculator.js create mode 100644 public/rom-patcher-js/modules/RomPatcher.format.aps_gba.js create mode 100644 public/rom-patcher-js/modules/RomPatcher.format.aps_n64.js create mode 100644 public/rom-patcher-js/modules/RomPatcher.format.bdf.js create mode 100644 public/rom-patcher-js/modules/RomPatcher.format.bps.js create mode 100644 public/rom-patcher-js/modules/RomPatcher.format.ips.js create mode 100644 public/rom-patcher-js/modules/RomPatcher.format.pmsr.js create mode 100644 public/rom-patcher-js/modules/RomPatcher.format.ppf.js create mode 100644 public/rom-patcher-js/modules/RomPatcher.format.rup.js create mode 100644 public/rom-patcher-js/modules/RomPatcher.format.ups.js create mode 100644 public/rom-patcher-js/modules/RomPatcher.format.vcdiff.js create mode 100644 public/rom-patcher-js/modules/bz2/LICENSE create mode 100644 public/rom-patcher-js/modules/bz2/bz2.js create mode 100644 public/rom-patcher-js/modules/zip.js/LICENSE create mode 100644 public/rom-patcher-js/modules/zip.js/inflate.js create mode 100644 public/rom-patcher-js/modules/zip.js/z-worker.js create mode 100644 public/rom-patcher-js/modules/zip.js/zip.min.js create mode 100644 resources/css/components/drafts.css create mode 100644 resources/css/components/modcp.css create mode 100644 resources/css/components/tools.css create mode 100644 resources/js/RomPatcher.js create mode 100644 resources/views/components/modcp-search.blade.php create mode 100644 resources/views/entries/draft_item.blade.php create mode 100644 resources/views/entries/drafts.blade.php create mode 100644 resources/views/layouts/modcp.blade.php create mode 100644 resources/views/modcp/authors.blade.php create mode 100644 resources/views/modcp/deleted.blade.php create mode 100644 resources/views/modcp/entries.blade.php create mode 100644 resources/views/modcp/games.blade.php create mode 100644 resources/views/modcp/index.blade.php create mode 100644 resources/views/modcp/resources.blade.php create mode 100644 resources/views/tools/patcher.blade.php diff --git a/app/Auth/XenForoGuard.php b/app/Auth/XenForoGuard.php index 83b691b..4a61359 100644 --- a/app/Auth/XenForoGuard.php +++ b/app/Auth/XenForoGuard.php @@ -74,4 +74,9 @@ class XenForoGuard implements Guard $this->user = $user; } + public function logout(): void + { + redirect('/'); + } + } diff --git a/app/Auth/XenForoUser.php b/app/Auth/XenForoUser.php index 28810ea..def6d9e 100644 --- a/app/Auth/XenForoUser.php +++ b/app/Auth/XenForoUser.php @@ -4,10 +4,13 @@ namespace App\Auth; use App\Services\XenforoService; use App\XenForoDataTypes\XenForoData; +use Filament\Models\Contracts\FilamentUser; +use Filament\Models\Contracts\HasName; +use Filament\Panel; use Illuminate\Contracts\Auth\Access\Authorizable; use Illuminate\Contracts\Auth\Authenticatable; -class XenForoUser extends XenForoData implements Authenticatable, Authorizable { +class XenForoUser extends XenForoData implements Authenticatable, Authorizable, FilamentUser, HasName { use \Illuminate\Foundation\Auth\Access\Authorizable; @@ -75,4 +78,31 @@ class XenForoUser extends XenForoData implements Authenticatable, Authorizable { return ($this->permissions[$permissionGroup][$permissionName] ?? 0) === true; } + + /* FILAMENT COMPATIBILITY */ + + public function canAccessPanel(Panel $panel): bool + { + return $this->is_admin === 1; + } + + public function getFilamentName(): string + { + return $this->username ?? "XF"; + } + + public function getAttributeValue($key) + { + return $this->{$key} ?? null; + } + + public function getKey() + { + return $this->data->user_id; + } + + public function getKeyName() + { + return 'user_id'; + } } diff --git a/app/Helpers/EntryHelpers.php b/app/Helpers/EntryHelpers.php index a12bd62..0d50789 100644 --- a/app/Helpers/EntryHelpers.php +++ b/app/Helpers/EntryHelpers.php @@ -48,6 +48,8 @@ class EntryHelpers { */ public static function buildCompleteTitle( string $section, array $fields = [] ){ + $fields = array_merge( ['entry_title' => 'Untitled', 'game_name' => '', 'languages_string' => '', 'platform_name' => ''], $fields ); + return match ($section) { 'translations' => sprintf('%s (%s Translation) %s', $fields['entry_title'] ?? $fields['game_name'], $fields['languages_string'], $fields['platform_name']), 'romhacks' => sprintf('%s (%s) Romhack', $fields['entry_title'], $fields['platform_name']), diff --git a/app/Http/Controllers/DynamicLoadController.php b/app/Http/Controllers/DynamicLoadController.php index 4e8b505..bbf847a 100644 --- a/app/Http/Controllers/DynamicLoadController.php +++ b/app/Http/Controllers/DynamicLoadController.php @@ -22,6 +22,7 @@ class DynamicLoadController extends Controller } return [ + 'user_id' => $user_id, 'username' => $user->username, 'avatar_url' => $user->getAvatarUrl(), 'avatar_color' => XenForoHelpers::getAvatarColor( $user ), diff --git a/app/Http/Controllers/EntryController.php b/app/Http/Controllers/EntryController.php index 96b49a2..07b9b71 100644 --- a/app/Http/Controllers/EntryController.php +++ b/app/Http/Controllers/EntryController.php @@ -18,6 +18,11 @@ class EntryController extends Controller return view('entries.index'); } + public function section_redirect(string $section) + { + return redirect( databaseRoute( ['types' => [ $section ] ] ) ); + } + public function show(string $section, Entry $entry): View { if (!in_array($section, self::SECTION_TYPES)) @@ -48,4 +53,15 @@ class EntryController extends Controller } + public function drafts(): View + { + $drafts = Entry::where('user_id', \Auth::user()->user_id ) + ->where('state', 'draft') + ->with('game.platform', 'status') + ->orderBy('updated_at', 'desc') + ->paginate(20); + + return view('entries.drafts', compact('drafts')); + } + } diff --git a/app/Http/Controllers/ModCP/AuthorController.php b/app/Http/Controllers/ModCP/AuthorController.php new file mode 100644 index 0000000..3eac649 --- /dev/null +++ b/app/Http/Controllers/ModCP/AuthorController.php @@ -0,0 +1,72 @@ +orderBy('name') + ->tap(fn($query) => $this->applySearch($query, ['name'])) + ->paginate(30) + ->withQueryString(); + + return view('modcp.authors', [ + 'items' => $items + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255|unique:authors,name', + 'owner_user_id' => [ 'nullable', 'integer', new XfUserExists ], + 'website' => 'nullable|string|max:255', + ]); + + Author::create([ + 'name' => trim( $request->name ), + 'slug' => EntryHelpers::uniqueSlug( $request->name, Author::class ), + 'user_id' => $request->owner_user_id, + 'website' => $request->website, + ]); + + return back()->with('success', 'Author added.'); + } + + public function update(Request $request, Author $author) + { + $request->validate([ + 'name' => 'required|string|max:255|unique:authors,name,' . $author->id, + 'owner_user_id' => [ 'nullable', 'integer', new XfUserExists ], + 'website' => 'nullable|string|max:255', + ]); + + $author->update([ + 'name' => trim($request->name), + 'slug' => EntryHelpers::uniqueSlug( $request->name, Author::class, $author->id ), + 'user_id' => $request->owner_user_id, + 'website' => $request->website, + ]); + + return back()->with('success', 'Author updated.'); + } + + public function destroy(Author $author) + { + $author->delete(); + return back()->with('success', 'Author deleted.'); + } +} diff --git a/app/Http/Controllers/ModCP/GameController.php b/app/Http/Controllers/ModCP/GameController.php new file mode 100644 index 0000000..6c8b9af --- /dev/null +++ b/app/Http/Controllers/ModCP/GameController.php @@ -0,0 +1,75 @@ +orderBy('name') + ->tap(fn($query) => $this->applySearch($query, ['name'])) + ->paginate(30)->withQueryString(); + $platforms = Platform::orderBy('name')->get(); + $genres = Genre::orderBy('name')->get(); + + return view('modcp.games', [ + 'items' => $items, + 'platforms' => $platforms, + 'genres' => $genres, + ]); + + } + + public function store(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'platform_id' => 'required|integer|exists:platforms,id', + 'genre_id' => 'required|integer|exists:genres,id', + ]); + + Game::create([ + 'name' => trim($request->name), + 'platform_id' => $request->platform_id, + 'genre_id' => $request->genre_id, + 'slug' => EntryHelpers::uniqueSlug($request->name, Game::class), + ]); + + return back()->with('success', 'Game added.'); + } + + public function update(Request $request, Game $game) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'platform_id' => 'required|integer|exists:platforms,id', + 'genre_id' => 'required|integer|exists:genres,id' + ]); + + $game->update([ + 'name' => trim($request->name), + 'platform_id' => $request->platform_id, + 'genre_id' => $request->genre_id, + 'slug' => EntryHelpers::uniqueSlug($request->name, Game::class, $game->id), + ]); + + return back()->with('success', 'Game updated.'); + } + + public function destroy(Game $game) + { + $game->delete(); + return back()->with('success', 'Game deleted.'); + } +} diff --git a/app/Http/Controllers/ModCP/GenreController.php b/app/Http/Controllers/ModCP/GenreController.php new file mode 100644 index 0000000..3364a63 --- /dev/null +++ b/app/Http/Controllers/ModCP/GenreController.php @@ -0,0 +1,68 @@ +orderBy('name') + ->tap(fn($query) => $this->applySearch($query, ['name'])) + ->paginate(30) + ->withQueryString(); + + return view('modcp.resources', [ + 'items' => $items, + 'title' => 'Genres', + 'singular' => 'Genre', + 'storeRoute' => 'modcp.genres.store', + 'updateRoute' => 'modcp.genres.update', + 'destroyRoute' => 'modcp.genres.destroy' + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255|unique:genres,name', + ]); + + Genre::create([ + 'name' => trim($request->name), + 'slug' => EntryHelpers::uniqueSlug( $request->name, Genre::class ), + ]); + + return back()->with('success', 'Genre added.'); + } + + public function update(Request $request, Genre $genre) + { + $request->validate([ + 'name' => 'required|string|max:255|unique:genres,name,' . $genre->id, + ]); + + $genre->update([ + 'name' => trim($request->name), + 'slug' => EntryHelpers::uniqueSlug( $request->name, Genre::class, $genre->id ), + ]); + + return back()->with('success', 'Genre updated.'); + } + + public function destroy(Genre $genre) + { + $genre->delete(); + return back()->with('success', 'Genre deleted.'); + } +} diff --git a/app/Http/Controllers/ModCP/LanguageController.php b/app/Http/Controllers/ModCP/LanguageController.php new file mode 100644 index 0000000..ca000ee --- /dev/null +++ b/app/Http/Controllers/ModCP/LanguageController.php @@ -0,0 +1,67 @@ +orderBy('name') + ->tap(fn($query) => $this->applySearch($query, ['name'])) + ->paginate(30) + ->withQueryString(); + + return view('modcp.resources', [ + 'items' => $items, + 'title' => 'Languages', + 'singular' => 'Language', + 'storeRoute' => 'modcp.languages.store', + 'updateRoute' => 'modcp.languages.update', + 'destroyRoute' => 'modcp.languages.destroy' + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255|unique:languages,name', + ]); + + Language::create([ + 'name' => trim($request->name), + 'slug' => EntryHelpers::uniqueSlug( $request->name, Language::class ), + ]); + + return back()->with('success', 'Language added.'); + } + + public function update(Request $request, Language $language) + { + $request->validate([ + 'name' => 'required|string|max:255|unique:languages,name,' . $language->id, + ]); + + $language->update([ + 'name' => trim($request->name), + 'slug' => EntryHelpers::uniqueSlug( $request->name, Language::class, $language->id ), + ]); + + return back()->with('success', 'Language updated.'); + } + + public function destroy(Language $language) + { + $language->delete(); + return back()->with('success', 'Language deleted.'); + } +} diff --git a/app/Http/Controllers/ModCP/PlatformController.php b/app/Http/Controllers/ModCP/PlatformController.php new file mode 100644 index 0000000..2cffa06 --- /dev/null +++ b/app/Http/Controllers/ModCP/PlatformController.php @@ -0,0 +1,68 @@ +orderBy('name') + ->tap(fn($query) => $this->applySearch($query, ['name'])) + ->paginate(30) + ->withQueryString(); + + return view('modcp.resources', [ + 'items' => $items, + 'title' => 'Platforms', + 'singular' => 'Platform', + 'storeRoute' => 'modcp.platforms.store', + 'updateRoute' => 'modcp.platforms.update', + 'destroyRoute' => 'modcp.platforms.destroy' + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255|unique:platforms,name', + ]); + + Platform::create([ + 'name' => trim($request->name), + 'slug' => EntryHelpers::uniqueSlug( $request->name, Platform::class ), + ]); + + return back()->with('success', 'Platform added.'); + } + + public function update(Request $request, Platform $platform) + { + $request->validate([ + 'name' => 'required|string|max:255|unique:platforms,name,' . $platform->id, + ]); + + $platform->update([ + 'name' => trim($request->name), + 'slug' => EntryHelpers::uniqueSlug( $request->name, Platform::class, $platform->id ), + ]); + + return back()->with('success', 'Platform updated.'); + } + + public function destroy(Platform $platform) + { + $platform->delete(); + return back()->with('success', 'Platform deleted.'); + } +} diff --git a/app/Http/Controllers/ModCPController.php b/app/Http/Controllers/ModCPController.php new file mode 100644 index 0000000..b0d0d52 --- /dev/null +++ b/app/Http/Controllers/ModCPController.php @@ -0,0 +1,78 @@ + Entry::where('state', 'pending')->count(), + 'locked' => Entry::where('state', 'locked')->count(), + 'total' => Entry::count() + ]; + + if( \Auth::user()->can('is-admin') ){ + $stats['draft'] = Entry::where('state', 'draft')->count(); + $stats['hidden'] = Entry::where('state', 'hidden')->count(); + $stats['deleted'] = Entry::where('state', 'deleted')->count(); + } + + $recentDeleted = Entry::onlyTrashed()->latest('deleted_at')->limit(5)->get(); + + return view('modcp.index', compact('stats', 'recentDeleted')); + } + + public function locked() + { + $entries = Entry::where('state', 'locked') + ->with(['game.platform', 'authors']) + ->latest()->paginate(25); + + return view('modcp.entries', compact('entries'))->with('pageTitle', "Locked Entries")->with('state', 'locked'); + } + + public function draft() + { + $entries = Entry::where('state', 'draft') + ->with(['game.platform', 'authors']) + ->latest()->paginate(25); + + return view('modcp.entries', compact('entries'))->with('pageTitle', "Draft Entries")->with('state', 'draft'); + } + + public function hidden() + { + $entries = Entry::where('state', 'hidden') + ->with(['game.platform', 'authors']) + ->latest()->paginate(25); + + return view('modcp.entries', compact('entries'))->with('pageTitle', "Hidden Entries")->with('state', 'hidden'); + } + + public function deleted() + { + $entries = Entry::onlyTrashed() + ->with(['game.platform', 'authors']) + ->latest('deleted_at')->paginate(25); + return view('modcp.deleted', compact('entries')); + } + + public function restore(Entry $entry) + { + $entry->restore(); + RestoreXenForoCommentsThread::dispatch($entry->comments_thread_id); + $entry->update(['state' => 'draft']); + return back()->with('success', "Entry restored"); + } + + public function destroy(Entry $entry) + { + $entry->forceDelete(); + return back()->with('success', "Entry permanently deleted"); + } +} diff --git a/app/Http/Controllers/SubmissionController.php b/app/Http/Controllers/SubmissionController.php index 10c0787..a8ee83d 100644 --- a/app/Http/Controllers/SubmissionController.php +++ b/app/Http/Controllers/SubmissionController.php @@ -4,7 +4,9 @@ namespace App\Http\Controllers; use App\Exceptions\SubmissionException; use App\Helpers\FormHelpers; +use App\Http\Requests\StoreDraftRequest; use App\Http\Requests\StoreEntryRequest; +use App\Jobs\DeleteXenForoCommentsThread; use App\Models\Author; use App\Models\Entry; use App\Models\EntryFile; @@ -17,6 +19,7 @@ use App\Models\Modification; use App\Models\Platform; use App\Models\Status; use App\Services\SubmissionsService; +use App\Services\XenforoApiService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Storage; @@ -82,7 +85,22 @@ class SubmissionController extends Controller return view('submissions.edit', $data); } - public function store(StoreEntryRequest $request, string $section){ + public function destroy(Request $request, string $section, Entry $entry) + { + if( $entry->type !== $section ) + abort(404); + + if( $entry->comments_thread_id) + DeleteXenForoCommentsThread::dispatch( $entry->comments_thread_id ); + + $entry->delete(); + return redirect( route('entries.index') )->with('success', "Entry successfully deleted."); + } + + public function store(Request $request, string $section){ + + $request = $request->input('submit-state') === 'draft' ? app(StoreDraftRequest::class) : app(StoreEntryRequest::class); + $request->validateResolved(); try { $entry = $this->services->storeEntry($request, $section); @@ -100,8 +118,11 @@ class SubmissionController extends Controller } - public function update(StoreEntryRequest $request, string $section, Entry $entry) + public function update(Request $request, string $section, Entry $entry) { + $request = $request->input('submit-state') === 'draft' ? app(StoreDraftRequest::class) : app(StoreEntryRequest::class); + $request->validateResolved(); + try { $entry = $this->services->editEntry($request, $section, $entry); diff --git a/app/Http/Controllers/ToolsController.php b/app/Http/Controllers/ToolsController.php new file mode 100644 index 0000000..4502744 --- /dev/null +++ b/app/Http/Controllers/ToolsController.php @@ -0,0 +1,22 @@ + 'ZELDA.ips', + 'name' => "Meltin", + 'description' => 'Blablabla', + 'outputName' => 'Game...' + ]; + + return view('tools.patcher', compact('patches')); + } +} diff --git a/app/Http/Requests/StoreDraftRequest.php b/app/Http/Requests/StoreDraftRequest.php new file mode 100644 index 0000000..1cf3387 --- /dev/null +++ b/app/Http/Requests/StoreDraftRequest.php @@ -0,0 +1,26 @@ + $r === 'required' ? 'nullable' : $r, $rule); + } + + return preg_replace( + ['/\brequired_without\S*/', '/required_with\S*/', '/\brequired\b/'], + ['nullable', 'nullable', 'nullable'], + $rule + ); + }, $rules ); + + return $rules; + } +} diff --git a/app/Http/Requests/StoreEntryRequest.php b/app/Http/Requests/StoreEntryRequest.php index d7adff4..2652b6f 100644 --- a/app/Http/Requests/StoreEntryRequest.php +++ b/app/Http/Requests/StoreEntryRequest.php @@ -127,6 +127,8 @@ class StoreEntryRequest extends FormRequest if( $isEdit && $this->user()->can('moderate', $this->route('entry') ) ){ $rules['staff_comment'] = 'nullable|string'; $rules['owner_user_id'] = [ 'required', 'integer', new XfUserExists ]; + $rules['comments_thread_id'] = 'nullable|integer'; + $rules['featured'] = 'nullable|boolean'; } return $rules; diff --git a/app/Jobs/DeleteXenForoCommentsThread.php b/app/Jobs/DeleteXenForoCommentsThread.php new file mode 100644 index 0000000..46cc2e2 --- /dev/null +++ b/app/Jobs/DeleteXenForoCommentsThread.php @@ -0,0 +1,34 @@ +deleteThreadWithEntry($this->threadId); + } +} diff --git a/app/Jobs/RestoreXenForoCommentsThread.php b/app/Jobs/RestoreXenForoCommentsThread.php new file mode 100644 index 0000000..e541c3a --- /dev/null +++ b/app/Jobs/RestoreXenForoCommentsThread.php @@ -0,0 +1,34 @@ +restoreThreadWithEntry($this->threadId); + } +} diff --git a/app/Livewire/Database.php b/app/Livewire/Database.php index 304de54..fa9d1c9 100644 --- a/app/Livewire/Database.php +++ b/app/Livewire/Database.php @@ -5,10 +5,12 @@ namespace App\Livewire; use App\Models\Author; use App\Models\Entry; use App\Models\Game; +use App\Models\Genre; use App\Models\Language; use App\Models\Modification; use App\Models\Platform; use App\Models\Status; +use Livewire\Attributes\Url; use Livewire\Component; use Livewire\WithPagination; @@ -20,18 +22,21 @@ class Database extends Component * entry_title search * @var string */ + #[Url(as: 's',except: '')] public string $search = ''; /** * type filter. * @var array */ + #[Url(except:[])] public array $types = []; /** * Games IDs filter. * @var array */ + #[Url(except:[])] public array $games = []; /** @@ -44,24 +49,35 @@ class Database extends Component * Platform IDs filter. * @var array */ + #[Url(except:[])] public array $platforms = []; + /** + * Genre IDs filter. + * @var array + */ + #[Url(except:[])] + public array $genres = []; + /** * Status IDs filter. * @var array */ + #[Url(except:[])] public array $statuses = []; /** * Authors IDs filter. * @var array */ + #[Url(except:[])] public array $authors = []; /** * Authors mode and/or. * @var string */ + #[Url(except:'or')] public string $authorsMode = 'or'; /** @@ -74,36 +90,42 @@ class Database extends Component * Languages IDs filter. * @var array */ + #[Url(except:[])] public array $languages = []; /** * Languages mode and/or * @var string */ + #[Url(except:'or')] public string $languagesMode = 'or'; /** * Modifications IDs filter. * @var array */ + #[Url(except:[])] public array $modifications = []; /** * Modifications mode and/or. * @var string */ + #[Url(except:'or')] public string $modificationsMode = 'or'; /** * Sort by field. * @var string */ + #[Url(as: 'sort',except: 'created_at')] public string $sortBy = 'created_at'; /** * asc/desc * @var string */ + #[Url(as: 'dir',except: 'desc')] public string $sortDir = 'desc'; /** @@ -134,6 +156,7 @@ class Database extends Component public function updatedTypes(): void { $this->resetPage(); $this->dispatch('filters-updated'); } public function updatedGames(): void { $this->resetPage(); $this->dispatch('filters-updated'); } public function updatedPlatforms(): void { $this->resetPage(); $this->dispatch('filters-updated'); } + public function updatedGenres(): void { $this->resetPage(); $this->dispatch('filters-updated'); } public function updatedStatuses(): void { $this->resetPage(); $this->dispatch('filters-updated'); } public function updatedAuthors(): void { $this->resetPage(); $this->dispatch('filters-updated'); } public function updatedAuthorsMode(): void { $this->resetPage(); $this->dispatch('filters-updated'); } @@ -145,7 +168,7 @@ class Database extends Component public function clearFilters(): void { $this->reset([ - 'search', 'types', 'platforms', 'statuses', 'authors', 'authorsMode', 'languages', 'languagesMode', 'modifications', 'modificationsMode' + 'search', 'types', 'platforms', 'genres', 'statuses', 'authors', 'authorsMode', 'languages', 'languagesMode', 'modifications', 'modificationsMode' ]); $this->resetPage(); } @@ -165,7 +188,7 @@ class Database extends Component private function buildQuery() { $query = Entry::query()->published()->with([ - 'game.platform', 'status', 'authors', 'languages' + 'game.platform', 'game.genre', 'status', 'authors', 'languages' ]); if( $this->search ) { @@ -186,6 +209,12 @@ class Database extends Component }); } + if( $this->genres ) { + $query->where(function($q) { + $q->whereHas('game', fn($q2) => $q2->whereIn('genre_id', $this->genres) ); + }); + } + if( $this->games ){ $query->whereIn('game_id', $this->games); } @@ -233,6 +262,7 @@ class Database extends Component 'entries' => $this->buildQuery()->paginate(self::PAGINATION), 'allGames' => Game::orderBy('name')->get(), 'allPlatforms' => Platform::orderBy('name')->get(), + 'allGenres' => Genre::orderBy('name')->get(), 'allStatuses' => Status::orderBy('name')->get(), 'allAuthors' => Author::orderBy('name')->get(), 'allLanguages' => Language::orderBy('name')->get(), diff --git a/app/Livewire/XfUserSelector.php b/app/Livewire/XfUserSelector.php index 02baafd..8c6fbd5 100644 --- a/app/Livewire/XfUserSelector.php +++ b/app/Livewire/XfUserSelector.php @@ -14,7 +14,7 @@ class XfUserSelector extends Component public ?int $selected = null; public ?string $selectedUsername = null; - public function mount( ?int $initialUserId ){ + public function mount( ?int $initialUserId = null ){ if( $initialUserId ){ $user = DB::connection('xenforo')->table('user')->where('user_id', $initialUserId)->first(); if( $user ){ diff --git a/app/Models/Author.php b/app/Models/Author.php index 3ee4c1d..357367d 100644 --- a/app/Models/Author.php +++ b/app/Models/Author.php @@ -2,11 +2,29 @@ namespace App\Models; +use App\Auth\XenForoUser; +use App\Services\XenforoService; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; class Author extends Model { protected $fillable = [ 'name', 'slug', 'user_id', 'website' ]; + + public function entries(): BelongsToMany + { + return $this->belongsToMany(Entry::class, 'entry_authors'); + } + + public function user(): ?XenForoUser + { + if( !$this->user_id ) + return null; + + return app(XenforoService::class)->getXfUser($this->user_id); + } + } diff --git a/app/Models/EntryFile.php b/app/Models/EntryFile.php index b21fdaf..c738572 100644 --- a/app/Models/EntryFile.php +++ b/app/Models/EntryFile.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; class EntryFile extends Model { protected $fillable = [ - 'entry_id', 'filename', 'filepath', 'favorite_server', 'favorite_at', 'filesize', 'state', 'file_uuid' + 'entry_id', 'filename', 'filepath', 'favorite_server', 'favorite_at', 'filesize', 'state', 'file_uuid', 'online_patcher', 'secondary_online_patcher', ]; protected $casts = [ diff --git a/app/Models/Game.php b/app/Models/Game.php index c6d4b1c..db4b115 100644 --- a/app/Models/Game.php +++ b/app/Models/Game.php @@ -21,4 +21,9 @@ class Game extends Model { return $this->belongsTo(Genre::class); } + + public function entries(): Game|\Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Entry::class); + } } diff --git a/app/Models/Genre.php b/app/Models/Genre.php index 999da8a..c2d3551 100644 --- a/app/Models/Genre.php +++ b/app/Models/Genre.php @@ -3,8 +3,15 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; class Genre extends Model { protected $fillable = [ 'name', 'slug' ]; + + public function games(): HasMany + { + return $this->hasMany(Game::class); + } } diff --git a/app/Models/Language.php b/app/Models/Language.php index e139057..a2de75b 100644 --- a/app/Models/Language.php +++ b/app/Models/Language.php @@ -3,8 +3,15 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; class Language extends Model { protected $fillable = [ 'name', 'slug' ]; + + public function entries(): BelongsToMany + { + return $this->belongsToMany(Entry::class, 'entry_languages'); + } } diff --git a/app/Models/Platform.php b/app/Models/Platform.php index ce9b92b..cac5bf3 100644 --- a/app/Models/Platform.php +++ b/app/Models/Platform.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; class Platform extends Model { @@ -14,4 +15,13 @@ class Platform extends Model 'name', 'slug', 'short_name' ]; + public function games(): HasMany + { + return $this->hasMany(Game::class); + } + + public function entries(): HasMany + { + return $this->hasMany(Entry::class); + } } diff --git a/app/Policies/EntryPolicy.php b/app/Policies/EntryPolicy.php index e78eccd..4b82c08 100644 --- a/app/Policies/EntryPolicy.php +++ b/app/Policies/EntryPolicy.php @@ -130,7 +130,7 @@ class EntryPolicy return false; } - public function skipQueue(User $user, Entry $entry): bool + public function skipQueue(User $user, ?Entry $entry = null): bool { return $user->_can( 'romhackplaza', 'canSubmitEntryInPublished' ); } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4879d47..7a1413e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,8 +3,10 @@ namespace App\Providers; use App\Auth\XenForoGuard; +use App\Auth\XenForoUser; use App\Policies\TempFilePolicy; use App\Services\TemporaryFileService; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -28,5 +30,12 @@ class AppServiceProvider extends ServiceProvider }); \Gate::policy(TemporaryFileService::class, TempFilePolicy::class ); + + Gate::define('is-admin', function (XenForoUser $user) { + return $user->is_admin === 1; + }); + Gate::define('is-mod', function (XenForoUser $user) { + return $user->is_moderator === 1; + }); } } diff --git a/app/Providers/Filament/ManagePanelProvider.php b/app/Providers/Filament/ManagePanelProvider.php index 0135210..1a1185d 100644 --- a/app/Providers/Filament/ManagePanelProvider.php +++ b/app/Providers/Filament/ManagePanelProvider.php @@ -27,7 +27,7 @@ class ManagePanelProvider extends PanelProvider ->default() ->id('manage') ->path('manage') - ->login() + ->authGuard('xenforo') ->colors([ 'primary' => Color::Amber, ]) diff --git a/app/Services/SubmissionsService.php b/app/Services/SubmissionsService.php index a11dc40..11ad10e 100644 --- a/app/Services/SubmissionsService.php +++ b/app/Services/SubmissionsService.php @@ -17,6 +17,7 @@ use App\Models\Genre; use App\Models\Language; use App\Models\Modification; use App\Models\Platform; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; @@ -28,9 +29,9 @@ class SubmissionsService { /** * Request for store/edit. - * @var StoreEntryRequest|null + * @var Request|null */ - private ?StoreEntryRequest $request = null; + private ?Request $request = null; /** * Section for store/edit. @@ -72,7 +73,9 @@ class SubmissionsService { 'done' => true, 'error' => null, 'uuid' => $uuid, - 'state' => $file->state + 'state' => $file->state, + 'meta_online_patcher' => $file->online_patcher, + 'meta_secondary_online_patcher' => $file->secondary_online_patcher, ]; $file = Cache::get("uploaded_file_{$uuid}"); @@ -86,7 +89,9 @@ class SubmissionsService { 'done' => true, 'error' => null, 'uuid' => $uuid, - 'state' => $file['state'] + 'state' => $file['state'], + 'meta_online_patcher' => false, + 'meta_secondary_online_patcher' => false, ]; return null; @@ -102,7 +107,7 @@ class SubmissionsService { * @throws SubmissionException * @throws \Throwable */ - public function storeEntry( StoreEntryRequest $request, string $section ){ + public function storeEntry( Request $request, string $section ){ // STEP 1 : Prepare basic fields. @@ -188,23 +193,30 @@ class SubmissionsService { $this->Step13_CreateCommentsThread( $entry ); // Step 14: Refresh XF count. - XenForoHelpers::updateEntriesCount( $entry->user_id ); + if( $entry->state !== 'draft') + XenForoHelpers::updateEntriesCount( $entry->user_id ); return $entry; } /** - * @return int + * @return null|int * * @throws SubmissionException */ - private function Step2_CreateAndReturnGameId(): int { + private function Step2_CreateAndReturnGameId(): ?int { // Already existing game. if( $this->request->input('game_id') ) return $this->request->input('game_id'); + // No fields like a draft. + if( !$this->request->input('new-game-title') && + !$this->request->input('new-game-platform') && + !$this->request->input('new-game-genre') ) + return null; + // Need to create a game. $game = $this->createGameFromFormFields(); @@ -244,14 +256,14 @@ class SubmissionsService { $fields['entry_title'] = $this->request->input('entry_title') ?? null; if( section_must_be( [ 'homebrew', 'translations' ], $this->section ) && $gameId ){ - $fields['game_name'] = Game::find( $gameId )->name; + $fields['game_name'] = $gameId ? Game::find( $gameId )->name : null; } if( section_must_be( 'translations', $this->section ) ) { $fields['languages_string'] = Language::whereIn('id', $this->request->input('languages', []))->pluck('name')->implode(', '); } if( section_must_be(['romhacks', 'translations', 'homebrew', 'lua-scripts', 'tutorials'], $this->section ) ) { // TODO: Add single platform ID compatibility. - $fields['platform_name'] = Game::find( $gameId )->platform->name; + $fields['platform_name'] = $gameId ? Game::find( $gameId )->platform->name : null; } return EntryHelpers::buildCompleteTitle( $this->section, $fields ); @@ -268,7 +280,7 @@ class SubmissionsService { if( !$uuidData ) $uuidData = $this->request->input('files_uuid', [] ); - foreach ( $uuidData as $uuid ) { + foreach ( $uuidData ?? [] as $uuid ) { $fileData = Cache::pull("uploaded_file_{$uuid}"); if( !$fileData ) throw new SubmissionException( "File {$uuid} has expired. Please delete all your files and retry. If it's an edition, delete all your new files and retry." ); @@ -294,7 +306,7 @@ class SubmissionsService { */ private function Step8_SaveHashes( int $entryId ): void { - foreach ( $this->request->input('hashes', [] ) as $hash ) { + foreach ( $this->request->input('hashes', [] ) ?? [] as $hash ) { if( !isset($hash['filename'], $hash['hash_crc32'], $hash['hash_sha1'], $hash['verified']) ) { continue; } @@ -320,7 +332,7 @@ class SubmissionsService { // TODO: Code fragment to be replaced by edit version. // Existing authors. - foreach ( $this->request->input('authors', [] ) as $authorId ) { + foreach ( $this->request->input('authors', [] ) ?? [] as $authorId ) { $author = Author::find( $authorId ); if( !$author ) throw new SubmissionException( "Author {$authorId} does not exist." ); @@ -328,7 +340,7 @@ class SubmissionsService { } // New Authors - foreach ( $this->request->input('new-authors', [] ) as $authorName ) { + foreach ( $this->request->input('new-authors', [] ) ?? [] as $authorName ) { $authorName = trim( $authorName ); if( $authorName === '' ) continue; @@ -352,7 +364,7 @@ class SubmissionsService { // TODO: Replace by edit version - foreach ( $this->request->input('modifications', [] ) as $modificationId ) { + foreach ( $this->request->input('modifications', [] ) ?? [] as $modificationId ) { $modification = Modification::find( $modificationId ); if( !$modification ) throw new SubmissionException( "Modification {$modificationId} does not exist." ); @@ -370,7 +382,7 @@ class SubmissionsService { { // TODO: Replace by edit version. - foreach ( $this->request->input('languages', [] ) as $languageId ) { + foreach ( $this->request->input('languages', [] ) ?? [] as $languageId ) { $language = Language::find( $languageId ); if( !$language ) throw new SubmissionException( "Language {$languageId} does not exist." ); @@ -381,7 +393,7 @@ class SubmissionsService { private function Step12a_PrepareGalleryImages( Entry $entry ): void { - foreach ( $this->request->input('gallery', [] ) as $imagePath ) { + foreach ( $this->request->input('gallery', [] ) ?? [] as $imagePath ) { EntryGallery::create([ 'entry_id' => $entry->id, 'image' => $imagePath, @@ -396,6 +408,10 @@ class SubmissionsService { */ private function Step12b_MoveMainImage( Entry $entry ): void { $mainImage = $entry->main_image; + + if( !$mainImage ) + return; + $newPath = 'entries/main-images/' . basename($mainImage); if( !Storage::disk('public')->move($mainImage, $newPath) ) @@ -406,7 +422,7 @@ class SubmissionsService { private function Step12c_SaveGalleryImages( Entry $entry ): void { - foreach ( $entry->gallery as $galleryItem ) { + foreach ( $entry->gallery ?? [] as $galleryItem ) { $newPath = 'entries/gallery-images/' . $entry->id . '/' . basename($galleryItem->image); if( !Storage::disk('public')->move($galleryItem->image, $newPath) ) @@ -416,7 +432,7 @@ class SubmissionsService { } } - public function editEntry( StoreEntryRequest $request, string $section, Entry $entry ): Entry + public function editEntry(Request $request, string $section, Entry $entry ): Entry { // STEP 1: Prepare basic fields and keep in save some others fields. @@ -477,6 +493,8 @@ class SubmissionsService { 'youtube_link' => $this->request->input('youtube_video'), 'user_id' => $user_id, 'complete_title' => $completeTitle, + 'comments_thread_id' => $this->request->input('comments_thread_id'), + 'featured' => $this->request->input('featured'), ]; if( \Auth::user()->can('moderate', $this->entry) ){ @@ -505,9 +523,6 @@ class SubmissionsService { // STEP 11: Prepare new gallery images and prepare deletion of others ones. $galleryPaths = $this->eStep11a_UpdateGalleryImages(); - // STEP 13: Try to create comments area if it doesn't exist. - $this->Step13_CreateCommentsThread( $this->entry ); - return $this->entry; }); @@ -519,9 +534,14 @@ class SubmissionsService { $this->eStep11c_UpdateGalleryImages( $galleryPaths ); // STEP 12: Refresh XF count. - if( $oldUserId ) - XenForoHelpers::updateEntriesCount( $oldUserId ); - XenForoHelpers::updateEntriesCount( $entry->user_id ); + if( $entry->state !== 'draft' ) { + if ($oldUserId) + XenForoHelpers::updateEntriesCount($oldUserId); + XenForoHelpers::updateEntriesCount($entry->user_id); + } + + // STEP 13: Try to create comments area if it doesn't exist. + $this->Step13_CreateCommentsThread( $this->entry ); return $entry; } @@ -550,6 +570,12 @@ class SubmissionsService { } + // In draft. + if( !$this->request->input('new-game-title') && + !$this->request->input('new-game-platform') && + !$this->request->input('new-game-genre') ) + return $this->entry->game_id; + // Need to create a game. $game = $this->createGameFromFormFields(); @@ -562,8 +588,8 @@ class SubmissionsService { */ private function eStep6_UpdateEntryFiles(int $entryId ): void { - $requestUuids = $this->request->input('files_uuid', []); - $requestStates = $this->request->input('files_state', []); + $requestUuids = $this->request->input('files_uuid', []) ?? []; + $requestStates = $this->request->input('files_state', []) ?? []; $existingUuids = EntryFile::where( 'entry_id', $entryId )->pluck('file_uuid')->toArray(); $needDeletion = array_diff( $existingUuids, $requestUuids ); @@ -626,7 +652,7 @@ class SubmissionsService { private function eStep8_UpdateAuthors(): void { $syncAuthorsId = []; - $requestAuthorsId = $this->request->input('authors', [] ); + $requestAuthorsId = $this->request->input('authors', [] ) ?? []; if( !empty( $requestAuthorsId ) ){ $valid = Author::whereIn( 'id', $requestAuthorsId )->pluck('id')->toArray(); @@ -638,7 +664,7 @@ class SubmissionsService { $syncAuthorsId = array_merge( $syncAuthorsId, $requestAuthorsId ); } - foreach ( $this->request->input('new-authors', [] ) as $authorName ) { + foreach ( $this->request->input('new-authors', [] ) ?? [] as $authorName ) { $authorName = trim($authorName); if ($authorName === '') continue; @@ -660,7 +686,7 @@ class SubmissionsService { */ private function eStep9_UpdateRomhacksModifications(): void { - $requestModifications = $this->request->input('modifications', [] ); + $requestModifications = $this->request->input('modifications', [] ) ?? []; if( !empty( $requestModifications ) ){ $valid = Modification::whereIn( 'id', $requestModifications )->pluck('id')->toArray(); @@ -681,7 +707,7 @@ class SubmissionsService { */ private function eStep10_UpdateLanguages(): void { - $requestLanguages = $this->request->input('languages', [] ); + $requestLanguages = $this->request->input('languages', [] ) ?? []; if( !empty( $requestLanguages ) ){ $valid = Language::whereIn( 'id', $requestLanguages )->pluck('id')->toArray(); if( count( $valid ) !== count( $requestLanguages ) ){ @@ -695,7 +721,7 @@ class SubmissionsService { private function eStep11a_UpdateGalleryImages(): array { - $requestGallery = $this->request->input('gallery', [] ); + $requestGallery = $this->request->input('gallery', [] ) ?? []; $existingGalleryPaths = $this->entry->gallery->pluck('image')->toArray(); $needDeletion = array_diff( $existingGalleryPaths, $requestGallery ); @@ -723,6 +749,12 @@ class SubmissionsService { if( $currentMainImagePath === $oldMainImagePath ) return; + if( !$currentMainImagePath ) { + if( $oldMainImagePath && Storage::disk('public')->exists($oldMainImagePath) ) + Storage::disk('public')->delete($oldMainImagePath); + return; + } + $newPath = 'entries/main-images/' . basename( $currentMainImagePath ); if( !Storage::disk('public')->move( $currentMainImagePath, $newPath ) ){ @@ -755,6 +787,9 @@ class SubmissionsService { private function Step13_CreateCommentsThread( Entry $entry ): void { + if( $entry->state !== 'published' ) + return; + if( !$entry->comments_thread_id ) CreateXenForoCommentsThread::dispatch( $entry ); // app(XenforoApiService::class)->createCommentsThread( $entry ); diff --git a/app/Services/XenforoApiService.php b/app/Services/XenforoApiService.php index 320243c..f1ce2ef 100644 --- a/app/Services/XenforoApiService.php +++ b/app/Services/XenforoApiService.php @@ -48,6 +48,19 @@ class XenforoApiService { return $response->json(); } + private function delete(string $endpoint, ?int $customUserId = null, array $data = [] ): mixed + { + $response = Http::withHeaders([ + 'XF-Api-Key' => $this->apiKey, + 'XF-Api-User' => $customUserId ?? $this->superUserId, + ])->delete("{$this->apiUrl}/{$endpoint}", $data); + + if( !$response->ok() ) + return null; + + return $response->json(); + } + public function getUserAlerts(int $userId): mixed { if( app(XenforoService::class)->getXfUser($userId)?->alerts_unviewed > 0 ) { @@ -121,4 +134,14 @@ class XenforoApiService { return $response['success'] ?? false; } + public function deleteThreadWithEntry(int $threadId): bool + { + return (bool) $this->delete( "threads/{$threadId}", data: ['reason' => "Deletion with entry." ] ); + } + + public function restoreThreadWithEntry(int $threadId): bool + { + return (bool) $this->post("threads/{$threadId}/undelete" ); + } + } diff --git a/app/Traits/ModCPSearch.php b/app/Traits/ModCPSearch.php new file mode 100644 index 0000000..a1bc3dd --- /dev/null +++ b/app/Traits/ModCPSearch.php @@ -0,0 +1,23 @@ +where(function ($query) use ($columns, $search) { + foreach ($columns as $i => $column) { + $method = $i === 0 ? 'where' : 'orWhere'; + $query->{$method}($column, 'LIKE', "%{$search}%"); + } + }); + } + +} diff --git a/app/View/Components/ModCPSearch.php b/app/View/Components/ModCPSearch.php new file mode 100644 index 0000000..5382494 --- /dev/null +++ b/app/View/Components/ModCPSearch.php @@ -0,0 +1,29 @@ + [], + 'platforms' => [], + 'genres' => [], + 'games' => [], + 'statuses' => [], + 'authors' => [], + 'authorsMode' => 'or', + 'languages' => [], + 'languagesMode' => 'or', + 'modifications' => [], + 'modificationsMode' => 'or', + 'sort' => 'created_at', + 'dir' => 'desc', + 's' => '' + ]; + + $query = array_filter( + array_merge($defaults, $params), + fn($v,$k) => match(true){ + is_array($v) => !empty($v), + in_array($k, ['authorsMode', 'languagesMode', 'modificationsMode']) => $v !== 'or', + $k === 'sort' => $v !== 'created_at', + $k === 'dir' => $v !== 'desc', + default => $v !== '', + }, + ARRAY_FILTER_USE_BOTH + ); + + return route('entries.index', $query ); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 50ee1ec..3928513 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Http\Request; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -16,6 +17,10 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->alias([ 'xf.auth' => \App\Http\Middleware\CheckXenForoPermissions::class, ]); + $middleware->redirectGuestsTo(function(Request $request): void { + if( $request->is('manage*')) + abort(403); + }); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/config/menu.php b/config/menu.php index 669e849..9941ab2 100644 --- a/config/menu.php +++ b/config/menu.php @@ -23,6 +23,12 @@ return [ 'icon' => 'gavel', 'route' => 'queue.index' ], + [ + 'name' => "My Drafts", + 'icon' => 'scissors', + 'route' => 'entries.drafts', + 'condition' => (fn() => !\Auth::guest()) + ] ] ], 'community' => [ @@ -33,6 +39,11 @@ return [ 'icon' => 'message-circle', 'xf_route' => '' ], + [ + 'name' => 'Clubs', + 'icon' => 'balloon', + 'xf_route' => 'clubs' + ], [ 'name' => 'Discord', 'icon' => 'messages-square', diff --git a/database/migrations/2026_06_04_083346_make_entries_fields_draft_compatible.php b/database/migrations/2026_06_04_083346_make_entries_fields_draft_compatible.php new file mode 100644 index 0000000..a18a81b --- /dev/null +++ b/database/migrations/2026_06_04_083346_make_entries_fields_draft_compatible.php @@ -0,0 +1,28 @@ +string('title')->nullable()->change(); + $table->longText('description')->nullable()->change(); + $table->string('slug')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/database/migrations/2026_06_05_163235_add_online_patcher_fields_to_files.php b/database/migrations/2026_06_05_163235_add_online_patcher_fields_to_files.php new file mode 100644 index 0000000..5b18fcc --- /dev/null +++ b/database/migrations/2026_06_05_163235_add_online_patcher_fields_to_files.php @@ -0,0 +1,30 @@ +boolean('online_patcher')->default(false); + $table->boolean('secondary_online_patcher')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('entry_files', function (Blueprint $table) { + $table->dropColumn('online_patcher'); + $table->dropColumn('secondary_online_patcher'); + }); + } +}; diff --git a/extra.less b/extra.less index ceef0c1..ea5932a 100644 --- a/extra.less +++ b/extra.less @@ -37,6 +37,8 @@ ul { list-style-type: square; } +[x-cloak] {display: none !important;} + /* File: resources/css/base/variables.css */ :root { @@ -267,6 +269,13 @@ ul { border-bottom: 1px solid var(--border); padding-bottom: 10px; } +.\$block-success { + background-color: var(--success); + border: 1px solid var(--success); + color: var(--text); + padding: 20px; + margin-bottom: 20px; +} .\$block-error { background-color: var(--error); border: 1px solid var(--error); @@ -381,6 +390,12 @@ ul { animation: spin 1s infinite linear; } +.\$search-button { + background: none; + border: none; + cursor: pointer; +} + /* File: resources/css/components/database.css */ .\$filter-bar { @@ -1359,6 +1374,75 @@ ul { color: var(--text2); white-space: nowrap; } +.\$upload-item-actions { + display: flex; + flex-direction: row; + gap: 15px; +} +.file-state-icon { width: 18px; height: 18px; } +.file-state-icon--public { color: var(--success); } +.file-state-icon--private { color: var(--text2); } +.file-state-icon--archived { color: var(--error); } + +.upload-item-state { display: flex; align-items: center; gap: 8px; } + +.author-search { position: relative; } + +.\$author-search-input { + display: flex; + align-items: center; + gap: 8px; + background-color: var(--bg); + border: 1px solid var(--border); + padding: 8px 12px; +} + +.\$author-search-input .\$form-input { + border: none; + padding: 0; + background: none; + flex: 1; +} + +.\$author-search-selected { + display: flex; + align-items: center; + gap: 5px; + color: var(--success); + font-size: 0.85rem; + white-space: nowrap; +} + +.\$author-search-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: var(--bg2); + border: 1px solid var(--border); + border-top: none; + z-index: 100; + max-height: 200px; + overflow-y: auto; +} + +.\$author-search-item { + display: flex; + align-items: center; + gap: 9px; + width: 100%; + padding: 9px 12px; + background: none; + border: none; + color: var(--text); + font-size: 0.88rem; + cursor: pointer; + text-align: left; + font-family: var(--typography); + transition: background-color 0.1s; +} + +.author-search-item:hover { background-color: var(--bg3); } /* File: resources/css/components/grid.css */ @@ -1721,6 +1805,196 @@ ul { } +/* File: resources/css/components/queue.css */ +.\$queue-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 15px; + padding: 80px 20px; + color: var(--text2); + font-size: 0.95rem; +} + +.\$queue-item { + background-color: var(--bg2); + border: 1px solid var(--border); + border-left-width: 4px; + margin-bottom: 20px; + padding: 20px; +} + +.\$queue-item--pending { + border-left-color: var(--rhpz-orange); +} +.\$queue-item--rejected { + position: relative; + overflow: hidden; + border-left-color: var(--error); + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + height: 2px; + background-color: var(--error); + width: var(--reject-progress, 100%); + opacity: 0.5; + transition: width 0.3s; + } +} + +.\$queue-item-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 15px; + margin-bottom: 20px; +} + +.\$queue-item-title { + font-size: 1.15rem; + font-weight: 600; + color: var(--text); + margin-bottom: 6px; +} + +.\$queue-item-meta { + font-size: 0.85rem; + color: var(--text2); + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.\$queue-item-actions-header { + display: flex; + gap: 8px; + flex-shrink: 0; +} + +.\$timeline-container { + padding: 15px 20px; + background-color: var(--bg); + border: 1px solid var(--border); + margin-bottom: 20px; +} + +.\$timeline { + display: flex; + justify-content: space-between; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 15px; + left: 30px; + right: 30px; + height: 2px; + background-color: var(--border); + z-index: 0; + } + +} + +.\$timeline-step { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + z-index: 1; + width: 100px; +} + +.\$timeline-dot { + width: 32px; + height: 32px; + border-radius: 50%; + background-color: var(--bg); + border: 2px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + color: var(--text2); + margin-bottom: 10px; + transition: all 0.2s; +} + +.\$timeline-step--active .\$timeline-dot { + border-color: var(--rhpz-orange); + background-color: var(--rhpz-orange); + color: #111; +} + +.\$timeline-step--validated .\$timeline-dot { + border-color: var(--success); + background-color: var(--success); + color: #111; +} + +.\$timeline-step--rejected .\$timeline-dot { + border-color: var(--error); + background-color: var(--error); + color: #fff; +} + +.\$timeline-label { + font-size: 0.78rem; + text-align: center; + color: var(--text2); + font-weight: 500; +} + +.timeline-step--active .timeline-label { color: var(--rhpz-orange); font-weight: 600; } +.timeline-step--validated .timeline-label { color: var(--success); } +.timeline-step--rejected .timeline-label { color: var(--error); } + +.\$queue-reject-reason { + display: flex; + gap: 10px; + align-items: flex-start; + padding: 12px 15px; + background-color: rgba(229, 115, 115, 0.08); + border-left: 2px solid var(--error); + color: var(--text); + font-size: 0.88rem; + margin-bottom: 15px; + line-height: 1.5; +} + +.\$queue-mod-zone { + border-top: 1px solid var(--border); + padding-top: 15px; + margin-top: 5px; + display: flex; + flex-direction: column; + gap: 15px; +} + +.\$queue-mod-separator { + border-top: 1px solid var(--border); +} + +.\$queue-mod-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 10px; +} + +.\$queue-reject-form { + margin-top: 12px; + padding: 15px; + background-color: var(--bg3); + border: 1px solid var(--border); +} + + + /* File: resources/css/components/settings.css */ .\$settings-dropdown { position: absolute; @@ -1946,7 +2220,7 @@ ul { /* File: resources/css/layout/entry.css */ -#entry-container { +#entry-container, #comments-section, #reviews-section { background-color: var(--bg2); border: 1px solid var(--border); display: flex; @@ -2054,6 +2328,7 @@ ul { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 15px; + margin-bottom: 30px; } .\$entry-gallery-item { @@ -2064,10 +2339,81 @@ ul { align-items: center; justify-content: center; cursor: pointer; + overflow: hidden; transition: border-color 0.2s; &:hover { border-color: var(--rhpz-orange); + + img { + transform: scale(1.05); + } + } + + img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; + } + } +} + +.\$gallery-modal { + position: fixed; + z-index: 3000; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + + .\$gallery-modal-content { + max-width: 90%; + max-height: 90%; + position: relative; + + img { + max-width: 100%; + max-height: 90vh; + object-fit: contain; + border: 2px solid var(--border); + background-color: var(--bg); + box-shadow: 0 10px 25px rgba(0,0,0,0.5); + } + } + + .\$gallery-modal-close { + position: absolute; + top: 20px; + right: 30px; + color: #fff; + font-size: 40px; + font-weight: bold; + cursor: pointer; + user-select: none; + + &:hover { + color: var(--rhpz-orange); + } + } + + .\$gallery-modal-video { + width: 90%; + max-width: 960px; + aspect-ratio: 16/9; + box-shadow: 0 10px 30px rgba(0,0,0,0.6); + border: 1px solid var(--border); + background-color: #000; + + iframe { + width: 100%; + height: 100%; + border: none; + display: block; } } } @@ -2077,6 +2423,186 @@ ul { color: var( --text2 ); } +.\$comment-block { + display: flex; + gap: 16px; + padding: 20px 0; + border-bottom: 1px solid var(--border); + + &:last-child { + border-bottom: none; + } + + .\$comment-avatar { + flex-shrink: 0; + width: 48px; + height: 48px; + border-radius: 50%; + overflow: hidden; + background-color: var(--bg4); + border: 1px solid var(--border); + + img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } + } + + .\$comment-content { + flex: 1; + min-width: 0; + + .\$comment-meta { + font-size: 0.88rem; + color: var(--text2); + margin-bottom: 6px; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + + .\$comment-author { + font-weight: 600; + color: var(--text); + text-decoration: none; + transition: color 0.2s; + + &:hover { + color: var(--rhpz-orange); + } + } + + .\$comment-date { + color: var(--text2); + } + + .\$comment-separator { + color: var(--border); + user-select: none; + } + } + + .\$comment-body { + font-size: 0.95rem; + color: var(--text); + line-height: 1.5; + word-wrap: break-word; + + p { + margin-bottom: 10px; + &:last-child { margin-bottom: 0; } + } + + a { + color: var(--rhpz-orange); + &:hover { + color: var(--rhpz-orange-hover); + text-decoration: underline; + } + } + + blockquote, .\$bbCodeBlock-blockquote { + background-color: var(--bg); + border-left: 3px solid var(--info); + padding: 12px 16px; + margin: 12px 0; + font-style: italic; + color: var(--text2); + } + + code { + font-family: monospace; + background-color: var(--bg3); + border: 1px solid var(--border); + padding: 2px 5px; + font-size: 0.9rem; + } + } + } +} + +.\$comments-empty { + text-align: center; + padding: 40px 20px; + color: var(--text2); + font-style: italic; + background-color: var(--bg); + border: 1px dashed var(--border); +} + +.\$entry-video-section { + margin-bottom: 30px; +} + +.\$video-thumbnail-wrapper { + position: relative; + width: 100%; + max-width: 500px; + aspect-ratio: 16/9; + background-color: #000; + border: 1px solid var(--border); + cursor: pointer; + overflow: hidden; + border-radius: 4px; + + img { + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0.8; + transition: transform 0.3s ease, opacity 0.3s ease; + } + + .\$play-trigger { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 65px; + height: 65px; + background-color: rgba(0, 0, 0, 0.7); + border: 2px solid #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 1.5rem; + transition: background-color 0.2s, transform 0.2s ease-out; + + i { margin-left: 4px; } + } + + &:hover { + img { + transform: scale(1.03); + opacity: 1; + } + .\$play-trigger { + background-color: var(--rhpz-orange); + border-color: var(--rhpz-orange); + transform: translate(-50%, -50%) scale(1.1); + box-shadow: 0 0 15px rgba(255, 115, 0, 0.5); + } + } +} + +.\$entry-submission-byline { + font-size: 0.85rem; + color: var(--text2); + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 15px; + + i { + vertical-align: middle; + margin-right: 4px; + } +} + /* File: resources/css/layout/menu.css */ #menu { diff --git a/public/ZELDA.ips b/public/ZELDA.ips new file mode 100644 index 0000000000000000000000000000000000000000..53748dae516c923f6728172506b2917947bee4fa GIT binary patch literal 1571 zcmZ9MU2NM_6vvO7Hl}Ifq$y2Y^5M|lZXgvF3Dl*nYj4nw3B(o&9`?3q>oTdJw$dI} zAk+nwzVM;((QSxIRNIOMk9x7# zNT90gD>bt=Gc@%5$Vjc0|KyrEJL}EE8;j7;-2>B~GS^y~`Gg<<8w?6KWJxR(EXxx6 z2HrWYE~EiHEnKR5DsLHEbx+mm$VcC8&CEcKt;=OcE{d7ACQVCGWO?fJZ<1jZ#>vIw!FN2=T4=nd!+{r4FJ=$nkV=~67`sc(6RTqT{2Z;HJOQA%3H}*CXet} z3zcg1s;=wvYj+C<4l^#^^!pnDSeB(}3r>6i8-YWxEwZIjsh~_ZP;b)~n}+G|TkteB z`ng~bJS@7v9>|KN$cijTQx7K~l?MH;ku6W30o^d}>4siT!8E!u(yCzqfFD?IG}%9S z0{bTnD%SRCS$G!Xvj0qq11SgrI0b-HfLpx=Jw`syUD+A>a4u& z@xW{dGW9M?PM7!C*}n3idp1@Hggf;@g;Y;1~AB$zz|0b6YSSr@}B9vwx2 z5BD%%XWrB_Q`5kup=tC7F2%g6s(BGy>LpFnN;UAwI?GV)% zP6L-BPY3#UnDlzRnpOapT4}YqTEhJfbM}}5m)g2%u26q2|EQU_ujAZd0RvHgE@@p@ zkzF4peSW+2_73SfjpuT*q!zrWcR6L}$%9MVJ+A+U>xE2+&K<6p<~ru1ImD+0I(L}Q z=V|HS(&XZqi3;}9;oJ#ex?%1V-t}I%aL&H+SH>VhI{D;Nq>F10_?{*s9kFoK&&A{L z*L_HN*zwV0>Au0-sgv(2vOHj4E`j4hAy3i+90&JbffW5%1bsd{|F9cBLPB#KkGvpo zY+JfZ>H?1WoY0WW7f%Ay%giOHJ=;O;xpr#Lw^I9MfZDH`sGVnQYd}kPZ#3A0|446? zxpWcQUfnN|VQHWA(jYk~lh>r-!G3137b76951J literal 0 HcmV?d00001 diff --git a/public/rom-patcher-js/RomPatcher.js b/public/rom-patcher-js/RomPatcher.js new file mode 100644 index 0000000..6dc2985 --- /dev/null +++ b/public/rom-patcher-js/RomPatcher.js @@ -0,0 +1,412 @@ +/* +* Rom Patcher JS core +* A ROM patcher/builder made in JavaScript, can be implemented as a webapp or a Node.JS CLI tool +* By Marc Robledo https://www.marcrobledo.com +* Sourcecode: https://github.com/marcrobledo/RomPatcher.js +* License: +* +* MIT License +* +* Copyright (c) 2016-2025 Marc Robledo +* +* 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. +*/ + +const RomPatcher = (function () { + const TOO_BIG_ROM_SIZE = 67108863; + + const HEADERS_INFO = [ + { extensions: ['nes'], size: 16, romSizeMultiple: 1024, name: 'iNES' }, /* https://www.nesdev.org/wiki/INES */ + { extensions: ['fds'], size: 16, romSizeMultiple: 65500, name: 'fwNES' }, /* https://www.nesdev.org/wiki/FDS_file_format */ + { extensions: ['lnx'], size: 64, romSizeMultiple: 1024, name: 'LNX' }, + { extensions: ['sfc', 'smc', 'swc', 'fig'], size: 512, romSizeMultiple: 262144, name: 'SNES copier' }, + ]; + + const GAME_BOY_NINTENDO_LOGO = [ + 0xce, 0xed, 0x66, 0x66, 0xcc, 0x0d, 0x00, 0x0b, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0c, 0x00, 0x0d, + 0x00, 0x08, 0x11, 0x1f, 0x88, 0x89, 0x00, 0x0e, 0xdc, 0xcc, 0x6e, 0xe6, 0xdd, 0xdd, 0xd9, 0x99 + ]; + + + + const _getRomSystem = function (binFile) { + /* to-do: add more systems */ + const extension = binFile.getExtension().trim(); + if (binFile.fileSize > 0x0200 && binFile.fileSize % 4 === 0) { + if ((extension === 'gb' || extension === 'gbc') && binFile.fileSize % 0x4000 === 0) { + binFile.seek(0x0104); + var valid = true; + for (var i = 0; i < GAME_BOY_NINTENDO_LOGO.length && valid; i++) { + if (GAME_BOY_NINTENDO_LOGO[i] !== binFile.readU8()) + valid = false; + } + if (valid) + return 'gb'; + } else if (extension === 'md' || extension === 'bin') { + binFile.seek(0x0100); + if (/SEGA (GENESIS|MEGA DR)/.test(binFile.readString(12))) + return 'smd'; + } else if (extension === 'z64' && binFile.fileSize >= 0x400000) { + return 'n64' + } + } else if (extension === 'fds' && binFile.fileSize % 65500 === 0) { + return 'fds' + } + return null; + } + const _getRomAdditionalChecksum = function (binFile) { + /* to-do: add more systems */ + const romSystem = _getRomSystem(binFile); + if (romSystem === 'n64') { + binFile.seek(0x3c); + const cartId = binFile.readString(3); + + binFile.seek(0x10); + const crc = binFile.readBytes(8).reduce(function (hex, b) { + if (b < 16) + return hex + '0' + b.toString(16); + else + return hex + b.toString(16); + }, ''); + return cartId + ' (' + crc + ')'; + } + return null; + } + + return { + parsePatchFile: function (patchFile) { + if (!(patchFile instanceof BinFile)) + throw new Error('Patch file is not an instance of BinFile'); + + patchFile.littleEndian = false; + patchFile.seek(0); + + var header = patchFile.readString(8); + var patch = null; + if (header.startsWith(IPS.MAGIC)) { + patch = IPS.fromFile(patchFile); + } else if (header.startsWith(UPS.MAGIC)) { + patch = UPS.fromFile(patchFile); + } else if (header.startsWith(APS.MAGIC)) { + patch = APS.fromFile(patchFile); + } else if (header.startsWith(APSGBA.MAGIC)) { + patch = APSGBA.fromFile(patchFile); + } else if (header.startsWith(BPS.MAGIC)) { + patch = BPS.fromFile(patchFile); + } else if (header.startsWith(RUP.MAGIC)) { + patch = RUP.fromFile(patchFile); + } else if (header.startsWith(PPF.MAGIC)) { + patch = PPF.fromFile(patchFile); + } else if (header.startsWith(BDF.MAGIC)) { + patch = BDF.fromFile(patchFile); + } else if (header.startsWith(PMSR.MAGIC)) { + patch = PMSR.fromFile(patchFile); + } else if (header.startsWith(VCDIFF.MAGIC)) { + patch = VCDIFF.fromFile(patchFile); + } + + if (patch) + patch._originalPatchFile = patchFile; + + return patch; + }, + + validateRom: function (romFile, patch, skipHeaderSize) { + if (!(romFile instanceof BinFile)) + throw new Error('ROM file is not an instance of BinFile'); + else if (typeof patch !== 'object') + throw new Error('Unknown patch format'); + + + if (typeof skipHeaderSize !== 'number' || skipHeaderSize < 0) + skipHeaderSize = 0; + + if ( + typeof patch.validateSource === 'function' && !patch.validateSource(romFile, skipHeaderSize) + ) { + return false; + } + return true; + }, + + applyPatch: function (romFile, patch, optionsParam) { + if (!(romFile instanceof BinFile)) + throw new Error('ROM file is not an instance of BinFile'); + else if (typeof patch !== 'object') + throw new Error('Unknown patch format'); + + + const options = { + requireValidation: false, + removeHeader: false, + addHeader: false, + fixChecksum: false, + outputSuffix: true + }; + if (typeof optionsParam === 'object') { + if (typeof optionsParam.requireValidation !== 'undefined') + options.requireValidation = !!optionsParam.requireValidation; + if (typeof optionsParam.removeHeader !== 'undefined') + options.removeHeader = !!optionsParam.removeHeader; + if (typeof optionsParam.addHeader !== 'undefined') + options.addHeader = !!optionsParam.addHeader; + if (typeof optionsParam.fixChecksum !== 'undefined') + options.fixChecksum = !!optionsParam.fixChecksum; + if (typeof optionsParam.outputSuffix !== 'undefined') + options.outputSuffix = !!optionsParam.outputSuffix; + } + + var extractedHeader = false; + var fakeHeaderSize = 0; + if (options.removeHeader) { + const headerInfo = RomPatcher.isRomHeadered(romFile); + if (headerInfo) { + const splitData = RomPatcher.removeHeader(romFile); + extractedHeader = splitData.header; + romFile = splitData.rom; + } + } else if (options.addHeader) { + const headerInfo = RomPatcher.canRomGetHeader(romFile); + if (headerInfo) { + fakeHeaderSize = headerInfo.fileSize; + romFile = RomPatcher.addFakeHeader(romFile); + } + } + + if (options.requireValidation && !RomPatcher.validateRom(romFile, patch)) { + throw new Error('Invalid input ROM checksum'); + } + + var patchedRom = patch.apply(romFile); + if (extractedHeader) { + /* reinsert header */ + if (options.fixChecksum) + RomPatcher.fixRomHeaderChecksum(patchedRom); + + const patchedRomWithHeader = new BinFile(extractedHeader.fileSize + patchedRom.fileSize); + patchedRomWithHeader.fileName = patchedRom.fileName; + patchedRomWithHeader.fileType = patchedRom.fileType; + extractedHeader.copyTo(patchedRomWithHeader, 0, extractedHeader.fileSize); + patchedRom.copyTo(patchedRomWithHeader, 0, patchedRom.fileSize, extractedHeader.fileSize); + + patchedRom = patchedRomWithHeader; + } else if (fakeHeaderSize) { + /* remove fake header */ + const patchedRomWithoutFakeHeader = patchedRom.slice(fakeHeaderSize); + + if (options.fixChecksum) + RomPatcher.fixRomHeaderChecksum(patchedRomWithoutFakeHeader); + + patchedRom = patchedRomWithoutFakeHeader; + + } else if (options.fixChecksum) { + RomPatcher.fixRomHeaderChecksum(patchedRom); + } + + if (options.outputSuffix) { + patchedRom.fileName = romFile.fileName.replace(/\.([^\.]*?)$/, ' (patched).$1'); + if(patchedRom.unpatched) + patchedRom.fileName = patchedRom.fileName.replace(' (patched)', ' (unpatched)'); + } else if (patch._originalPatchFile) { + patchedRom.fileName = patch._originalPatchFile.fileName.replace(/\.\w+$/i, (/\.\w+$/i.test(romFile.fileName) ? romFile.fileName.match(/\.\w+$/i)[0] : '')); + } else { + patchedRom.fileName = romFile.fileName; + } + + return patchedRom; + }, + + createPatch: function (originalFile, modifiedFile, format, metadata) { + if (!(originalFile instanceof BinFile)) + throw new Error('Original ROM file is not an instance of BinFile'); + else if (!(modifiedFile instanceof BinFile)) + throw new Error('Modified ROM file is not an instance of BinFile'); + + if (typeof format === 'string') + format = format.trim().toLowerCase(); + else if (typeof format === 'undefined') + format = 'ips'; + + var patch; + if (format === 'ips') { + patch = IPS.buildFromRoms(originalFile, modifiedFile); + } else if (format === 'bps') { + patch = BPS.buildFromRoms(originalFile, modifiedFile, (originalFile.fileSize <= 4194304)); + } else if (format === 'ppf') { + patch = PPF.buildFromRoms(originalFile, modifiedFile); + } else if (format === 'ups') { + patch = UPS.buildFromRoms(originalFile, modifiedFile); + } else if (format === 'aps') { + patch = APS.buildFromRoms(originalFile, modifiedFile); + } else if (format === 'rup') { + patch = RUP.buildFromRoms(originalFile, modifiedFile, metadata && metadata.Description? metadata.Description : null); + } else if (format === 'ebp') { + patch = IPS.buildFromRoms(originalFile, modifiedFile, metadata); + } else { + throw new Error('Invalid patch format'); + } + + if ( + !(format === 'ppf' && originalFile.fileSize > modifiedFile.fileSize) && //skip verification if PPF and PPF+modified size>original size + modifiedFile.hashCRC32() !== patch.apply(originalFile).hashCRC32() + ) { + //throw new Error('Unexpected error: verification failed. Patched file and modified file mismatch. Please report this bug.'); + } + return patch; + }, + + + /* check if ROM can inject a fake header (for patches that require a headered ROM) */ + canRomGetHeader: function (romFile) { + if (romFile.fileSize <= 0x600000) { + const compatibleHeader = HEADERS_INFO.find(headerInfo => headerInfo.extensions.indexOf(romFile.getExtension()) !== -1 && romFile.fileSize % headerInfo.romSizeMultiple === 0); + if (compatibleHeader) { + return { + name: compatibleHeader.name, + size: compatibleHeader.size + }; + } + } + return null; + }, + + /* check if ROM has a known header */ + isRomHeadered: function (romFile) { + if (romFile.fileSize <= 0x600200 && romFile.fileSize % 1024 !== 0) { + const compatibleHeader = HEADERS_INFO.find(headerInfo => headerInfo.extensions.indexOf(romFile.getExtension()) !== -1 && (romFile.fileSize - headerInfo.size) % headerInfo.romSizeMultiple === 0); + if (compatibleHeader) { + return { + name: compatibleHeader.name, + size: compatibleHeader.size + }; + } + } + return null; + }, + + /* remove ROM header */ + removeHeader: function (romFile) { + const headerInfo = RomPatcher.isRomHeadered(romFile); + if (headerInfo) { + return { + header: romFile.slice(0, headerInfo.size), + rom: romFile.slice(headerInfo.size) + } + } + return null; + }, + + /* add fake ROM header */ + addFakeHeader: function (romFile) { + const headerInfo = RomPatcher.canRomGetHeader(romFile); + if (headerInfo) { + const romWithFakeHeader = new BinFile(headerInfo.size + romFile.fileSize); + romWithFakeHeader.fileName = romFile.fileName; + romWithFakeHeader.fileType = romFile.fileType; + romFile.copyTo(romWithFakeHeader, 0, romFile.fileSize, headerInfo.size); + + //add a correct FDS header + if (_getRomSystem(romWithFakeHeader) === 'fds') { + romWithFakeHeader.seek(0); + romWithFakeHeader.writeBytes([0x46, 0x44, 0x53, 0x1a, romFile.fileSize / 65500]); + } + + romWithFakeHeader.fakeHeader = true; + + return romWithFakeHeader; + } + return null; + }, + + /* get ROM internal checksum, if possible */ + fixRomHeaderChecksum: function (romFile) { + const romSystem = _getRomSystem(romFile); + + if (romSystem === 'gb') { + /* get current checksum */ + romFile.seek(0x014d); + const currentChecksum = romFile.readU8(); + + /* calculate checksum */ + var newChecksum = 0x00; + romFile.seek(0x0134); + for (var i = 0; i <= 0x18; i++) { + newChecksum = ((newChecksum - romFile.readU8() - 1) >>> 0) & 0xff; + } + + /* fix checksum */ + if (currentChecksum !== newChecksum) { + console.log('fixed Game Boy checksum'); + romFile.seek(0x014d); + romFile.writeU8(newChecksum); + return true; + } + + } else if (romSystem === 'smd') { + /* get current checksum */ + romFile.seek(0x018e); + const currentChecksum = romFile.readU16(); + + /* calculate checksum */ + var newChecksum = 0x0000; + romFile.seek(0x0200); + while (!romFile.isEOF()) { + newChecksum = ((newChecksum + romFile.readU16()) >>> 0) & 0xffff; + } + + /* fix checksum */ + if (currentChecksum !== newChecksum) { + console.log('fixed Megadrive/Genesis checksum'); + romFile.seek(0x018e); + romFile.writeU16(newChecksum); + return true; + } + } + + return false; + }, + + /* get ROM additional checksum info, if possible */ + getRomAdditionalChecksum: function (romFile) { + return _getRomAdditionalChecksum(romFile); + }, + + + /* check if ROM is too big */ + isRomTooBig: function (romFile) { + return romFile && romFile.fileSize > TOO_BIG_ROM_SIZE; + } + } +}()); + + +if (typeof module !== 'undefined' && module.exports) { + module.exports = RomPatcher; + + IPS = require('./modules/RomPatcher.format.ips'); + UPS = require('./modules/RomPatcher.format.ups'); + APS = require('./modules/RomPatcher.format.aps_n64'); + APSGBA = require('./modules/RomPatcher.format.aps_gba'); + BPS = require('./modules/RomPatcher.format.bps'); + RUP = require('./modules/RomPatcher.format.rup'); + PPF = require('./modules/RomPatcher.format.ppf'); + BDF = require('./modules/RomPatcher.format.bdf'); + PMSR = require('./modules/RomPatcher.format.pmsr'); + VCDIFF = require('./modules/RomPatcher.format.vcdiff'); +} \ No newline at end of file diff --git a/public/rom-patcher-js/RomPatcher.webapp.js b/public/rom-patcher-js/RomPatcher.webapp.js new file mode 100644 index 0000000..6e2fedd --- /dev/null +++ b/public/rom-patcher-js/RomPatcher.webapp.js @@ -0,0 +1,2167 @@ +/* +* Rom Patcher JS - Webapp implementation +* A web implementation for Rom Patcher JS +* By Marc Robledo https://www.marcrobledo.com +* Sourcecode: https://github.com/marcrobledo/RomPatcher.js +* License: +* +* MIT License +* +* Copyright (c) 2016-2025 Marc Robledo +* +* 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. +*/ + + +/* + to-do list: + - allow multiple instances of RomPatcherWeb? + - switch to ES6 classes and modules? +*/ + +const ROM_PATCHER_JS_PATH = './rom-patcher-js/'; + +const RomPatcherWeb = (function () { + const SCRIPT_DEPENDENCIES = [ + 'modules/BinFile.js', + 'modules/HashCalculator.js', + 'modules/RomPatcher.format.ips.js', + 'modules/RomPatcher.format.ups.js', + 'modules/RomPatcher.format.aps_n64.js', + 'modules/RomPatcher.format.aps_gba.js', + 'modules/RomPatcher.format.bps.js', + 'modules/RomPatcher.format.rup.js', + 'modules/RomPatcher.format.ppf.js', + 'modules/RomPatcher.format.bdf.js', + 'modules/RomPatcher.format.pmsr.js', + 'modules/RomPatcher.format.vcdiff.js', + 'modules/zip.js/zip.min.js', + 'RomPatcher.js' + ]; + + const WEB_CRYPTO_AVAILABLE = window.crypto && window.crypto.subtle && window.crypto.subtle.digest; + const settings = { + language: typeof navigator.language === 'string' ? navigator.language.substring(0, 2) : 'en', + outputSuffix: true, + fixChecksum: false, + requireValidation: false, + + allowDropFiles: false, + + oninitialize: null, + onloadrom: null, + onvalidaterom: null, + onloadpatch: null, + onpatch: null + }; + var romFile, patch; + + const isBrowserSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent); /* Safari userAgent does not include word Chrome, Chrome includes both! */ + const isBrowserMobile = /Mobile(\/\S+)? /.test(navigator.userAgent); + + /* embeded patches */ + var currentEmbededPatches = null; + const _parseEmbededPatchInfo = function (embededPatchInfo) { + const parsedPatch = { + file: embededPatchInfo.file.trim(), + name: null, + description: null, + outputName: null, + outputExtension: null, + patches: null + }; + + if (typeof embededPatchInfo.name === 'string') { + parsedPatch.name = embededPatchInfo.name.trim(); + } else { + parsedPatch.name = embededPatchInfo.file.replace(/(.*?\/)+/g, ''); + } + + if (typeof embededPatchInfo.description === 'string') { + parsedPatch.description = embededPatchInfo.description; + } + + parsedPatch.optional = !!embededPatchInfo.optional; + + if (typeof embededPatchInfo.outputName === 'string') { + parsedPatch.outputName = embededPatchInfo.outputName; + } + if (typeof embededPatchInfo.outputExtension === 'string') { + parsedPatch.outputExtension = embededPatchInfo.outputExtension; + } + + if (typeof embededPatchInfo.inputMd5 !== 'undefined') { + if (!Array.isArray(embededPatchInfo.inputMd5)) + embededPatchInfo.inputMd5 = [embededPatchInfo.inputMd5]; + + const validMd5s = embededPatchInfo.inputMd5.reduce(function (acc, md5) { + if (typeof md5 === 'string' && /^(0x)?[0-9a-fA-F]{32}$/i.test(md5.trim())) { + acc.push(md5.trim().toLowerCase()); + } else { + console.warn('Rom Patcher JS: inputMd5 must be a string (32 characters long, characters allowed: 0-9, a-f)'); + } + return acc; + }, []); + + if (validMd5s.length) { + parsedPatch.inputValidation = { + 'type': 'MD5', + 'value': validMd5s + }; + } else { + console.warn('Rom Patcher JS: invalid inputMd5 for embeded patch', embededPatchInfo.inputMd5); + } + } + if (typeof embededPatchInfo.inputCrc32 !== 'undefined') { + if (!parsedPatch.inputValidation) { + if (!Array.isArray(embededPatchInfo.inputCrc32)) + embededPatchInfo.inputCrc32 = [embededPatchInfo.inputCrc32]; + + const validCrcs = embededPatchInfo.inputCrc32.reduce(function (acc, crc32) { + if (typeof crc32 === 'string' && /^(0x)?[0-9a-fA-F]{8}$/i.test(crc32.trim())) { + acc.push(parseInt(crc32.trim().replace('0x', ''), 16)); + } else if (typeof crc32 === 'number') { + acc.push((crc32 >>> 0) & 0xffffffff); + } else { + console.warn('Rom Patcher JS: invalid inputCrc32 value'); + } + return acc; + }, []); + + if (validCrcs.length) { + parsedPatch.inputValidation = { + 'type': 'CRC32', + 'value': validCrcs + }; + } else { + console.warn('Rom Patcher JS: invalid inputCrc32 for embeded patch', embededPatchInfo.inputCrc32); + } + } else { + console.warn('Rom Patcher JS: a valid inputMd5 was provided, inputCrc32 will be ignored', embededPatchInfo); + } + } + + return parsedPatch; + } + var isFetching = false; + const _fetchPatchFile = function (embededPatchInfo) { + if (isFetching) + throw new Error('Rom Patcher JS: already fetching another file'); + isFetching = true; + + htmlElements.disableAll(); + + const spinnerHtml = ''; + + htmlElements.hide('select-patch'); + htmlElements.empty('select-patch'); + htmlElements.removeClass('select-patch', 'single'); + htmlElements.removeClass('select-patch', 'multiple'); + htmlElements.setEnabled('select-patch', false); + htmlElements.empty('container-optional-patches'); + htmlElements.setText('span-loading-embeded-patch', _('Downloading...') + ' ' + spinnerHtml); + htmlElements.show('span-loading-embeded-patch'); + + + fetch(decodeURI(embededPatchInfo.file)) + .then(result => result.arrayBuffer()) // Gets the response and returns it as a blob + .then(arrayBuffer => { + const fetchedFile = new BinFile(arrayBuffer); + if (ZIPManager.isZipFile(fetchedFile)) { + if (typeof embededPatchInfo.patches === 'object') { + if (Array.isArray(embededPatchInfo.patches)) { + currentEmbededPatches = embededPatchInfo.patches.map((embededPatchInfo) => _parseEmbededPatchInfo(embededPatchInfo)); + } else { + console.warn('Rom Patcher JS: Invalid patches object for embeded patch', embededPatchInfo); + } + } else { + currentEmbededPatches = [_parseEmbededPatchInfo(embededPatchInfo)]; + } + htmlElements.setText('span-loading-embeded-patch', _('Unzipping...') + ' ' + spinnerHtml); + + ZIPManager.unzipEmbededPatches(arrayBuffer, currentEmbededPatches); + } else { + const parsedPatch = _parseEmbededPatchInfo(embededPatchInfo); + + currentEmbededPatches = [parsedPatch]; + const option = document.createElement('option'); + option.innerHTML = parsedPatch.name; + htmlElements.get('select-patch').appendChild(option); + htmlElements.setEnabled('select-patch', false); + htmlElements.addClass('select-patch', 'single'); + htmlElements.hide('span-loading-embeded-patch'); + htmlElements.show('select-patch'); + + fetchedFile.fileName = embededPatchInfo.file; + RomPatcherWeb.providePatchFile(fetchedFile); + } + isFetching = false; + }) + .catch(function (evt) { + isFetching = false; + _setToastError((_('Error downloading %s') + '
' + evt.message).replace('%s', embededPatchInfo.file.replace(/^.*[\/\\]/g, ''))); + }); + }; + const _getEmbededPatchInfo = function (fileName) { + if (currentEmbededPatches) + return currentEmbededPatches.find((embededPatchInfo) => embededPatchInfo.file === fileName); + return null; + } + + + + const _padZeroes = function (intVal, nBytes) { + var hexString = intVal.toString(16); + while (hexString.length < nBytes * 2) + hexString = '0' + hexString; + return hexString + } + + const _setElementsStatus = function (status, applyButtonStatus) { + htmlElements.setEnabled('input-file-rom', status); + htmlElements.setEnabled('input-file-patch', status); + if (htmlElements.get('select-patch')) { + htmlElements.setEnabled('select-patch', htmlElements.get('select-patch').children.length > 1 ? status : false); + } + htmlElements.setEnabled('checkbox-alter-header', status); + + if (romFile && patch && status) { + if (settings.requireValidation && typeof applyButtonStatus !== 'undefined') + status = !!applyButtonStatus; + htmlElements.setEnabled('button-apply', status); + } else { + htmlElements.setEnabled('button-apply', false); + } + }; + + const _setInputFileSpinner = function (inputFileId, status) { + const elementId = currentEmbededPatches && inputFileId === 'patch' ? ('select-' + inputFileId) : ('input-file-' + inputFileId); + const spinnerId = 'spinner-' + inputFileId; + + htmlElements.removeClass(elementId, 'empty'); + + + if (status) { + const spinner = document.createElement('spinner'); + spinner.id = 'rom-patcher-' + spinnerId; + spinner.className = 'rom-patcher-spinner'; + + const htmlInputFile = htmlElements.get(elementId); + if (htmlInputFile) { + if (elementId === 'select-patch') { + htmlInputFile.parentElement.insertBefore(spinner, htmlElements.get('span-loading-embeded-patch')); + } else { + htmlInputFile.parentElement.appendChild(spinner); + } + } + + htmlElements.addClass(elementId, 'loading'); + htmlElements.removeClass(elementId, 'valid'); + htmlElements.removeClass(elementId, 'invalid'); + + return spinner; + } else { + const spinner = htmlElements.get(spinnerId); + if (spinner) + spinner.parentElement.removeChild(spinner); + htmlElements.removeClass(elementId, 'loading'); + + return spinner; + } + } + const _setRomInputSpinner = function (status) { + return _setInputFileSpinner('rom', status); + } + const _setPatchInputSpinner = function (status) { + return _setInputFileSpinner('patch', status); + } + const _setApplyButtonSpinner = function (status) { + if (status) { + htmlElements.setText('button-apply', ' ' + _('Applying patch...')); + } else { + htmlElements.setText('button-apply', _('Apply patch')); + } + } + const _setToastError = function (errorMessage, className) { + const row = htmlElements.get('row-error-message'); + const span = htmlElements.get('error-message'); + if (row && span) { + if (errorMessage) { + htmlElements.addClass('row-error-message', 'show'); + htmlElements.setText('error-message', errorMessage); + if (className === 'warning') + htmlElements.addClass('error-message', 'warning'); + else + htmlElements.removeClass('error-message', 'warning'); + } else { + htmlElements.removeClass('row-error-message', 'show'); + htmlElements.setText('error-message', ''); + + } + } else { + console.error('Rom Patcher JS: ' + errorMessage); + } + } + + const htmlElements = { + get: function (id) { + return document.getElementById('rom-patcher-' + id); + }, + + enableAll: function () { + _setElementsStatus(true); + }, + disableAll: function () { + _setElementsStatus(false); + }, + + show: function (id) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).style.display = 'block'; + }, + hide: function (id) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).style.display = 'none'; + }, + + setValue: function (id, val) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).value = val; + }, + getValue: function (id, val, fallback) { + if (document.getElementById('rom-patcher-' + id)) + return document.getElementById('rom-patcher-' + id).value; + return fallback || 0; + }, + setFakeFile: function (id, fileName) { + if (!isBrowserSafari && document.getElementById('rom-patcher-input-file-' + id)) { /* safari does not show fake file name: https://pqina.nl/blog/set-value-to-file-input/#but-safari */ + try { + /* add a fake file to the input file, so it shows the chosen file name */ + const fakeFile = new File(new Uint8Array(0), fileName); + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(fakeFile); + document.getElementById('rom-patcher-input-file-' + id).files = dataTransfer.files; + } catch (ex) { + console.warning('File API constructor is not supported'); + } + } + }, + + setText: function (id, text) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).innerHTML = text; + }, + + empty: function (id) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).innerHTML = ''; + }, + addClass: function (id, className) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).classList.add(className); + }, + removeClass: function (id, className) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).classList.remove(className); + }, + setClass: function (id, className) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).className = className; + }, + + setEnabled: function (id, enabled) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).disabled = !enabled; + }, + setChecked: function (id, checked) { + if (document.getElementById('rom-patcher-' + id)) + document.getElementById('rom-patcher-' + id).checked = !!checked; + }, + isChecked: function (id) { + if (document.getElementById('rom-patcher-' + id)) + return document.getElementById('rom-patcher-' + id).checked; + return false; + }, + + setSpinner: function (inputFileId, status) { + if (inputFileId !== 'rom' && inputFileId !== 'patch') + throw new Error('RomPatcherWeb.htmlElements.setSpinner: only rom or patch input file ids are allowed'); + + return _setInputFileSpinner(inputFileId, status); + } + } + + + + /* web workers */ + const webWorkerApply = new Worker(ROM_PATCHER_JS_PATH + 'RomPatcher.webworker.apply.js'); + webWorkerApply.onmessage = event => { // listen for events from the worker + //retrieve arraybuffers back from webworker + romFile._u8array = event.data.romFileU8Array; + patch._originalPatchFile._u8array = event.data.patchFileU8Array; + + htmlElements.enableAll(); + _setApplyButtonSpinner(false); + if (event.data.patchedRomU8Array && !event.data.errorMessage) { + var patchedRom = new BinFile(event.data.patchedRomU8Array.buffer); + patchedRom.fileName = event.data.patchedRomFileName; + + if (currentEmbededPatches) { + const optionalPatches = currentEmbededPatches.filter((embededPatchInfo) => embededPatchInfo.optional); + if (optionalPatches.length) { + const originalFileName = patchedRom.fileName; + for (var i = 0; i < optionalPatches.length; i++) { + /* could be improved by using webWorkerApply to apply optional patches */ + if (optionalPatches[i].checkbox.checked) + patchedRom = RomPatcher.applyPatch(patchedRom, optionalPatches[i].parsedPatch, { requireValidation: false, fixChecksum: true }); + } + patchedRom.fileName = originalFileName; + } + } + + if (typeof settings.onpatch === 'function') + settings.onpatch(patchedRom); + + patchedRom.save(); + _setToastError(); + } else { + _setToastError(event.data.errorMessage); + } + }; + webWorkerApply.onerror = event => { // listen for exceptions from the worker + htmlElements.enableAll(); + _setApplyButtonSpinner(false); + _setToastError('webWorkerApply error: ' + event.message); + }; + + const webWorkerCrc = new Worker(ROM_PATCHER_JS_PATH + 'RomPatcher.webworker.crc.js'); + webWorkerCrc.onmessage = event => { // listen for events from the worker + //console.log('received_crc'); + htmlElements.setText('span-crc32', _padZeroes(event.data.crc32, 4)); + htmlElements.setText('span-md5', _padZeroes(event.data.md5, 16)); + romFile._u8array = event.data.u8array; + + if (WEB_CRYPTO_AVAILABLE) { + romFile.hashSHA1().then(function (res) { + htmlElements.setText('span-sha1', res); + }); + } + + if (event.data.rom) { + htmlElements.setText('span-rom-info', event.data.rom); + htmlElements.addClass('row-info-rom', 'show'); + } + + validRom = RomPatcherWeb.validateCurrentRom(event.data.checksumStartOffset); + _setElementsStatus(true, validRom); + }; + webWorkerCrc.onerror = event => { // listen for events from the worker + _setToastError('webWorkerCrc error: ' + event.message); + }; + + const _getChecksumStartOffset = function () { + if (romFile) { + const headerInfo = RomPatcher.isRomHeadered(romFile); + if (headerInfo) { + const htmlCheckboxAlterHeader = htmlElements.get('checkbox-alter-header'); + if (htmlCheckboxAlterHeader && htmlCheckboxAlterHeader.checked) + return headerInfo.size; + } + } + return 0; + } + + + const _getScriptPath = function () { + const currentScripts = document.querySelectorAll('script'); + var scriptPath; + if (document.currentScript) { + scriptPath = document.currentScript.src; + } else { + for (var i = 0; i < currentScripts.length; i++) { + if (currentScripts[i].src.indexOf('RomPatcher.webapp.js') !== -1) { + scriptPath = currentScripts[i].src; + break; + } + } + if (!scriptPath) + scriptPath = './rom-patcher-js/'; + } + return scriptPath.substring(0, scriptPath.lastIndexOf('/') + 1); + } + const _getMissingDependencies = function () { + const currentScripts = document.querySelectorAll('script'); + const scriptPath = _getScriptPath(); + var missingDependencies = []; + for (var i = 0; i < SCRIPT_DEPENDENCIES.length && !isLoaded; i++) { + var isLoaded = false; + for (var j = 0; j < currentScripts.length; j++) { + if (currentScripts[j].src === scriptPath + SCRIPT_DEPENDENCIES[i]) + isLoaded = true; + } + if (!isLoaded) + missingDependencies.push(scriptPath + SCRIPT_DEPENDENCIES[i]); + } + + return missingDependencies; + } + + + + const _checkEmbededPatchParameter = function (embededPatchInfo) { + if (embededPatchInfo) { + if (typeof embededPatchInfo === 'string') + embededPatchInfo = { file: embededPatchInfo }; + + if (typeof embededPatchInfo !== 'object') + throw new Error('Rom Patcher JS: invalid embeded patch parameter'); + else if (typeof embededPatchInfo.file !== 'string') + throw new Error('Rom Patcher JS: embeded patch missing file property'); + + return embededPatchInfo; + } + + return false; + } + + const _dragEventContainsFiles = function (evt) { + if (evt.dataTransfer.types) { + for (var i = 0; i < evt.dataTransfer.types.length; i++) { + if (evt.dataTransfer.types[i] === 'Files') + return true; + } + } + return false; + } + const _initialize = function (newSettings, embededPatchInfo) { + /* embeded patches */ + var validEmbededPatch = _checkEmbededPatchParameter(embededPatchInfo); + if (newSettings && typeof newSettings.file === 'string') { + console.warn('Rom Patcher JS: embeded patch info was provided in settings and will be ignored, must be passed as second parameter'); + } + + + + /* check if Rom Patcher JS core is available */ + if (typeof RomPatcher !== 'object') { + throw new Error('Rom Patcher JS: core not found'); + } + + + + /* check if zip-js web worker is available */ + if (typeof zip !== 'object' || typeof zip.useWebWorkers !== 'boolean') { + console.error('Rom Patcher JS: zip.js web worker not found'); + throw new Error('Rom Patcher JS: zip.js web worker not found'); + } + zip.useWebWorkers = true; + zip.workerScriptsPath = ROM_PATCHER_JS_PATH + 'modules/zip.js/'; + + /* check if all required HTML elements are in DOM */ + const htmlInputFileRom = htmlElements.get('input-file-rom'); + if (htmlInputFileRom && htmlInputFileRom.tagName === 'INPUT' && htmlInputFileRom.type === 'file') { + htmlInputFileRom.addEventListener('change', function (evt) { + if (this.files && this.files.length) { + htmlElements.disableAll(); + new BinFile(this, RomPatcherWeb.provideRomFile); + } else if (romFile) { + /* Webkit browsers trigger the change event when user cancels file selection and resets the input file value */ + /* since we keep a cached copy of ROM file as a BinFile, we do not lose data but the input text, so we try to set it back */ + /* Firefox keeps the previously selected file and does not trigger the change event */ + htmlElements.setFakeFile('rom', romFile.fileName); + } + }); + + if (!isBrowserSafari) + htmlInputFileRom.classList.add('no-file-selector-button'); + } else { + console.error('Rom Patcher JS: input#rom-patcher-input-file-rom[type=file] not found'); + throw new Error('Rom Patcher JS: input#rom-patcher-input-file-rom[type=file] not found'); + } + if (validEmbededPatch) { + const htmlSelectPatch = htmlElements.get('select-patch'); + if (htmlSelectPatch && htmlSelectPatch.tagName === 'SELECT') { + htmlSelectPatch.addEventListener('change', function (evt) { + const zippedEntryIndex = parseInt(this.value); + this._unzipSelectedPatch(zippedEntryIndex); + }); + } else { + console.error('Rom Patcher JS: select#rom-patcher-select-patch not found'); + throw new Error('Rom Patcher JS: select#rom-patcher-select-patch not found'); + } + const loadingSpan = document.createElement('span'); + loadingSpan.id = 'rom-patcher-span-loading-embeded-patch'; + loadingSpan.style.display = 'none'; + htmlSelectPatch.parentElement.appendChild(loadingSpan); + const containerOptionalPatches = document.createElement('div'); + containerOptionalPatches.id = 'rom-patcher-container-optional-patches'; + containerOptionalPatches.style.display = 'none'; + htmlSelectPatch.parentElement.appendChild(containerOptionalPatches); + } else { + const htmlInputFilePatch = htmlElements.get('input-file-patch'); + if (htmlInputFilePatch && htmlInputFilePatch.tagName === 'INPUT' && htmlInputFilePatch.type === 'file') { + htmlInputFilePatch.addEventListener('change', function (evt) { + if (this.files && this.files.length) { + htmlElements.disableAll(); + new BinFile(this, RomPatcherWeb.providePatchFile); + } else if (patch && patch._originalPatchFile) { + /* Webkit browsers trigger the change event when user cancels file selection and resets the input file value */ + /* since we keep a cached copy of patch file as a BinFile, we do not lose data but the input text, so we try to set it back */ + /* Firefox keeps the previously selected file and does not trigger the change event */ + htmlElements.setFakeFile('patch', patch._originalPatchFile.fileName); + } + }); + + if (!isBrowserSafari) + htmlInputFilePatch.classList.add('no-file-selector-button'); + } else { + console.error('Rom Patcher JS: input#rom-patcher-input-file-patch[type=file] not found'); + throw new Error('Rom Patcher JS: input#rom-patcher-input-file-patch[type=file] not found'); + } + + /* dirty fix for iOS Safari, which only supports mimetypes in accept attribute */ + /* accept attribute compatibility: https://caniuse.com/input-file-accept */ + if (isBrowserSafari && isBrowserMobile) { + htmlInputFilePatch.accept = 'application/zip, application/octet-stream, application/x-zip-compressed, multipart/x-zip'; + } + } + const htmlButtonApply = htmlElements.get('button-apply'); + if (htmlButtonApply && htmlButtonApply.tagName === 'BUTTON') { + htmlButtonApply.addEventListener('click', RomPatcherWeb.applyPatch); + } else { + console.error('Rom Patcher JS: button#rom-patcher-button-apply not found'); + throw new Error('Rom Patcher JS: button#rom-patcher-button-apply not found'); + } + const htmlCheckboxAlterHeader = htmlElements.get('checkbox-alter-header'); + if (htmlCheckboxAlterHeader && htmlCheckboxAlterHeader.tagName === 'INPUT' && htmlCheckboxAlterHeader.type === 'checkbox') { + htmlCheckboxAlterHeader.addEventListener('change', function (evt) { + if (!romFile) + return false; + + const headerInfo = RomPatcher.isRomHeadered(romFile); + if (headerInfo) { + htmlElements.disableAll(); + webWorkerCrc.postMessage({ u8array: romFile._u8array, fileName: romFile.fileName, checksumStartOffset: _getChecksumStartOffset() }, [romFile._u8array.buffer]); + } + }); + } + /* set all default input status just in case HTML is wrong */ + htmlElements.disableAll(); + /* reset input files */ + htmlElements.setValue('input-file-rom', ''); + htmlElements.setValue('input-file-patch', ''); + + + /* translatable elements */ + const translatableElements = document.querySelectorAll('*[data-localize="yes"]'); + for (var i = 0; i < translatableElements.length; i++) { + translatableElements[i].setAttribute('data-localize', translatableElements[i].innerHTML); + } + + /* add drag and drop events */ + if (newSettings && newSettings.allowDropFiles) { + window.addEventListener('dragover', function (evt) { + if (_dragEventContainsFiles(evt)) + evt.preventDefault(); /* needed ! */ + }); + window.addEventListener('drop', function (evt) { + evt.preventDefault(); + if (_dragEventContainsFiles(evt)) { + const droppedFiles = evt.dataTransfer.files; + if (droppedFiles && droppedFiles.length === 1) { + new BinFile(droppedFiles[0], function (binFile) { + if (RomPatcherWeb.getEmbededPatches()) { + RomPatcherWeb.provideRomFile(binFile, true); + } else if (ZIPManager.isZipFile(binFile)) { + ZIPManager.unzipAny(binFile._u8array.buffer); + } else if (RomPatcher.parsePatchFile(binFile)) { + RomPatcherWeb.providePatchFile(binFile, null, true); + } else { + RomPatcherWeb.provideRomFile(binFile, true); + } + }); + } + } + }); + htmlInputFileRom.addEventListener('drop', function (evt) { + evt.stopPropagation(); + }); + if (!validEmbededPatch) { + htmlElements.get('input-file-patch').addEventListener('drop', function (evt) { + evt.stopPropagation(); + }); + } + } + + console.log('Rom Patcher JS initialized'); + initialized = true; + + + /* initialize Rom Patcher */ + RomPatcherWeb.setSettings(newSettings); + + /* download embeded patch */ + if (validEmbededPatch) + _fetchPatchFile(validEmbededPatch); + else + htmlElements.enableAll(); + + if (typeof settings.oninitialize === 'function') + settings.oninitialize(this); + } + + + + /* localization */ + const _ = function (str) { return ROM_PATCHER_LOCALE[settings.language] && ROM_PATCHER_LOCALE[settings.language][str] ? ROM_PATCHER_LOCALE[settings.language][str] : str }; + + + var initialized = false; + var loading = 0; + return { + _: function (str) { /* public localization function for external usage purposes */ + return _(str); + }, + getHtmlElements: function () { + return htmlElements; + }, + + isInitialized: function () { + return initialized; + }, + getEmbededPatches: function () { + return currentEmbededPatches; + }, + + provideRomFile: function (binFile, transferFakeFile) { + htmlElements.disableAll(); + + + romFile = binFile; + + + + const canRomGetHeader = RomPatcher.canRomGetHeader(romFile); + const isRomHeadered = RomPatcher.isRomHeadered(romFile); + RomPatcherWeb.getHtmlElements().setChecked('checkbox-alter-header', false); + if (canRomGetHeader) { + RomPatcherWeb.getHtmlElements().setText('span-alter-header', _('Add %s header').replace('%s', _(canRomGetHeader.name))); + RomPatcherWeb.getHtmlElements().addClass('row-alter-header', 'show'); + } else if (isRomHeadered) { + RomPatcherWeb.getHtmlElements().setText('span-alter-header', _('Remove %s header').replace('%s', _(isRomHeadered.name))); + RomPatcherWeb.getHtmlElements().addClass('row-alter-header', 'show'); + } else { + RomPatcherWeb.getHtmlElements().setText('span-alter-header', ''); + RomPatcherWeb.getHtmlElements().removeClass('row-alter-header', 'show'); + } + + + romFile.seek(0); + + + _setRomInputSpinner(false); + if (ZIPManager.isZipFile(romFile)) { + ZIPManager.unzipRoms(romFile._u8array.buffer); + } else { + if (typeof settings.onloadrom === 'function') + settings.onloadrom(romFile); + RomPatcherWeb.calculateCurrentRomChecksums(); + } + + if (transferFakeFile) { + htmlElements.setFakeFile('rom', romFile.fileName); + } + }, + + providePatchFile: function (binFile, transferFakeFile) { + htmlElements.disableAll(); + + patch = null; + if (binFile) { + if (ZIPManager.isZipFile(binFile)) { + ZIPManager.unzipPatches(binFile._u8array.buffer); + } else { + try { + const parsedPatch = RomPatcher.parsePatchFile(binFile); + if (parsedPatch) { + patch = parsedPatch; + _setPatchInputSpinner(false); + + const embededPatchInfo = _getEmbededPatchInfo(binFile.fileName); + if (embededPatchInfo) { + /* custom input validation */ + if (embededPatchInfo.inputValidation) { + if (embededPatchInfo.inputValidation.type === 'CRC32') { + patch.validateSource = function (romFile, headerSize) { + for (var i = 0; i < embededPatchInfo.inputValidation.value.length; i++) { + if (embededPatchInfo.inputValidation.value[i] === romFile.hashCRC32(headerSize)) + return true; + } + return false; + } + } else if (embededPatchInfo.inputValidation.type === 'MD5') { + patch.validateSource = function (romFile, headerSize) { + for (var i = 0; i < embededPatchInfo.inputValidation.value.length; i++) { + if (embededPatchInfo.inputValidation.value[i] === romFile.hashMD5(headerSize)) + return true; + } + return false; + } + } else { + throw new Error('Rom Patcher JS: Invalid inputValidation type'); + } + patch.getValidationInfo = function () { + return embededPatchInfo.inputValidation + }; + } + + /* custom description */ + if (embededPatchInfo.description) { + patch.getDescription = function () { + return embededPatchInfo.description; + } + } + } + + /* toggle ROM requirements */ + if (htmlElements.get('row-patch-requirements') && htmlElements.get('patch-requirements-value')) { + if (typeof patch.getValidationInfo === 'function' && patch.getValidationInfo()) { + var validationInfo = patch.getValidationInfo(); + if (Array.isArray(validationInfo) || !validationInfo.type) { + validationInfo = { + type: 'ROM', + value: validationInfo + } + } + htmlElements.setText('patch-requirements-value', ''); + + htmlElements.setText('patch-requirements-type', validationInfo.type === 'ROM' ? _('Required ROM:') : _('Required %s:').replace('%s', validationInfo.type)); + + if (!Array.isArray(validationInfo.value)) + validationInfo.value = [validationInfo.value]; + + validationInfo.value.forEach(function (value) { + var line = document.createElement('div'); + if (typeof value !== 'string') { + if (validationInfo.type === 'CRC32') { + value = value.toString(16); + while (value.length < 8) + value = '0' + value; + } else { + value = value.toString(); + } + } + /* + var a=document.createElement('a'); + a.href='https://www.google.com/search?q=%22'+value+'%22'; + a.target='_blank'; + a.className='clickable'; + a.innerHTML=value; + line.appendChild(a); + */ + line.innerHTML = value; + htmlElements.get('patch-requirements-value').appendChild(line); + }); + htmlElements.addClass('row-patch-requirements', 'show'); + } else { + htmlElements.setText('patch-requirements-value', ''); + htmlElements.removeClass('row-patch-requirements', 'show'); + } + } + + /* toggle patch description */ + if (typeof patch.getDescription === 'function' && patch.getDescription()) { + htmlElements.setText('patch-description', patch.getDescription()/* .replace(/\n/g, '
') */); + //htmlElements.setTitle('patch-description', patch.getDescription()); + htmlElements.addClass('row-patch-description', 'show'); + } else { + htmlElements.setText('patch-description', ''); + //htmlElements.setTitle('patch-description', ''); + htmlElements.removeClass('row-patch-description', 'show'); + } + + RomPatcherWeb.validateCurrentRom(_getChecksumStartOffset()); + + if (typeof settings.onloadpatch === 'function') { + settings.onloadpatch(binFile, embededPatchInfo, parsedPatch); + } + + if (transferFakeFile) { + htmlElements.setFakeFile('patch', binFile.fileName); + } + } else { + _setToastError(_('Invalid patch file')); + } + } catch (ex) { + _setToastError(ex.message); + } + } + } + + if (patch) { + htmlElements.removeClass('input-file-patch', 'invalid'); + } else { + htmlElements.addClass('input-file-patch', 'invalid'); + } + htmlElements.enableAll(); + }, + + refreshRomFileName: function () { + if (romFile) + htmlElements.setFakeFile('rom', romFile.fileName); + }, + + pickEmbededFile: function (fileName) { + if (!currentEmbededPatches) + throw new Error('No embeded patches available'); + else if (typeof fileName !== 'string') + throw new Error('Invalid embeded patch file name'); + + const selectPatch = htmlElements.get('select-patch'); + for (var i = 0; i < selectPatch.children.length; i++) { + if (selectPatch.children[i].patchFileName === fileName) { + if (selectPatch.value != selectPatch.children[i].value) { + selectPatch.value = selectPatch.children[i].value; + + /* create and dispatch change event */ + const evt = new Event('change'); + selectPatch.dispatchEvent(evt); + } + break; + } + } + }, + + initialize: function (newSettings, embededPatchInfo) { + if (initialized) + throw new Error('Rom Patcher JS was already initialized'); + else if (loading) + throw new Error('Rom Patcher JS is already loading or has failed to load'); + + + /* check incompatible browsers */ + if ( + typeof window === 'undefined' || + typeof Worker !== 'function' || + typeof Array.isArray !== 'function' || + typeof window.addEventListener !== 'function' + // !document.createElement('div').classList instanceof DOMTokenList + ) + throw new Error('Rom Patcher JS: incompatible browser'); + + + /* queue script dependencies */ + const missingDependencies = _getMissingDependencies(); + loading = missingDependencies.length; + const onLoadScript = function () { + loading--; + if (loading === 0) { + try { + _initialize(newSettings, embededPatchInfo); + } catch (ex) { + _setToastError(ex.message); + htmlElements.disableAll(); + } + } + }; + const onErrorScript = function () { + throw new Error('Rom Patcher JS: error loading script ' + script.src); + }; + console.log('Rom Patcher JS: loading ' + missingDependencies.length + ' dependencies'); + missingDependencies.forEach(function (path) { + var script = document.createElement('script'); + script.onload = onLoadScript; + script.onerror = onErrorScript; + script.src = path; + + document.head.appendChild(script); + }); + }, + setEmbededPatches: function (embededPatchInfo) { + if (!currentEmbededPatches) + throw new Error('Rom Patcher JS: not in embeded patch mode'); + + /* embeded patches */ + var validEmbededPatch = _checkEmbededPatchParameter(embededPatchInfo); + if (!validEmbededPatch) + throw new Error('Rom Patcher JS: invalid embeded patch parameter'); + + _fetchPatchFile(validEmbededPatch); + }, + + applyPatch: function () { + if (romFile && patch) { + const romPatcherOptions = { + requireValidation: settings.requireValidation, + removeHeader: RomPatcher.isRomHeadered(romFile) && htmlElements.isChecked('checkbox-alter-header'), + addHeader: RomPatcher.canRomGetHeader(romFile) && htmlElements.isChecked('checkbox-alter-header'), + fixChecksum: settings.fixChecksum, + outputSuffix: settings.outputSuffix + }; + + htmlElements.disableAll(); + _setApplyButtonSpinner(true); + + const embededPatchInfo = _getEmbededPatchInfo(patch._originalPatchFile.fileName); + webWorkerApply.postMessage( + { + romFileU8Array: romFile._u8array, + patchFileU8Array: patch._originalPatchFile._u8array, + romFileName: romFile.fileName, + patchFileName: patch._originalPatchFile.fileName, + patchExtraInfo: embededPatchInfo, + //romFileType:romFile.fileType, + options: romPatcherOptions + }, + [ + romFile._u8array.buffer, + patch._originalPatchFile._u8array.buffer + ] + ); + } else if (!romFile) { + _setToastError(_('No ROM provided')); + } else if (!patch) { + _setToastError(_('No patch file provided')); + } + }, + + calculateCurrentRomChecksums: function (force) { + if (romFile.fileSize > 67108864 && !force) { + htmlElements.setText('span-crc32', _('File is too big.') + ' ' + _('Force calculate checksum') + ''); + htmlElements.setText('span-md5', ''); + htmlElements.setText('span-sha1', ''); + htmlElements.enableAll(); + return false; + } + + htmlElements.setText('span-crc32', _('Calculating...')); + htmlElements.setText('span-md5', _('Calculating...')); + if (WEB_CRYPTO_AVAILABLE) + htmlElements.setText('span-sha1', _('Calculating...')); + + htmlElements.setText('span-rom-info', ''); + htmlElements.removeClass('row-info-rom', 'show'); + + htmlElements.disableAll(); + webWorkerCrc.postMessage({ u8array: romFile._u8array, fileName: romFile.fileName, checksumStartOffset: _getChecksumStartOffset() }, [romFile._u8array.buffer]); + }, + + validateCurrentRom: function (checksumStartOffset) { + if (romFile && patch && typeof patch.validateSource === 'function') { + const validRom = RomPatcher.validateRom(romFile, patch, checksumStartOffset ?? 0); + if (validRom) { + htmlElements.addClass('input-file-rom', 'valid'); + htmlElements.removeClass('input-file-rom', 'invalid'); + _setToastError(); + } else { + htmlElements.addClass('input-file-rom', 'invalid'); + htmlElements.removeClass('input-file-rom', 'valid'); + _setToastError(_('Source ROM checksum mismatch')); + } + + if (typeof settings.onvalidaterom === 'function') + settings.onvalidaterom(romFile, validRom); + + return validRom; + } else { + htmlElements.removeClass('input-file-rom', 'valid'); + htmlElements.removeClass('input-file-rom', 'invalid'); + _setToastError(); + if (romFile && patch && typeof settings.onvalidaterom === 'function') + settings.onvalidaterom(romFile, true); + + return (romFile && patch); + } + }, + + enable: function () { + htmlElements.enableAll(); + }, + disable: function () { + htmlElements.disableAll(); + }, + setErrorMessage: function (message, className) { + _setToastError(message, className); + }, + translateUI: function (newLanguage) { + if (typeof newLanguage === 'object' && typeof newLanguage.language === 'string') + newLanguage = newLanguage.language; + if (typeof newLanguage === 'string') + settings.language = newLanguage; + + const translatableElements = document.querySelectorAll('*[data-localize]'); + for (var i = 0; i < translatableElements.length; i++) { + translatableElements[i].innerHTML = _(translatableElements[i].getAttribute('data-localize')); + } + }, + + getCurrentLanguage: function () { + return settings.language; + }, + setSettings: function (newSettings) { + if (newSettings && typeof newSettings === 'object') { + if (typeof newSettings.language === 'string') + settings.language = newSettings.language; + + if (typeof newSettings.outputSuffix === 'boolean') + settings.outputSuffix = newSettings.outputSuffix; + + if (typeof newSettings.fixChecksum === 'boolean') + settings.fixChecksum = newSettings.fixChecksum; + + if (typeof newSettings.requireValidation === 'boolean') + settings.requireValidation = newSettings.requireValidation; + + if (typeof newSettings.oninitialize === 'function') + settings.oninitialize = newSettings.oninitialize; + else if (typeof newSettings.oninitialize !== 'undefined') + settings.oninitialize = null; + + if (typeof newSettings.onloadrom === 'function') + settings.onloadrom = newSettings.onloadrom; + else if (typeof newSettings.onloadrom !== 'undefined') + settings.onloadrom = null; + + if (typeof newSettings.onvalidaterom === 'function') + settings.onvalidaterom = newSettings.onvalidaterom; + else if (typeof newSettings.onvalidaterom !== 'undefined') + settings.onvalidaterom = null; + + if (typeof newSettings.onloadpatch === 'function') + settings.onloadpatch = newSettings.onloadpatch; + else if (typeof newSettings.onloadpatch !== 'undefined') + settings.onloadpatch = null; + + if (typeof newSettings.onpatch === 'function') + settings.onpatch = newSettings.onpatch; + else if (typeof newSettings.onpatch !== 'undefined') + settings.onpatch = null; + } + RomPatcherWeb.translateUI(); + } + } +}()); + + + + +/* ZIP manager */ +const ZIPManager = (function (romPatcherWeb) { + const _ = romPatcherWeb._; + const htmlElements = romPatcherWeb.getHtmlElements(); + + const _setRomInputSpinner = function (status) { + htmlElements.setSpinner('rom', status); + }; + const _setPatchInputSpinner = function (status) { + htmlElements.setSpinner('patch', status); + + }; + + const ZIP_MAGIC = '\x50\x4b\x03\x04'; + + const FILTER_PATCHES = /\.(ips|ups|bps|aps|rup|ppf|ebp|bdf|bspatch|mod|xdelta|vcdiff)$/i; + //const FILTER_ROMS=/(? 1) { + _showFilePicker(filteredEntries, romPatcherWeb.provideRomFile); + romPatcherWeb.enable(); + } else { + /* no possible patchable files found in zip, treat zip file as ROM file */ + romPatcherWeb.calculateCurrentRomChecksums(); + } + }); + }, + /* failed */ + _unzipError + ); + }, + + unzipPatches: function (arrayBuffer) { + zip.createReader( + new zip.BlobReader(new Blob([arrayBuffer])), + /* success */ + function (zipReader) { + zipReader.getEntries(function (zipEntries) { + const filteredEntries = _filterEntriesPatches(zipEntries); + + if (filteredEntries.length === 1) { + _unzipEntry(filteredEntries[0], romPatcherWeb.providePatchFile); + } else if (filteredEntries.length > 1) { + _showFilePicker(filteredEntries, romPatcherWeb.providePatchFile); + } else { + romPatcherWeb.providePatchFile(null); + } + + }); + }, + /* failed */ + _unzipError + ); + }, + + unzipAny: function (arrayBuffer) { + zip.createReader( + new zip.BlobReader(new Blob([arrayBuffer])), + /* success */ + function (zipReader) { + zipReader.getEntries(function (zipEntries) { + const filteredEntriesRoms = _filterEntriesRoms(zipEntries); + const filteredEntriesPatches = _filterEntriesPatches(zipEntries); + + if (filteredEntriesRoms.length && filteredEntriesPatches.length === 0) { + if (filteredEntriesRoms.length === 1) { + _unzipEntry(filteredEntriesRoms[0], romPatcherWeb.provideRomFile); + } else { + _showFilePicker(filteredEntriesRoms, romPatcherWeb.provideRomFile); + romPatcherWeb.enable(); + } + } else if (filteredEntriesPatches.length && filteredEntriesRoms.length === 0) { + if (filteredEntriesPatches.length === 1) { + _unzipEntry(filteredEntriesPatches[0], romPatcherWeb.providePatchFile); + } else { + _showFilePicker(filteredEntriesPatches, romPatcherWeb.providePatchFile); + } + } else { + console.warn('ZIPManager.unzipAny: zip file contains both ROMs and patches, cannot guess'); + } + }); + }, + /* failed */ + _unzipError + ); + }, + + unzipEmbededPatches: function (arrayBuffer, embededPatchesInfo) { + zip.createReader( + new zip.BlobReader(new Blob([arrayBuffer])), + /* success */ + function (zipReader) { + zipReader.getEntries(function (zipEntries) { + const filteredEntries = _filterEntriesPatches(zipEntries); + + if (filteredEntries.length) { + const selectablePatches = []; + const optionalPatches = []; + for (var i = 0; i < filteredEntries.length; i++) { + const embededPatchInfo = embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === filteredEntries[i].filename); + if (embededPatchInfo && embededPatchInfo.optional) + optionalPatches.push(filteredEntries[i]); + else + selectablePatches.push(filteredEntries[i]); + } + + if (!selectablePatches.length) { + romPatcherWeb.setErrorMessage(_('No valid non-optional patches found in ZIP'), 'error'); + romPatcherWeb.disable(); + throw new Error('No valid non-optional patches found in ZIP'); + } + + if (embededPatchesInfo.length && embededPatchesInfo.length === 1 && selectablePatches.length === 1) + embededPatchesInfo[0].file = selectablePatches[0].filename; + + for (var i = 0; i < selectablePatches.length; i++) { + const embededPatchInfo = embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === selectablePatches[i].filename); + const option = document.createElement('option'); + option.innerHTML = embededPatchInfo && embededPatchInfo.name ? embededPatchInfo.name : selectablePatches[i].filename; + option.value = i; + option.patchFileName = selectablePatches[i].filename; + htmlElements.get('select-patch').appendChild(option); + } + htmlElements.get('select-patch')._unzipSelectedPatch = function (fileIndex) { + _unzipEntry(selectablePatches[fileIndex], romPatcherWeb.providePatchFile); + }; + + for (var i = 0; i < optionalPatches.length; i++) { + const embededPatchInfo = embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === optionalPatches[i].filename); + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.value = i; + checkbox.checked = false; + checkbox.disabled = true; + embededPatchInfo.checkbox = checkbox; + + const label = document.createElement('label'); + label.className = 'rom-patcher-checkbox-optional-patch'; + label.appendChild(checkbox); + label.appendChild(document.createTextNode(embededPatchInfo.name || embededPatchInfo.file)); + if (embededPatchInfo.description) + label.title = embededPatchInfo.description; + + htmlElements.get('container-optional-patches').appendChild(label); + + optionalPatches[i].getData(new zip.BlobWriter(), function (blob) { + const fileReader = new FileReader(); + fileReader.onload = function () { + const binFile = new BinFile(this.result); + binFile.fileName = 'optional_patch.unk'; + embededPatchInfo.parsedPatch = RomPatcher.parsePatchFile(binFile); + checkbox.disabled = false; + }; + fileReader.readAsArrayBuffer(blob); + }); + } + if (optionalPatches.length === 1) + htmlElements.show('container-optional-patches'); + + + + if (selectablePatches.length === 1) + htmlElements.addClass('select-patch', 'single'); + else + htmlElements.addClass('select-patch', 'multiple'); + + htmlElements.setEnabled('select-patch', false); + htmlElements.hide('span-loading-embeded-patch'); + htmlElements.show('select-patch'); + + _unzipEntry(selectablePatches[0], romPatcherWeb.providePatchFile); + + } else { + romPatcherWeb.setErrorMessage(_('No valid patches found in ZIP'), 'error'); + romPatcherWeb.disable(); + } + }); + }, + /* failed */ + _unzipError + ); + } + } +})(RomPatcherWeb); + + + + + +/* Patch Builder */ +const PatchBuilderWeb = (function (romPatcherWeb) { + var originalRom, modifiedRom; + + const isBrowserSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent); /* Safari userAgent does not include word Chrome, Chrome includes both! */ + + /* localization */ + const _ = function (str) { + const language = romPatcherWeb.getCurrentLanguage(); + return ROM_PATCHER_LOCALE[language] && ROM_PATCHER_LOCALE[language][str] ? ROM_PATCHER_LOCALE[language][str] : str + }; + + const _setCreateButtonSpinner = function (status) { + if (status) { + document.getElementById('patch-builder-button-create').innerHTML = ' ' + _('Creating patch...'); + } else { + document.getElementById('patch-builder-button-create').innerHTML = _('Create patch'); + } + } + + const _setToastError = function (errorMessage, className) { + const row = document.getElementById('patch-builder-row-error-message'); + const span = document.getElementById('patch-builder-error-message'); + + if (row && span) { + if (errorMessage) { + row.classList.add('show'); + span.innerHTML = errorMessage; + } else { + row.classList.remove('show'); + span.innerHTML = ''; + } + if (className === 'warning') + span.classList.add('warning'); + else + span.classList.remove('warning'); + } else { + if (className === 'warning') + console.warn('Patch Builder JS: ' + errorMessage); + else + console.error('Patch Builder JS: ' + errorMessage); + } + } + + const _setElementsStatus = function (status) { + document.getElementById('patch-builder-input-file-original').disabled = !status; + document.getElementById('patch-builder-input-file-modified').disabled = !status; + document.getElementById('patch-builder-select-patch-type').disabled = !status; + if (originalRom && modifiedRom && status) { + document.getElementById('patch-builder-button-create').disabled = !status; + } else { + document.getElementById('patch-builder-button-create').disabled = true + } + }; + + const _getMetadataFields = function (patchFormat) { + if (patchFormat === 'rup') { + return ['Description']; + } else if (patchFormat === 'ebp') { + return ['Author', 'Title', 'Description']; + } + return []; + }; + const _buildMetadataObject = function (patchFormat) { + return _getMetadataFields(patchFormat).reduce((metadata, field) => { + const input = document.getElementById('patch-builder-input-metadata-' + field.toLowerCase().replace(/\s+/g, '-')); + if (input && input.value.trim()) + metadata[field] = input.value.trim(); + return metadata; + }, {}); + }; + + var webWorkerCreate; + + var initialized = false; + return { + isInitialized: function () { + return initialized; + }, + + initialize: function () { + if (initialized) + throw new Error('Patch Builder JS was already initialized'); + else if (!romPatcherWeb.isInitialized()) + throw new Error('Rom Patcher JS must be initialized before Patch Builder JS'); + + + if (!document.getElementById('patch-builder-button-create') || document.getElementById('patch-builder-button-create').tagName !== 'BUTTON') { + console.error('Patch Builder JS: button#patch-builder-button-create not found'); + throw new Error('Patch Builder JS: button#patch-builder-button-create not found'); + } + if (!document.getElementById('patch-builder-select-patch-type') || document.getElementById('patch-builder-select-patch-type').tagName !== 'SELECT') { + console.error('Patch Builder JS: select#patch-builder-select-patch-type not found'); + throw new Error('Patch Builder JS: select#patch-builder-select-patch-type not found'); + } + if (!document.getElementById('patch-builder-input-file-original') || document.getElementById('patch-builder-input-file-original').tagName !== 'INPUT' || document.getElementById('patch-builder-input-file-original').type !== 'file') { + console.error('Patch Builder JS: input[type=file]#patch-builder-input-file-original not found'); + throw new Error('Patch Builder JS: input[type=file]#patch-builder-input-file-original not found'); + } + if (!document.getElementById('patch-builder-input-file-modified') || document.getElementById('patch-builder-input-file-modified').tagName !== 'INPUT' || document.getElementById('patch-builder-input-file-modified').type !== 'file') { + console.error('Patch Builder JS: input[type=file]#patch-builder-input-file-modified not found'); + throw new Error('Patch Builder JS: input[type=file]#patch-builder-input-file-modified not found'); + } + if (!isBrowserSafari) { + document.getElementById('patch-builder-input-file-original').classList.add('no-file-selector-button'); + document.getElementById('patch-builder-input-file-modified').classList.add('no-file-selector-button'); + } + + webWorkerCreate = new Worker(ROM_PATCHER_JS_PATH + 'RomPatcher.webworker.create.js'); + webWorkerCreate.onmessage = event => { // listen for events from the worker + //retrieve arraybuffers back from webworker + originalRom._u8array = event.data.originalRomU8Array; + modifiedRom._u8array = event.data.modifiedRomU8Array; + + _setElementsStatus(true); + _setCreateButtonSpinner(false); + + const patchFile = new BinFile(event.data.patchFileU8Array.buffer); + patchFile.fileName = modifiedRom.getName() + '.' + document.getElementById('patch-builder-select-patch-type').value; + patchFile.save(); + + _setToastError(); + }; + webWorkerCreate.onerror = event => { // listen for events from the worker + _setElementsStatus(true); + _setCreateButtonSpinner(false); + _setToastError('webWorkerCreate error: ' + event.message); + }; + + document.getElementById('patch-builder-button-create').disabled = true; + + document.getElementById('patch-builder-input-file-original').addEventListener('change', function () { + if (this.files && this.files.length) { + _setElementsStatus(false); + this.classList.remove('empty'); + originalRom = new BinFile(this.files[0], function (evt) { + _setElementsStatus(true); + + if (RomPatcher.isRomTooBig(originalRom)) + _setToastError(_('Using big files is not recommended'), 'warning'); + else if (ZIPManager.isZipFile(originalRom)) + _setToastError(_('Patch creation is not compatible with zipped ROMs'), 'warning'); + }); + } + }); + document.getElementById('patch-builder-input-file-modified').addEventListener('change', function () { + _setElementsStatus(false); + this.classList.remove('empty'); + modifiedRom = new BinFile(this.files[0], function (evt) { + _setElementsStatus(true); + + if (RomPatcher.isRomTooBig(modifiedRom)) + _setToastError(_('Using big files is not recommended'), 'warning'); + else if (ZIPManager.isZipFile(modifiedRom)) + _setToastError(_('Patch creation is not compatible with zipped ROMs'), 'warning'); + }); + }); + document.getElementById('patch-builder-select-patch-type').addEventListener('change', function () { + if (!document.getElementById('patch-builder-container-metadata-inputs')) + return; + + document.getElementById('patch-builder-container-metadata-inputs').innerHTML = ''; + + _getMetadataFields(this.value).forEach(function (field) { + const input = document.createElement('input'); + input.id = 'patch-builder-input-metadata-' + field.toLowerCase().replace(/\s+/g, '-'); + input.className = 'patch-builder-input-metadata'; + input.type = 'text'; + input.placeholder = _(field); + document.getElementById('patch-builder-container-metadata-inputs').appendChild(input); + }); + }); + document.getElementById('patch-builder-button-create').addEventListener('click', function () { + const patchFormat=document.getElementById('patch-builder-select-patch-type').value; + _setElementsStatus(false); + _setCreateButtonSpinner(true); + webWorkerCreate.postMessage( + { + originalRomU8Array: originalRom._u8array, + modifiedRomU8Array: modifiedRom._u8array, + format: patchFormat, + metadata: _buildMetadataObject(patchFormat) + }, [ + originalRom._u8array.buffer, + modifiedRom._u8array.buffer + ] + ); + }); + + console.log('Patch Builder JS initialized'); + initialized = true; + _setElementsStatus(true); + } + } +}(RomPatcherWeb)); + + + + + + + + + + + + + + + + + + + + +const ROM_PATCHER_LOCALE = { + 'fr': { + 'Creator mode': 'Mode créateur', + 'Settings': 'Configurations', + 'Use patch name for output': 'Utiliser le nom du patch pour renommer la ROM une fois patchée', + 'Light theme': 'Thème Clair', + + 'Apply patch': 'Appliquer le patch', + 'ROM file:': 'Fichier ROM:', + 'Patch file:': 'Fichier patch:', + 'Remove %s header': 'Supprimer l\'en-tête %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Formats compatibles:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Application du patch...', + 'Downloading...': 'Téléchargement...', + 'Unzipping...': 'Décompresser...', + + 'Create patch': 'Créer le patch', + 'Original ROM:': 'ROM originale:', + 'Modified ROM:': 'ROM modifiée:', + 'Patch type:': 'Type de patch:', + 'Creating patch...': 'Création du patch...', + + 'Source ROM checksum mismatch': 'Non-concordance de la somme de contrôle de la ROM source', + 'Target ROM checksum mismatch': 'Non-concordance de la somme de contrôle de la ROM cible', + 'Patch checksum mismatch': 'Non-concordance de la somme de contrôle du patch', + 'Error downloading %s': 'Erreur lors du téléchargement du patch', + 'Error unzipping file': 'Erreur lors de la décompression du fichier', + 'Invalid patch file': 'Fichier patch invalide', + 'Using big files is not recommended': 'L\'utilisation de gros fichiers n\'est pas recommandée' + }, + 'de': { + 'Creator mode': 'Erstellmodus', + 'Settings': 'Einstellungen', + 'Use patch name for output': 'Output ist Name vom Patch', + 'Fix ROM header checksum': 'Prüfsumme im ROM Header korrigieren', + 'Light theme': 'Helles Design', + + 'Apply patch': 'Patch anwenden', + 'ROM file:': 'ROM-Datei:', + 'Patch file:': 'Patch-Datei:', + 'Remove %s header': 'Header entfernen %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Unterstützte Formate:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Patch wird angewandt...', + 'Downloading...': 'Herunterladen...', + 'Unzipping...': 'Entpacken...', + + 'Create patch': 'Patch erstellen', + 'Original ROM:': 'Originale ROM:', + 'Modified ROM:': 'Veränderte ROM:', + 'Patch type:': 'Patch-Format:', + 'Creating patch...': 'Patch wird erstellt...', + + 'Source ROM checksum mismatch': 'Prüfsumme der Input-ROM stimmt nicht überein', + 'Target ROM checksum mismatch': 'Prüfsumme der Output-ROM stimmt nicht überein', + 'Patch checksum mismatch': 'Prüfsumme vom Patch stimmt nicht überein', + 'Error downloading %s': 'Fehler beim Herunterladen vom %s', + 'Error unzipping file': 'Fehler beim Entpacken', + 'Invalid patch file': 'Ungültiger Patch', + 'Using big files is not recommended': 'Große Dateien zu verwenden ist nicht empfohlen' + }, + 'es': { + 'Creator mode': 'Modo creador', + 'Settings': 'Configuración', + 'Use patch name for output': 'Guardar con nombre del parche', + 'Fix ROM header checksum': 'Corregir checksum cabecera ROM', + 'Light theme': 'Tema claro', + + 'Apply patch': 'Aplicar parche', + 'ROM file:': 'Archivo ROM:', + 'Patch file:': 'Archivo parche:', + 'Remove %s header': 'Quitar cabecera %s', + 'Add %s header': 'Añadir cabecera %s', + 'Compatible formats:': 'Formatos compatibles:', + 'Description:': 'Descripción:', + 'Required ROM:': 'ROM requerida:', + 'Required %s:': '%s requerido:', + 'Applying patch...': 'Aplicando parche...', + 'Downloading...': 'Descargando...', + 'Unzipping...': 'Descomprimiendo...', + 'Calculating...': 'Calculando...', + 'Force calculate checksum': 'Forzar cálculo de checksum', + + 'Create patch': 'Crear parche', + 'Original ROM:': 'ROM original:', + 'Modified ROM:': 'ROM modificada:', + 'Patch type:': 'Tipo de parche:', + 'Creating patch...': 'Creando parche...', + 'Author': 'Autor', + 'Title': 'Título', + 'Description': 'Descripción', + + 'Source ROM checksum mismatch': 'Checksum de ROM original no válida', + 'Target ROM checksum mismatch': 'Checksum de ROM creada no válida', + 'Patch checksum mismatch': 'Checksum de parche no válida', + 'Error downloading %s': 'Error descargando %s', + 'Error unzipping file': 'Error descomprimiendo archivo', + 'Invalid patch file': 'Archivo de parche no válido', + 'Using big files is not recommended': 'No es recomendable usar archivos muy grandes', + + 'SNES copier': 'copión SNES' + }, + 'it': { + 'Creator mode': 'Modalità creatore', + 'Settings': 'Impostazioni', + 'Use patch name for output': 'Usa il nome della patch per uscita', + 'Light theme': 'Tema chiaro', + + 'Apply patch': 'Applica patch', + 'ROM file:': 'File ROM:', + 'Patch file:': 'File patch:', + 'Remove %s header': 'Rimuovi header %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Formati:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Applica patch...', + 'Downloading...': 'Scaricamento...', + 'Unzipping...': 'Estrazione...', + + 'Create patch': 'Crea patch', + 'Original ROM:': 'ROM originale:', + 'Modified ROM:': 'ROM modificata:', + 'Patch type:': 'Tipologia patch:', + 'Creating patch...': 'Creazione patch...', + + 'Source ROM checksum mismatch': 'Checksum della ROM sorgente non valido', + 'Target ROM checksum mismatch': 'Checksum della ROM destinataria non valido', + 'Patch checksum mismatch': 'Checksum della patch non valido', + 'Error downloading %s': 'Errore di scaricamento %s', + 'Error unzipping file': 'Errore estrazione file', + 'Invalid patch file': 'File della patch non valido', + 'Using big files is not recommended': 'Non è raccomandato usare file di grandi dimensioni' + }, + 'nl': { + 'Creator mode': 'Creator-modus', + 'Settings': 'Settings', + 'Use patch name for output': 'Use patch name for output', + 'Light theme': 'Light theme', + + 'Apply patch': 'Pas patch toe', + 'ROM file:': 'ROM bestand:', + 'Patch file:': 'Patch bestand:', + 'Remove %s header': 'Verwijder rubriek %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Compatibele formaten:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Patch toepassen...', + 'Downloading...': 'Downloaden...', + 'Unzipping...': 'Uitpakken...', + + 'Create patch': 'Maak patch', + 'Original ROM:': 'Originale ROM:', + 'Modified ROM:': 'Aangepaste ROM:', + 'Patch type:': 'Type patch:', + 'Creating patch...': 'Patch maken...', + + 'Source ROM checksum mismatch': 'Controlesom van bron-ROM komt niet overeen', + 'Target ROM checksum mismatch': 'Controlesom van doel-ROM komt niet overeen', + 'Patch checksum mismatch': 'Controlesom van patch komt niet overeen', + 'Error downloading %s': 'Fout bij downloaden van patch', + 'Error unzipping file': 'Fout bij uitpakken van bestand', + 'Invalid patch file': 'Ongeldig patchbestand', + 'Using big files is not recommended': 'Het gebruik van grote bestanden wordt niet aanbevolen' + }, + 'sv': { + 'Creator mode': 'Skaparläge', + 'Settings': 'Settings', + 'Use patch name for output': 'Use patch name for output', + 'Light theme': 'Light theme', + + 'Apply patch': 'Tillämpa korrigeringsfil', + 'ROM file:': 'ROM-fil:', + 'Patch file:': 'Korrigeringsfil:', + 'Remove %s header': 'Ta bort rubrik %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Kompatibla format:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Tillämpar korrigeringsfil...', + 'Downloading...': 'Ladda ner...', + 'Unzipping...': 'Packa upp...', + + 'Create patch': 'Skapa korrigeringsfil', + 'Original ROM:': 'Original-ROM:', + 'Modified ROM:': 'Modifierad ROM:', + 'Patch type:': 'Korrigeringsfil-typ:', + 'Creating patch...': 'Skapa korrigeringsfil...', + + 'Source ROM checksum mismatch': 'ROM-källans kontrollsumman matchar inte', + 'Target ROM checksum mismatch': 'ROM-målets kontrollsumman matchar inte', + 'Patch checksum mismatch': 'korrigeringsfilens kontrollsumman matchar inte', + 'Error downloading %s': 'Fel vid nedladdning av korrigeringsfilen', + 'Error unzipping file': 'Det gick inte att packa upp filen', + 'Invalid patch file': 'Ogiltig korrigeringsfil', + 'Using big files is not recommended': 'Användning av stora filer rekommenderas inte' + }, + 'ca': { + 'Creator mode': 'Mode creador', + 'Settings': 'Configuració', + 'Use patch name for output': 'Desar amb nom del pedaç', + 'Light theme': 'Tema clar', + + 'Apply patch': 'Aplicar pedaç', + 'ROM file:': 'Arxiu ROM:', + 'Patch file:': 'Arxiu pedaç:', + 'Remove %s header': 'Treure capçalera %s', + 'Add %s header': 'Afegir capçalera %s', + 'Compatible formats:': 'Formats compatibles:', + 'Description:': 'Descripció:', + 'Required ROM:': 'ROM requerida:', + 'Required %s:': '%s requerit:', + 'Applying patch...': 'Aplicant pedaç...', + 'Downloading...': 'Descarregant...', + 'Unzipping...': 'Descomprimint...', + + 'Create patch': 'Crear pedaç', + 'Original ROM:': 'ROM original:', + 'Modified ROM:': 'ROM modificada:', + 'Patch type:': 'Tipus de pedaç:', + 'Creating patch...': 'Creant pedaç...', + 'Author': 'Autor', + 'Title': 'Títol', + 'Description': 'Descripció', + + 'Source ROM checksum mismatch': 'Checksum de ROM original no vàlida', + 'Target ROM checksum mismatch': 'Checksum de ROM creada no vàlida', + 'Patch checksum mismatch': 'Checksum de pedaç no vàlida', + 'Error downloading %s': 'Error descarregant %s', + 'Error unzipping file': 'Error descomprimint arxiu', + 'Invalid patch file': 'Arxiu de pedaç no vàlid', + 'Using big files is not recommended': 'No és recomanable usar arxius molt grans' + }, + 'ca-va': { + 'Creator mode': 'Mode creador', + 'Settings': 'Configuració', + 'Use patch name for output': 'Guardar amb nom del pedaç', + 'Light theme': 'Tema clar', + + 'Apply patch': 'Aplicar pedaç', + 'ROM file:': 'Arxiu ROM:', + 'Patch file:': 'Arxiu pedaç:', + 'Remove %s header': 'Llevar capçalera %s', + 'Add %s header': 'Afegir capçalera %s', + 'Compatible formats:': 'Formats compatibles:', + 'Description:': 'Descripció:', + 'Required ROM:': 'ROM requerida:', + 'Required %s:': '%s requerit:', + 'Applying patch...': 'Aplicant pedaç...', + 'Downloading...': 'Descarregant...', + 'Unzipping...': 'Descomprimint...', + + 'Create patch': 'Crear pedaç', + 'Original ROM:': 'ROM original:', + 'Modified ROM:': 'ROM modificada:', + 'Patch type:': 'Tipus de pedaç:', + 'Creating patch...': 'Creant pedaç...', + + 'Source ROM checksum mismatch': 'Checksum de ROM original incorrecta', + 'Target ROM checksum mismatch': 'Checksum de ROM creada incorrecta', + 'Patch checksum mismatch': 'Checksum de pedaç incorrecte', + 'Error downloading %s': 'Error descarregant %s', + 'Error unzipping file': 'Error descomprimint arxiu', + 'Invalid patch file': 'Arxiu de pedaç incorrecte', + 'Using big files is not recommended': 'No és recomanable utilitzar arxius molt grans' + }, + 'ru': { + 'Creator mode': 'Режим создания', + 'Settings': 'Settings', + 'Use patch name for output': 'Use patch name for output', + 'Light theme': 'Light theme', + + 'Apply patch': 'Применить патч', + 'ROM file:': 'Файл ROM:', + 'Patch file:': 'Файл патча:', + 'Remove %s header': 'Удалить заголовок перед применением', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Совместимые форматы:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Применяется патч...', + 'Downloading...': 'Загрузка...', + 'Unzipping...': 'Unzipping...', + + 'Create patch': 'Создать патч', + 'Original ROM:': 'Оригинальный ROM:', + 'Modified ROM:': 'Изменённый ROM:', + 'Patch type:': 'Тип патча:', + 'Creating patch...': 'Патч создаётся...', + + 'Source ROM checksum mismatch': 'Неправильная контрольная сумма входного ROM', + 'Target ROM checksum mismatch': 'Неправильная контрольная сумма выходного ROM', + 'Patch checksum mismatch': 'Неправильная контрольная сумма патча', + 'Error downloading %s': 'Ошибка при скачивании патча', + 'Error unzipping file': 'Error unzipping file', + 'Invalid patch file': 'Неправильный файл патча', + 'Using big files is not recommended': 'Не рекомендуется использовать большие файлы' + }, + 'pt-br': { + 'Creator mode': 'Modo criador', + 'Settings': 'Configurações', + 'Use patch name for output': 'Usar o nome do patch na saída', + 'Fix ROM header checksum': 'Consertar o checksum do cabeçalho da ROM', + 'Light theme': 'Tema leve', + + 'Apply patch': 'Aplicar patch', + 'ROM file:': 'Arquivo da ROM:', + 'Patch file:': 'Arquivo do patch:', + 'Remove %s header': 'Remover cabeçalho %s', + //'Add %s header': 'Add %s header', + 'Compatible formats:': 'Formatos compatíveis:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'Aplicando patch...', + 'Downloading...': 'Baixando...', + 'Unzipping...': 'Descompactando...', + + 'Create patch': 'Criar patch', + 'Original ROM:': 'ROM original:', + 'Modified ROM:': 'ROM modificada:', + 'Patch type:': 'Tipo de patch:', + 'Creating patch...': 'Criando o patch...', + + 'Source ROM checksum mismatch': 'O checksum da ROM original é inválido', + 'Target ROM checksum mismatch': 'O checksum da ROM alvo é inválido', + 'Patch checksum mismatch': 'O checksum do patch é inválido', + 'Error downloading %s': 'Erro ao baixar o %s', + 'Error unzipping file': 'Erro ao descompactar o arquivo', + 'Invalid patch file': 'Arquivo do patch inválido', + 'Using big files is not recommended': 'O uso de arquivos grandes não é recomendado' + }, + 'ja': { + 'Creator mode': '作成モード', + 'Settings': '設定', + 'Use patch name for output': 'パッチと同じ名前で出力', + 'Light theme': 'ライトテーマ', + + 'Apply patch': 'パッチを当て', + 'ROM file:': 'ROMファィル:', + 'Patch file:': 'パッチファイル:', + 'Remove %s header': 'ヘッダーを削除', + //'Add %s header': 'Add %s header', + 'Compatible formats:': '互換性のあるフォーマット:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': 'パッチを当ている…', + 'Downloading...': 'ダウンロードしている…', + 'Unzipping...': '解凍している…', + + 'Create patch': 'パッチを作成', + 'Original ROM:': '元のROM:', + 'Modified ROM:': '変更されたROM:', + 'Patch type:': 'パッチのタイプ:', + 'Creating patch...': 'パッチを作成している…', + + 'Source ROM checksum mismatch': 'ソースROMチェックサムの不一致', + 'Target ROM checksum mismatch': 'ターゲットROMチェクサムの不一致', + 'Patch checksum mismatch': 'バッチチェックサムの不一致', + 'Error downloading %s': 'パッチのダウンロードエラー', + 'Error unzipping file': 'パッチの解凍エラー', + 'Invalid patch file': '無効なパッチエラー', + 'Using big files is not recommended': '大きなファイルの使いはおすすめしない。' + }, + 'zh-cn': { + 'Creator mode': '创建模式', + 'Settings': '设置', + 'Use patch name for output': '修改后ROM文件名和补丁保持一致', + 'Light theme': '浅色主题', + + 'Apply patch': '打补丁', + 'ROM file:': 'ROM文件:', + 'Patch file:': '补丁文件:', + 'Remove %s header': '删除文件头', + //'Add %s header': 'Add %s header', + 'Compatible formats:': '兼容补丁格式:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': '正在打补丁……', + 'Downloading...': '正在下载……', + 'Unzipping...': '正在解压……', + + 'Create patch': '创建补丁', + 'Original ROM:': '原始ROM:', + 'Modified ROM:': '修改后ROM:', + 'Patch type:': '补丁类型:', + 'Creating patch...': '正在创建补丁……', + + 'Source ROM checksum mismatch': '原始ROM校验和不匹配', + 'Target ROM checksum mismatch': '目标ROM校验和不匹配', + 'Patch checksum mismatch': '补丁文件校验和不匹配', + 'Error downloading %s': '下载出错:%s', + 'Error unzipping file': '解压出错', + 'Invalid patch file': '无效补丁', + 'Using big files is not recommended': '不推荐使用大文件。' + }, + 'zh-tw': { + 'Creator mode': '創作者模式', + 'Settings': '設定', + 'Use patch name for output': '修改後ROM檔名和patch保持一致', + 'Fix ROM header checksum': '修正ROM檔頭校驗碼', + 'Light theme': '淺色主題', + + 'Apply patch': '套用patch', + 'ROM file:': 'ROM檔:', + 'Patch file:': 'patch檔:', + 'Remove %s header': '刪除檔頭', + //'Add %s header': 'Add %s header', + 'Compatible formats:': '相容格式:', + //'Description:': 'Description:', + //'Required ROM:': 'Required ROM:', + //'Required %s:': 'Required %s:', + 'Applying patch...': '套用patch中……', + 'Downloading...': '下載中……', + 'Unzipping...': '解壓中……', + + 'Create patch': '創建patch', + 'Original ROM:': '原始ROM:', + 'Modified ROM:': '修改後ROM:', + 'Patch type:': 'patch類型:', + 'Creating patch...': '正在創建patch……', + + 'Source ROM checksum mismatch': '原始ROM校驗碼不匹配', + 'Target ROM checksum mismatch': '目標ROM校驗碼不匹配', + 'Patch checksum mismatch': 'patch檔校驗碼不匹配', + 'Error downloading %s': '下載出錯:%s', + 'Error unzipping file': '解壓出錯', + 'Invalid patch file': '無效的patch檔', + 'Using big files is not recommended': '不建議使用大檔。' + } +}; \ No newline at end of file diff --git a/public/rom-patcher-js/RomPatcher.webworker.apply.js b/public/rom-patcher-js/RomPatcher.webworker.apply.js new file mode 100644 index 0000000..119c401 --- /dev/null +++ b/public/rom-patcher-js/RomPatcher.webworker.apply.js @@ -0,0 +1,83 @@ +/* Rom Patcher JS v20250922 - Marc Robledo 2016-2025 - http://www.marcrobledo.com/license */ + +self.importScripts( + './RomPatcher.js', + './modules/BinFile.js', + './modules/HashCalculator.js', + './modules/RomPatcher.format.ips.js', + './modules/RomPatcher.format.aps_n64.js', + './modules/RomPatcher.format.aps_gba.js', + './modules/RomPatcher.format.ups.js', + './modules/RomPatcher.format.bps.js', + './modules/RomPatcher.format.rup.js', + './modules/RomPatcher.format.ppf.js', + './modules/RomPatcher.format.bdf.js', + './modules/RomPatcher.format.pmsr.js', + './modules/RomPatcher.format.vcdiff.js' +); + + +self.onmessage = event => { // listen for messages from the main thread + const romFile=new BinFile(event.data.romFileU8Array); + romFile.fileName=event.data.romFileName; + //romFile.fileType.event.data.romFileType; + const patchFile=new BinFile(event.data.patchFileU8Array); + patchFile.fileName=event.data.patchFileName; + + const patch=RomPatcher.parsePatchFile(patchFile); + + var errorMessage=false; + + + var patchedRom; + if(patch){ + try{ + patchedRom=RomPatcher.applyPatch(romFile, patch, event.data.options); + }catch(evt){ + errorMessage=evt.message; + } + }else{ + errorMessage='Invalid patch file'; + } + + //console.log('postMessage'); + if(patchedRom){ + /* set custom output name if embeded patch */ + const patchExtraInfo=event.data.patchExtraInfo; + if(patchExtraInfo){ + if(typeof patchExtraInfo.outputName === 'string') + patchedRom.setName(patchExtraInfo.outputName); + if(typeof patchExtraInfo.outputExtension === 'string') + patchedRom.setExtension(patchExtraInfo.outputExtension); + } + + self.postMessage( + { + success: !!errorMessage, + romFileU8Array:event.data.romFileU8Array, + patchFileU8Array:event.data.patchFileU8Array, + patchedRomU8Array:patchedRom._u8array, + patchedRomFileName:patchedRom.fileName, + errorMessage:errorMessage + }, + [ + event.data.romFileU8Array.buffer, + event.data.patchFileU8Array.buffer, + patchedRom._u8array.buffer + ] + ); + }else{ + self.postMessage( + { + success: false, + romFileU8Array:event.data.romFileU8Array, + patchFileU8Array:event.data.patchFileU8Array, + errorMessage:errorMessage + }, + [ + event.data.romFileU8Array.buffer, + event.data.patchFileU8Array.buffer + ] + ); + } +}; \ No newline at end of file diff --git a/public/rom-patcher-js/RomPatcher.webworker.crc.js b/public/rom-patcher-js/RomPatcher.webworker.crc.js new file mode 100644 index 0000000..8d39204 --- /dev/null +++ b/public/rom-patcher-js/RomPatcher.webworker.crc.js @@ -0,0 +1,26 @@ +/* Rom Patcher JS v20240302 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */ + +self.importScripts( + './RomPatcher.js', + './modules/BinFile.js', + './modules/HashCalculator.js' +); + + +self.onmessage = event => { // listen for messages from the main thread + const binFile=new BinFile(event.data.u8array); + binFile.fileName=event.data.fileName; + const startOffset=typeof event.data.checksumStartOffset==='number'? event.data.checksumStartOffset : 0; + + self.postMessage( + { + action: event.data.action, + crc32:binFile.hashCRC32(startOffset), + md5:binFile.hashMD5(startOffset), + checksumStartOffset: startOffset, + rom:RomPatcher.getRomAdditionalChecksum(binFile), + u8array:event.data.u8array + }, + [event.data.u8array.buffer] + ); +}; \ No newline at end of file diff --git a/public/rom-patcher-js/RomPatcher.webworker.create.js b/public/rom-patcher-js/RomPatcher.webworker.create.js new file mode 100644 index 0000000..dff6d9e --- /dev/null +++ b/public/rom-patcher-js/RomPatcher.webworker.create.js @@ -0,0 +1,37 @@ +/* Rom Patcher JS v20240302 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */ + +self.importScripts( + './RomPatcher.js', + './modules/BinFile.js', + './modules/HashCalculator.js', + './modules/RomPatcher.format.ips.js', + './modules/RomPatcher.format.aps_n64.js', + './modules/RomPatcher.format.ups.js', + './modules/RomPatcher.format.bps.js', + './modules/RomPatcher.format.rup.js', + './modules/RomPatcher.format.ppf.js' +); + + +self.onmessage = event => { // listen for messages from the main thread + const originalFile=new BinFile(event.data.originalRomU8Array); + const modifiedFile=new BinFile(event.data.modifiedRomU8Array); + const format=event.data.format; + const metadata=event.data.metadata; + + const patch=RomPatcher.createPatch(originalFile, modifiedFile, format, metadata); + const patchFile=patch.export('my_patch'); + + self.postMessage( + { + originalRomU8Array:event.data.originalRomU8Array, + modifiedRomU8Array:event.data.modifiedRomU8Array, + patchFileU8Array:patchFile._u8array + }, + [ + event.data.originalRomU8Array.buffer, + event.data.modifiedRomU8Array.buffer, + patchFile._u8array.buffer + ] + ); +}; \ No newline at end of file diff --git a/public/rom-patcher-js/modules/BinFile.js b/public/rom-patcher-js/modules/BinFile.js new file mode 100644 index 0000000..5a8a855 --- /dev/null +++ b/public/rom-patcher-js/modules/BinFile.js @@ -0,0 +1,483 @@ +/* +* BinFile.js (last update: 2024-08-21) +* by Marc Robledo, https://www.marcrobledo.com +* +* a JS class for reading/writing sequentially binary data from/to a file +* that allows much more manipulation than simple DataView +* compatible with both browsers and Node.js +* +* MIT License +* +* Copyright (c) 2014-2024 Marc Robledo +* +* 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. +*/ + + + +function BinFile(source, onLoad) { + this.littleEndian = false; + this.offset = 0; + this._lastRead = null; + this._offsetsStack = []; + + + if ( + BinFile.RUNTIME_ENVIROMENT === 'browser' && ( + source instanceof File || + source instanceof FileList || + (source instanceof HTMLElement && source.tagName === 'INPUT' && source.type === 'file') + ) + ) { + if (source instanceof HTMLElement) + source = source.files; + if (source instanceof FileList) + source = source[0]; + + this.fileName = source.name; + this.fileType = source.type; + this.fileSize = source.size; + + if (typeof window.FileReader !== 'function') + throw new Error('Incompatible browser'); + + this._fileReader = new FileReader(); + this._fileReader.addEventListener('load', function () { + this.binFile._u8array = new Uint8Array(this.result); + + if (typeof onLoad === 'function') + onLoad(this.binFile); + }, false); + + + this._fileReader.binFile = this; + + this._fileReader.readAsArrayBuffer(source); + + + + } else if (BinFile.RUNTIME_ENVIROMENT === 'node' && typeof source === 'string') { + if (!nodeFs.existsSync(source)) + throw new Error(source + ' does not exist'); + + const arrayBuffer = nodeFs.readFileSync(source); + + this.fileName = nodePath.basename(source); + this.fileType = nodeFs.statSync(source).type; + this.fileSize = arrayBuffer.byteLength; + + this._u8array = new Uint8Array(arrayBuffer); + + if (typeof onLoad === 'function') + onLoad(this); + + + + } else if (source instanceof BinFile) { /* if source is another BinFile, clone it */ + this.fileName = source.fileName; + this.fileType = source.fileType; + this.fileSize = source.fileSize; + + this._u8array = new Uint8Array(source._u8array.buffer.slice()); + + if (typeof onLoad === 'function') + onLoad(this); + + + + } else if (source instanceof ArrayBuffer) { + this.fileName = 'file.bin'; + this.fileType = 'application/octet-stream'; + this.fileSize = source.byteLength; + + this._u8array = new Uint8Array(source); + + if (typeof onLoad === 'function') + onLoad(this); + + + + } else if (ArrayBuffer.isView(source)) { /* source is TypedArray */ + this.fileName = 'file.bin'; + this.fileType = 'application/octet-stream'; + this.fileSize = source.buffer.byteLength; + + this._u8array = new Uint8Array(source.buffer); + + if (typeof onLoad === 'function') + onLoad(this); + + + + } else if (typeof source === 'number') { /* source is integer, create new empty file */ + this.fileName = 'file.bin'; + this.fileType = 'application/octet-stream'; + this.fileSize = source; + + this._u8array = new Uint8Array(new ArrayBuffer(source)); + + if (typeof onLoad === 'function') + onLoad(this); + + + + } else { + throw new Error('invalid BinFile source'); + } +} +BinFile.RUNTIME_ENVIROMENT = (function () { + if (typeof window === 'object' && typeof window.document === 'object') + return 'browser'; + else if (typeof WorkerGlobalScope === 'function' && self instanceof WorkerGlobalScope) + return 'webworker'; + else if (typeof require === 'function' && typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string') + return 'node'; + else + return null; +}()); +BinFile.DEVICE_LITTLE_ENDIAN = (function () { /* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView#Endianness */ + var buffer = new ArrayBuffer(2); + new DataView(buffer).setInt16(0, 256, true /* littleEndian */); + // Int16Array uses the platform's endianness. + return new Int16Array(buffer)[0] === 256; +})(); + + + +BinFile.prototype.push = function () { + this._offsetsStack.push(this.offset); +} +BinFile.prototype.pop = function () { + this.seek(this._offsetsStack.pop()); +} +BinFile.prototype.seek = function (offset) { + this.offset = offset; +} +BinFile.prototype.skip = function (nBytes) { + this.offset += nBytes; +} +BinFile.prototype.isEOF = function () { + return !(this.offset < this.fileSize) +} +BinFile.prototype.slice = function (offset, len, doNotClone) { + if (typeof offset !== 'number' || offset < 0) + offset = 0; + else if (offset >= this.fileSize) + throw new Error('out of bounds slicing'); + else + offset = Math.floor(offset); + + if (typeof len !== 'number' || offset < 0 || (offset + len) >= this.fileSize.length) + len = this.fileSize - offset; + else if (len === 0) + throw new Error('zero length provided for slicing'); + else + len = Math.floor(len); + + if (offset === 0 && len === this.fileSize && doNotClone) + return this; + + + var newFile = new BinFile(this._u8array.buffer.slice(offset, offset + len)); + newFile.fileName = this.fileName; + newFile.fileType = this.fileType; + newFile.littleEndian = this.littleEndian; + return newFile; +} +BinFile.prototype.prependBytes = function (bytes) { + var newFile = new BinFile(this.fileSize + bytes.length); + newFile.seek(0); + newFile.writeBytes(bytes); + this.copyTo(newFile, 0, this.fileSize, bytes.length); + + this.fileSize = newFile.fileSize; + this._u8array = newFile._u8array; + return this; +} +BinFile.prototype.removeLeadingBytes = function (nBytes) { + this.seek(0); + var oldData = this.readBytes(nBytes); + var newFile = this.slice(nBytes.length); + + this.fileSize = newFile.fileSize; + this._u8array = newFile._u8array; + return oldData; +} + + +BinFile.prototype.copyTo = function (target, offsetSource, len, offsetTarget) { + if (!(target instanceof BinFile)) + throw new Error('target is not a BinFile object'); + + if (typeof offsetTarget !== 'number') + offsetTarget = offsetSource; + + len = len || (this.fileSize - offsetSource); + + for (var i = 0; i < len; i++) { + target._u8array[offsetTarget + i] = this._u8array[offsetSource + i]; + } +} + + +BinFile.prototype.save = function () { + if (BinFile.RUNTIME_ENVIROMENT === 'browser') { + var fileBlob = new Blob([this._u8array], { type: this.fileType }); + var blobUrl = URL.createObjectURL(fileBlob); + var a = document.createElement('a'); + a.href = blobUrl; + a.download = this.fileName; + document.body.appendChild(a); + a.dispatchEvent(new MouseEvent('click')); + URL.revokeObjectURL(blobUrl); + document.body.removeChild(a); + } else if (BinFile.RUNTIME_ENVIROMENT === 'node') { + nodeFs.writeFileSync(this.fileName, Buffer.from(this._u8array.buffer)); + } else { + throw new Error('invalid runtime environment, can\'t save file'); + } +} + + +BinFile.prototype.getExtension = function () { + var ext = this.fileName ? this.fileName.toLowerCase().match(/\.(\w+)$/) : ''; + + return ext ? ext[1] : ''; +} +BinFile.prototype.getName = function () { + return this.fileName.replace(new RegExp('\\.' + this.getExtension() + '$', 'i'), ''); +} +BinFile.prototype.setExtension = function (newExtension) { + return (this.fileName = this.getName() + '.' + newExtension); +} +BinFile.prototype.setName = function (newName) { + return (this.fileName = newName + '.' + this.getExtension()); +} + + +BinFile.prototype.readU8 = function () { + this._lastRead = this._u8array[this.offset++]; + + return this._lastRead +} +BinFile.prototype.readU16 = function () { + if (this.littleEndian) + this._lastRead = this._u8array[this.offset] + (this._u8array[this.offset + 1] << 8); + else + this._lastRead = (this._u8array[this.offset] << 8) + this._u8array[this.offset + 1]; + + this.offset += 2; + return this._lastRead >>> 0 +} +BinFile.prototype.readU24 = function () { + if (this.littleEndian) + this._lastRead = this._u8array[this.offset] + (this._u8array[this.offset + 1] << 8) + (this._u8array[this.offset + 2] << 16); + else + this._lastRead = (this._u8array[this.offset] << 16) + (this._u8array[this.offset + 1] << 8) + this._u8array[this.offset + 2]; + + this.offset += 3; + return this._lastRead >>> 0 +} +BinFile.prototype.readU32 = function () { + if (this.littleEndian) + this._lastRead = this._u8array[this.offset] + (this._u8array[this.offset + 1] << 8) + (this._u8array[this.offset + 2] << 16) + (this._u8array[this.offset + 3] << 24); + else + this._lastRead = (this._u8array[this.offset] << 24) + (this._u8array[this.offset + 1] << 16) + (this._u8array[this.offset + 2] << 8) + this._u8array[this.offset + 3]; + + this.offset += 4; + return this._lastRead >>> 0 +} +BinFile.prototype.readU64 = function () { + if (this.littleEndian) + this._lastRead = this._u8array[this.offset] + (this._u8array[this.offset + 1] << 8) + (this._u8array[this.offset + 2] << 16) + (this._u8array[this.offset + 3] << 24) + (this._u8array[this.offset + 4] << 32) + (this._u8array[this.offset + 5] << 40) + (this._u8array[this.offset + 6] << 48) + (this._u8array[this.offset + 7] << 56); + else + this._lastRead = (this._u8array[this.offset] << 56) + (this._u8array[this.offset + 1] << 48) + (this._u8array[this.offset + 2] << 40) + (this._u8array[this.offset + 3] << 32) + (this._u8array[this.offset + 4] << 24) + (this._u8array[this.offset + 5] << 16) + (this._u8array[this.offset + 6] << 8) + this._u8array[this.offset + 7]; + this.offset += 8; + return this._lastRead >>> 0 +} + + + +BinFile.prototype.readBytes = function (len) { + this._lastRead = new Array(len); + for (var i = 0; i < len; i++) { + this._lastRead[i] = this._u8array[this.offset + i]; + } + + this.offset += len; + return this._lastRead +} + +BinFile.prototype.readString = function (len) { + this._lastRead = ''; + for (var i = 0; i < len && (this.offset + i) < this.fileSize && this._u8array[this.offset + i] > 0; i++) + this._lastRead = this._lastRead + String.fromCharCode(this._u8array[this.offset + i]); + + this.offset += len; + return this._lastRead +} + +BinFile.prototype.writeU8 = function (u8) { + this._u8array[this.offset++] = u8; +} +BinFile.prototype.writeU16 = function (u16) { + if (this.littleEndian) { + this._u8array[this.offset] = u16 & 0xff; + this._u8array[this.offset + 1] = u16 >> 8; + } else { + this._u8array[this.offset] = u16 >> 8; + this._u8array[this.offset + 1] = u16 & 0xff; + } + + this.offset += 2; +} +BinFile.prototype.writeU24 = function (u24) { + if (this.littleEndian) { + this._u8array[this.offset] = u24 & 0x0000ff; + this._u8array[this.offset + 1] = (u24 & 0x00ff00) >> 8; + this._u8array[this.offset + 2] = (u24 & 0xff0000) >> 16; + } else { + this._u8array[this.offset] = (u24 & 0xff0000) >> 16; + this._u8array[this.offset + 1] = (u24 & 0x00ff00) >> 8; + this._u8array[this.offset + 2] = u24 & 0x0000ff; + } + + this.offset += 3; +} +BinFile.prototype.writeU32 = function (u32) { + if (this.littleEndian) { + this._u8array[this.offset] = u32 & 0x000000ff; + this._u8array[this.offset + 1] = (u32 & 0x0000ff00) >> 8; + this._u8array[this.offset + 2] = (u32 & 0x00ff0000) >> 16; + this._u8array[this.offset + 3] = (u32 & 0xff000000) >> 24; + } else { + this._u8array[this.offset] = (u32 & 0xff000000) >> 24; + this._u8array[this.offset + 1] = (u32 & 0x00ff0000) >> 16; + this._u8array[this.offset + 2] = (u32 & 0x0000ff00) >> 8; + this._u8array[this.offset + 3] = u32 & 0x000000ff; + } + + this.offset += 4; +} + + +BinFile.prototype.writeBytes = function (a) { + for (var i = 0; i < a.length; i++) + this._u8array[this.offset + i] = a[i] + + this.offset += a.length; +} + +BinFile.prototype.writeString = function (str, len) { + len = len || str.length; + for (var i = 0; i < str.length && i < len; i++) + this._u8array[this.offset + i] = str.charCodeAt(i); + + for (; i < len; i++) + this._u8array[this.offset + i] = 0x00; + + this.offset += len; +} + + +BinFile.prototype.swapBytes = function (swapSize, newFile) { + if (typeof swapSize !== 'number') { + swapSize = 4; + } + + if (this.fileSize % swapSize !== 0) { + throw new Error('file size is not divisible by ' + swapSize); + } + + var swappedFile = new BinFile(this.fileSize); + this.seek(0); + while (!this.isEOF()) { + swappedFile.writeBytes( + this.readBytes(swapSize).reverse() + ); + } + + if (newFile) { + swappedFile.fileName = this.fileName; + swappedFile.fileType = this.fileType; + + return swappedFile; + } else { + this._u8array = swappedFile._u8array; + + return this; + } + +} + + + + + +BinFile.prototype.hashSHA1 = async function (start, len) { + if (typeof HashCalculator !== 'object' || typeof HashCalculator.sha1 !== 'function') + throw new Error('no Hash object found or missing sha1 function'); + + return HashCalculator.sha1(this.slice(start, len, true)._u8array.buffer); +} +BinFile.prototype.hashMD5 = function (start, len) { + if (typeof HashCalculator !== 'object' || typeof HashCalculator.md5 !== 'function') + throw new Error('no Hash object found or missing md5 function'); + + return HashCalculator.md5(this.slice(start, len, true)._u8array.buffer); +} +BinFile.prototype.hashCRC32 = function (start, len) { + if (typeof HashCalculator !== 'object' || typeof HashCalculator.crc32 !== 'function') + throw new Error('no Hash object found or missing crc32 function'); + + return HashCalculator.crc32(this.slice(start, len, true)._u8array.buffer); +} +BinFile.prototype.hashAdler32 = function (start, len) { + if (typeof HashCalculator !== 'object' || typeof HashCalculator.adler32 !== 'function') + throw new Error('no Hash object found or missing adler32 function'); + + return HashCalculator.adler32(this.slice(start, len, true)._u8array.buffer); +} +BinFile.prototype.hashCRC16 = function (start, len) { + if (typeof HashCalculator !== 'object' || typeof HashCalculator.crc16 !== 'function') + throw new Error('no Hash object found or missing crc16 function'); + + return HashCalculator.crc16(this.slice(start, len, true)._u8array.buffer); +} + + + + + + + + + + + + + + + +if (BinFile.RUNTIME_ENVIROMENT === 'node' && typeof module !== 'undefined' && module.exports) { + module.exports = BinFile; + HashCalculator = require('./HashCalculator'); + nodePath = require('path'); + nodeFs = require('fs'); +} diff --git a/public/rom-patcher-js/modules/HashCalculator.js b/public/rom-patcher-js/modules/HashCalculator.js new file mode 100644 index 0000000..1d9d3cb --- /dev/null +++ b/public/rom-patcher-js/modules/HashCalculator.js @@ -0,0 +1,179 @@ +/* +* HashCalculator.js (last update: 2021-08-15) +* by Marc Robledo, https://www.marcrobledo.com +* +* data hash calculator (CRC32, MD5, SHA1, ADLER-32, CRC16) +* +* MIT License +* +* Copyright (c) 2016-2021 Marc Robledo +* +* 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. +*/ + + +const HashCalculator = (function () { + const HEX_CHR = '0123456789abcdef'.split(''); + + /* MD5 helpers */ + const _add32 = function (a, b) { return (a + b) & 0xffffffff } + const _md5cycle = function (x, k) { var a = x[0], b = x[1], c = x[2], d = x[3]; a = ff(a, b, c, d, k[0], 7, -680876936); d = ff(d, a, b, c, k[1], 12, -389564586); c = ff(c, d, a, b, k[2], 17, 606105819); b = ff(b, c, d, a, k[3], 22, -1044525330); a = ff(a, b, c, d, k[4], 7, -176418897); d = ff(d, a, b, c, k[5], 12, 1200080426); c = ff(c, d, a, b, k[6], 17, -1473231341); b = ff(b, c, d, a, k[7], 22, -45705983); a = ff(a, b, c, d, k[8], 7, 1770035416); d = ff(d, a, b, c, k[9], 12, -1958414417); c = ff(c, d, a, b, k[10], 17, -42063); b = ff(b, c, d, a, k[11], 22, -1990404162); a = ff(a, b, c, d, k[12], 7, 1804603682); d = ff(d, a, b, c, k[13], 12, -40341101); c = ff(c, d, a, b, k[14], 17, -1502002290); b = ff(b, c, d, a, k[15], 22, 1236535329); a = gg(a, b, c, d, k[1], 5, -165796510); d = gg(d, a, b, c, k[6], 9, -1069501632); c = gg(c, d, a, b, k[11], 14, 643717713); b = gg(b, c, d, a, k[0], 20, -373897302); a = gg(a, b, c, d, k[5], 5, -701558691); d = gg(d, a, b, c, k[10], 9, 38016083); c = gg(c, d, a, b, k[15], 14, -660478335); b = gg(b, c, d, a, k[4], 20, -405537848); a = gg(a, b, c, d, k[9], 5, 568446438); d = gg(d, a, b, c, k[14], 9, -1019803690); c = gg(c, d, a, b, k[3], 14, -187363961); b = gg(b, c, d, a, k[8], 20, 1163531501); a = gg(a, b, c, d, k[13], 5, -1444681467); d = gg(d, a, b, c, k[2], 9, -51403784); c = gg(c, d, a, b, k[7], 14, 1735328473); b = gg(b, c, d, a, k[12], 20, -1926607734); a = hh(a, b, c, d, k[5], 4, -378558); d = hh(d, a, b, c, k[8], 11, -2022574463); c = hh(c, d, a, b, k[11], 16, 1839030562); b = hh(b, c, d, a, k[14], 23, -35309556); a = hh(a, b, c, d, k[1], 4, -1530992060); d = hh(d, a, b, c, k[4], 11, 1272893353); c = hh(c, d, a, b, k[7], 16, -155497632); b = hh(b, c, d, a, k[10], 23, -1094730640); a = hh(a, b, c, d, k[13], 4, 681279174); d = hh(d, a, b, c, k[0], 11, -358537222); c = hh(c, d, a, b, k[3], 16, -722521979); b = hh(b, c, d, a, k[6], 23, 76029189); a = hh(a, b, c, d, k[9], 4, -640364487); d = hh(d, a, b, c, k[12], 11, -421815835); c = hh(c, d, a, b, k[15], 16, 530742520); b = hh(b, c, d, a, k[2], 23, -995338651); a = ii(a, b, c, d, k[0], 6, -198630844); d = ii(d, a, b, c, k[7], 10, 1126891415); c = ii(c, d, a, b, k[14], 15, -1416354905); b = ii(b, c, d, a, k[5], 21, -57434055); a = ii(a, b, c, d, k[12], 6, 1700485571); d = ii(d, a, b, c, k[3], 10, -1894986606); c = ii(c, d, a, b, k[10], 15, -1051523); b = ii(b, c, d, a, k[1], 21, -2054922799); a = ii(a, b, c, d, k[8], 6, 1873313359); d = ii(d, a, b, c, k[15], 10, -30611744); c = ii(c, d, a, b, k[6], 15, -1560198380); b = ii(b, c, d, a, k[13], 21, 1309151649); a = ii(a, b, c, d, k[4], 6, -145523070); d = ii(d, a, b, c, k[11], 10, -1120210379); c = ii(c, d, a, b, k[2], 15, 718787259); b = ii(b, c, d, a, k[9], 21, -343485551); x[0] = _add32(a, x[0]); x[1] = _add32(b, x[1]); x[2] = _add32(c, x[2]); x[3] = _add32(d, x[3]) } + const _md5blk = function (d) { var md5blks = [], i; for (i = 0; i < 64; i += 4)md5blks[i >> 2] = d[i] + (d[i + 1] << 8) + (d[i + 2] << 16) + (d[i + 3] << 24); return md5blks } + const _cmn = function (q, a, b, x, s, t) { a = _add32(_add32(a, q), _add32(x, t)); return _add32((a << s) | (a >>> (32 - s)), b) } + const ff = function (a, b, c, d, x, s, t) { return _cmn((b & c) | ((~b) & d), a, b, x, s, t) } + const gg = function (a, b, c, d, x, s, t) { return _cmn((b & d) | (c & (~d)), a, b, x, s, t) } + const hh = function (a, b, c, d, x, s, t) { return _cmn(b ^ c ^ d, a, b, x, s, t) } + const ii = function (a, b, c, d, x, s, t) { return _cmn(c ^ (b | (~d)), a, b, x, s, t) } + + /* CRC32 helpers */ + const CRC32_TABLE = (function () { + var c, crcTable = []; + for (var n = 0; n < 256; n++) { + c = n; + for (var k = 0; k < 8; k++) + c = ((c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1)); + crcTable[n] = c; + } + return crcTable; + }()); + + /* Adler-32 helpers */ + const ADLER32_MOD = 0xfff1; + + + + + const generateUint8Array = function (arrayBuffer, offset, len) { + if (typeof offset !== 'number' || offset < 0) + offset = 0; + else if (offset < arrayBuffer.byteLength) + offset = Math.floor(offset); + else + throw new Error('out of bounds slicing'); + + if (typeof len !== 'number' || len < 0 || (offset + len) >= arrayBuffer.byteLength.length) + len = arrayBuffer.byteLength - offset; + else if (len > 0) + len = Math.floor(len); + else + throw new Error('zero length provided for slicing'); + + return new Uint8Array(arrayBuffer, offset, len); + } + + + return { + /* SHA-1 using WebCryptoAPI */ + sha1: async function sha1(arrayBuffer, offset, len) { + if(typeof window === 'undefined' || typeof window.crypto === 'undefined') + throw new Error('Web Crypto API is not available'); + + const u8array = generateUint8Array(arrayBuffer, offset, len); + if (u8array.byteLength !== arrayBuffer.byteLength) { + arrayBuffer = arrayBuffer.slice(u8array.byteOffset, u8array.byteOffset + u8array.byteLength); + } + + const hash = await window.crypto.subtle.digest('SHA-1', arrayBuffer); + + const bytes = new Uint8Array(hash); + let hexString = ''; + for (let i = 0; i < bytes.length; i++) + hexString += bytes[i] < 16 ? '0' + bytes[i].toString(16) : bytes[i].toString(16); + return hexString; + }, + + /* MD5 - from Joseph's Myers - http://www.myersdaily.org/joseph/javascript/md5.js */ + md5: function (arrayBuffer, offset, len) { + let u8array = generateUint8Array(arrayBuffer, offset, len); + + var n = u8array.byteLength, state = [1732584193, -271733879, -1732584194, 271733878], i; + for (i = 64; i <= u8array.byteLength; i += 64) + _md5cycle(state, _md5blk(u8array.slice(i - 64, i))); + u8array = u8array.slice(i - 64); + var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < u8array.byteLength; i++) + tail[i >> 2] |= u8array[i] << ((i % 4) << 3); + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + _md5cycle(state, tail); + for (i = 0; i < 16; i++)tail[i] = 0; + } + tail[14] = n * 8; + tail[15] = Math.floor(n / 536870912) >>> 0; //if file is bigger than 512Mb*8, value is bigger than 32 bits, so it needs two words to store its length + _md5cycle(state, tail); + + for (var i = 0; i < state.length; i++) { + var s = '', j = 0; + for (; j < 4; j++) + s += HEX_CHR[(state[i] >> (j * 8 + 4)) & 0x0f] + HEX_CHR[(state[i] >> (j * 8)) & 0x0f]; + state[i] = s; + } + return state.join('') + }, + + /* CRC32 - from Alex - https://stackoverflow.com/a/18639999 */ + crc32: function (arrayBuffer, offset, len) { + const u8array = generateUint8Array(arrayBuffer, offset, len); + + var crc = 0 ^ (-1); + + for (var i = 0; i < u8array.byteLength; i++) + crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ u8array[i]) & 0xff]; + + return ((crc ^ (-1)) >>> 0); + }, + + /* Adler-32 - https://en.wikipedia.org/wiki/Adler-32#Example_implementation */ + adler32: function (arrayBuffer, offset, len) { + const u8array = generateUint8Array(arrayBuffer, offset, len); + + var a = 1, b = 0; + + for (var i = 0; i < u8array.byteLength; i++) { + a = (a + u8array[i]) % ADLER32_MOD; + b = (b + a) % ADLER32_MOD; + } + + return ((b << 16) | a) >>> 0; + }, + + /* CRC16/CCITT-FALSE */ + crc16: function (arrayBuffer, offset, len) { + const u8array = generateUint8Array(arrayBuffer, offset, len); + + var crc = 0xffff; + + var offset = 0; + + for (var i = 0; i < u8array.byteLength; i++) { + crc ^= u8array[offset++] << 8; + for (j = 0; j < 8; ++j) { + crc = (crc & 0x8000) >>> 0 ? (crc << 1) ^ 0x1021 : crc << 1; + } + } + + return crc & 0xffff; + } + } +}()); + + +if (typeof module !== 'undefined' && module.exports) { + module.exports = HashCalculator; +} diff --git a/public/rom-patcher-js/modules/RomPatcher.format.aps_gba.js b/public/rom-patcher-js/modules/RomPatcher.format.aps_gba.js new file mode 100644 index 0000000..66e258f --- /dev/null +++ b/public/rom-patcher-js/modules/RomPatcher.format.aps_gba.js @@ -0,0 +1,114 @@ +/* APS (GBA) module for Rom Patcher JS v20230331 - Marc Robledo 2017-2023 - http://www.marcrobledo.com/license */ +/* File format specification: https://github.com/btimofeev/UniPatcher/wiki/APS-(GBA) */ + +const APS_GBA_MAGIC='APS1'; +const APS_GBA_BLOCK_SIZE=0x010000; //64Kb +const APS_GBA_RECORD_SIZE=4 + 2 + 2 + APS_GBA_BLOCK_SIZE; +if(typeof module !== "undefined" && module.exports){ + module.exports = APSGBA; +} +function APSGBA(){ + this.sourceSize=0; + this.targetSize=0; + this.records=[]; +} +APSGBA.prototype.addRecord=function(offset, sourceCrc16, targetCrc16, xorBytes){ + this.records.push({ + offset:offset, + sourceCrc16:sourceCrc16, + targetCrc16:targetCrc16, + xorBytes:xorBytes} + ); +} +APSGBA.prototype.toString=function(){ + var s='Total records: '+this.records.length; + s+='\nInput file size: '+this.sourceSize; + s+='\nOutput file size: '+this.targetSize; + return s +} +APSGBA.prototype.validateSource=function(sourceFile){ + if(sourceFile.fileSize!==this.sourceSize) + return false; + + for(var i=0; i2){ + patch.addRLERecord(offset, differentBytes[0], differentBytes.length); + }else{ + patch.addRecord(offset, differentBytes); + } + } + } + + return patch +} \ No newline at end of file diff --git a/public/rom-patcher-js/modules/RomPatcher.format.bdf.js b/public/rom-patcher-js/modules/RomPatcher.format.bdf.js new file mode 100644 index 0000000..0367a08 --- /dev/null +++ b/public/rom-patcher-js/modules/RomPatcher.format.bdf.js @@ -0,0 +1,90 @@ +/* BDF module for Rom Patcher JS v20250922 - Marc Robledo 2025 - http://www.marcrobledo.com/license */ +/* File format specification: https://www.daemonology.net/bsdiff/ */ + +const BDF_MAGIC='BSDIFF40'; + +if(typeof module !== "undefined" && module.exports){ + module.exports = BDF; +} + +function BDF(){ + this.records=[]; + this.patchedSize=0; +} + +BDF.prototype.apply=function(file){ + var tempFile=new BinFile(this.patchedSize); + + for (const record of this.records) { + for (const b of record.diff) { + tempFile.writeU8(file.readU8() + b); + } + tempFile.writeBytes(record.extra); + file.seek(file.offset + record.skip); + } + return tempFile; +} + +BDF.MAGIC = BDF_MAGIC; + +BDF.fromFile=function(file){ + var patch=new BDF(); + + file.seek(8); + file.littleEndian=true; + var controlSize=file.readU64(); + var diffSize=file.readU64(); + patch.patchedSize=file.readU64(); + + var controlCompressed=file.readBytes(controlSize); + var diffCompressed=file.readBytes(diffSize); + var extraCompressed=file.readBytes(file.fileSize-file.offset); + + var controlFile=new BinFile(bz2.decompress(controlCompressed)); + controlFile.littleEndian=true; + var diffFile=new BinFile(bz2.decompress(diffCompressed)); + var extraFile=new BinFile(bz2.decompress(extraCompressed)); + + while(!controlFile.isEOF()){ + var diffLen=controlFile.readU64(); + var extraLen=controlFile.readU64(); + var skip=controlFile.readU64(); + if(skip&(1<<63)) + skip=-(skip&~(1<<63)); + var diff=diffFile.readBytes(diffLen); + var extra=extraFile.readBytes(extraLen); + patch.records.push({diff, extra, skip}); + } + + return patch; +} + + + + +/* +bz2 (C) 2019-present SheetJS LLC + +Copyright 2019 SheetJS LLC + +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. + +*/ +"use strict"; +const bz2=function $(){let x=[0,79764919,159529838,222504665,319059676,398814059,445009330,507990021,638119352,583659535,797628118,726387553,890018660,835552979,1015980042,944750013,1276238704,1221641927,1167319070,1095957929,1595256236,1540665371,1452775106,1381403509,1780037320,1859660671,1671105958,1733955601,2031960084,2111593891,1889500026,1952343757,2552477408,2632100695,2443283854,2506133561,2334638140,2414271883,2191915858,2254759653,3190512472,3135915759,3081330742,3009969537,2905550212,2850959411,2762807018,2691435357,3560074640,3505614887,3719321342,3648080713,3342211916,3287746299,3467911202,3396681109,4063920168,4143685023,4223187782,4286162673,3779000052,3858754371,3904687514,3967668269,881225847,809987520,1023691545,969234094,662832811,591600412,771767749,717299826,311336399,374308984,453813921,533576470,25881363,88864420,134795389,214552010,2023205639,2086057648,1897238633,1976864222,1804852699,1867694188,1645340341,1724971778,1587496639,1516133128,1461550545,1406951526,1302016099,1230646740,1142491917,1087903418,2896545431,2825181984,2770861561,2716262478,3215044683,3143675388,3055782693,3001194130,2326604591,2389456536,2200899649,2280525302,2578013683,2640855108,2418763421,2498394922,3769900519,3832873040,3912640137,3992402750,4088425275,4151408268,4197601365,4277358050,3334271071,3263032808,3476998961,3422541446,3585640067,3514407732,3694837229,3640369242,1762451694,1842216281,1619975040,1682949687,2047383090,2127137669,1938468188,2001449195,1325665622,1271206113,1183200824,1111960463,1543535498,1489069629,1434599652,1363369299,622672798,568075817,748617968,677256519,907627842,853037301,1067152940,995781531,51762726,131386257,177728840,240578815,269590778,349224269,429104020,491947555,4046411278,4126034873,4172115296,4234965207,3794477266,3874110821,3953728444,4016571915,3609705398,3555108353,3735388376,3664026991,3290680682,3236090077,3449943556,3378572211,3174993278,3120533705,3032266256,2961025959,2923101090,2868635157,2813903052,2742672763,2604032198,2683796849,2461293480,2524268063,2284983834,2364738477,2175806836,2238787779,1569362073,1498123566,1409854455,1355396672,1317987909,1246755826,1192025387,1137557660,2072149281,2135122070,1912620623,1992383480,1753615357,1816598090,1627664531,1707420964,295390185,358241886,404320391,483945776,43990325,106832002,186451547,266083308,932423249,861060070,1041341759,986742920,613929101,542559546,756411363,701822548,3316196985,3244833742,3425377559,3370778784,3601682597,3530312978,3744426955,3689838204,3819031489,3881883254,3928223919,4007849240,4037393693,4100235434,4180117107,4259748804,2310601993,2373574846,2151335527,2231098320,2596047829,2659030626,2470359227,2550115596,2947551409,2876312838,2788305887,2733848168,3165939309,3094707162,3040238851,2985771188,],f=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535,131071,262143,524287,1048575,2097151,4194303,8388607,16777215,33554431,67108863,134217727,268435455,536870911,1073741823,-2147483648,];function e($){let x=[];for(let f=0;f<$.length;f+=1)x.push([f,$[f]]);x.push([$.length,-1]);let e=[],d=x[0][0],b=x[0][1];for(let _=0;_$.bits-x.bits||$.code-x.code);let l=0,o=-1,r=[],n;for(let s=0;s=$.length)throw RangeError("Out of bound");let f=$.slice();$.sort(($,x)=>$-x);let e={};for(let d=$.length-1;d>=0;d-=1)e[$[d]]=d;let b=[];for(let _=0;_<$.length;_+=1)b.push(e[f[_]]++);let a,c=$[a=x],t=[];for(let l=1;l<$.length;l+=1){let o=$[a=b[a]];void 0===o?t.push(255):t.push(o)}return t.push(c),t.reverse(),t}let b={decompress:function $(b,_=!1){let a=0,c=0,t=0,l=$=>{if($>=32){let x=$>>1;return l(x)*(1<>t-$&e;return t-=$,c&=~(e<=49&&n<=57)n-=48;else throw Error("Invalid blocksize");let s=new Uint8Array(1.5*b.length),i=0,h=-1;for(;;){let u=l(48),p=0|l(32);if(54156738319193===u){if(l(1))throw Error("do not support randomised");let g=l(24),w=[],m=l(16);for(let v=32768;v>0;v>>=1){if(!(m&v)){for(let y=0;y<16;y+=1)w.push(!1);continue}let k=l(16);for(let I=32768;I>0;I>>=1)w.push(!!(k&I))}let A=l(3);if(A<2||A>6)throw Error("Invalid number of huffman groups");let z=l(15),C=[],O=Array.from({length:A},($,x)=>x);for(let F=0;F=A)throw Error("MTF table out of range");let M=O[H];for(let P=H;P>0;O[P]=O[--P]);C.push(M),O[0]=M}let R=w.reduce(($,x)=>$+x,0)+2,T=[];for(let j=0;j20)throw Error("Huffman group length outside range");for(;l(1);)q-=2*l(1)-1;B.push(q)}T.push(e(B))}let E=[];for(let G=0;G>t-V])){c&=f[t-=V],N=N.code;break}if(N>=0&&N<=1){0===Q&&(S=1),Q+=S<0;Q-=1)U.push(W)}if(N===R-1)break;{let X=E[N-1];for(let Y=N-1;Y>0;E[Y]=E[--Y]);E[0]=X,U.push(X)}}let Z=d(U,g),$$=0;for(;$$=s.length){let $e=s;(s=new Uint8Array(2*$e.length)).set($e)}for(let $d=0;$d<$f;$d+=1)_&&(h=h<<8^x[(h>>24^$x)&255]),s[i]=$x,i+=1}if(_){let $b=-1^h;if($b!==p)throw Error(`CRC mismatch: ${$b} !== ${p}`);h=-1}}else if(25779555029136===u){l(7&t);break}else throw Error("Invalid bz2 blocktype")}return s.subarray(0,i)}};return b}(); \ No newline at end of file diff --git a/public/rom-patcher-js/modules/RomPatcher.format.bps.js b/public/rom-patcher-js/modules/RomPatcher.format.bps.js new file mode 100644 index 0000000..b2e8266 --- /dev/null +++ b/public/rom-patcher-js/modules/RomPatcher.format.bps.js @@ -0,0 +1,466 @@ +/* BPS module for Rom Patcher JS v20240821 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */ +/* File format specification: https://www.romhacking.net/documents/746/ */ + +const BPS_MAGIC='BPS1'; +const BPS_ACTION_SOURCE_READ=0; +const BPS_ACTION_TARGET_READ=1; +const BPS_ACTION_SOURCE_COPY=2; +const BPS_ACTION_TARGET_COPY=3; +if(typeof module !== "undefined" && module.exports){ + module.exports = BPS; +} + +function BPS(){ + this.sourceSize=0; + this.targetSize=0; + this.metaData=''; + this.actions=[]; + this.sourceChecksum=0; + this.targetChecksum=0; + this.patchChecksum=0; +} +BPS.prototype.toString=function(){ + var s='Source size: '+this.sourceSize; + s+='\nTarget size: '+this.targetSize; + s+='\nMetadata: '+this.metaData; + s+='\n#Actions: '+this.actions.length; + return s +} +BPS.prototype.calculateFileChecksum = function () { + var patchFile = this.export(); + return patchFile.hashCRC32(0, patchFile.fileSize - 4); +} +BPS.prototype.validateSource=function(romFile,headerSize){return this.sourceChecksum===romFile.hashCRC32(headerSize)} +BPS.prototype.getValidationInfo=function(){ + return { + 'type':'CRC32', + 'value':this.sourceChecksum + } +} +BPS.prototype.apply=function(romFile, validate){ + if(validate && !this.validateSource(romFile)){ + throw new Error('Source ROM checksum mismatch'); + } + + + tempFile=new BinFile(this.targetSize); + + + //patch + var sourceRelativeOffset=0; + var targetRelativeOffset=0; + for(var i=0; i> 2)+1}; + + if(action.type===BPS_ACTION_TARGET_READ){ + action.bytes=file.readBytes(action.length); + + }else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){ + var relativeOffset=file.readVLV(); + action.relativeOffset=(relativeOffset & 1? -1 : +1) * (relativeOffset >> 1) + } + + patch.actions.push(action); + } + + //file.seek(endActionsOffset); + patch.sourceChecksum=file.readU32(); + patch.targetChecksum=file.readU32(); + patch.patchChecksum=file.readU32(); + + if (patch.patchChecksum !== patch.calculateFileChecksum()) { + throw new Error('Patch checksum mismatch'); + } + + + return patch; +} + + + +function BPS_readVLV(){ + var data=0, shift=1; + while(true){ + var x = this.readU8(); + data += (x & 0x7f) * shift; + if(x & 0x80) + break; + shift <<= 7; + data += shift; + } + + this._lastRead=data; + return data; +} +function BPS_writeVLV(data){ + while(true){ + var x = data & 0x7f; + data >>= 7; + if(data === 0){ + this.writeU8(0x80 | x); + break; + } + this.writeU8(x); + data--; + } +} +function BPS_getVLVLen(data){ + var len=0; + while(true){ + var x = data & 0x7f; + data >>= 7; + if(data === 0){ + len++; + break; + } + len++; + data--; + } + return len; +} + + +BPS.prototype.export=function(fileName){ + var patchFileSize=BPS_MAGIC.length; + patchFileSize+=BPS_getVLVLen(this.sourceSize); + patchFileSize+=BPS_getVLVLen(this.targetSize); + patchFileSize+=BPS_getVLVLen(this.metaData.length); + patchFileSize+=this.metaData.length; + for(var i=0; i= 4) { + //write byte to repeat + targetReadLength++; + outputOffset++; + targetReadFlush(); + + //copy starting from repetition byte + //encode(TargetCopy | ((rleLength - 1) << 2)); + var relativeOffset = (outputOffset - 1) - targetRelativeOffset; + //encode(relativeOffset << 1); + patchActions.push({type:BPS_ACTION_TARGET_COPY, length:rleLength, relativeOffset:relativeOffset}); + outputOffset += rleLength; + targetRelativeOffset = outputOffset - 1; + } else if(sourceLength >= 4) { + targetReadFlush(); + //encode(SourceRead | ((sourceLength - 1) << 2)); + patchActions.push({type:BPS_ACTION_SOURCE_READ, length:sourceLength}); + outputOffset += sourceLength; + } else { + targetReadLength += Granularity; + outputOffset += Granularity; + } + } + + targetReadFlush(); + + + + return patchActions; +} + +/* delta implementation from https://github.com/chiya/beat/blob/master/nall/beat/delta.hpp */ +function createBPSFromFilesDelta(original, modified){ + var patchActions=[]; + + + /* references to match original beat code */ + var sourceData=original._u8array; + var targetData=modified._u8array; + var sourceSize=original.fileSize; + var targetSize=modified.fileSize; + var Granularity=1; + + + + var sourceRelativeOffset=0; + var targetRelativeOffset=0; + var outputOffset=0; + + + + var sourceTree=new Array(65536); + var targetTree=new Array(65536); + for(var n=0; n<65536; n++){ + sourceTree[n]=null; + targetTree[n]=null; + } + + + + //source tree creation + for(var offset=0; offset maxLength) maxLength = length, mode = BPS_ACTION_SOURCE_READ; + } + + { //source copy + var node = sourceTree[symbol]; + while(node) { + var length = 0, x = node.offset, y = outputOffset; + while(x < sourceSize && y < targetSize && sourceData[x++] == targetData[y++]) length++; + if(length > maxLength) maxLength = length, maxOffset = node.offset, mode = BPS_ACTION_SOURCE_COPY; + node = node.next; + } + } + + { //target copy + var node = targetTree[symbol]; + while(node) { + var length = 0, x = node.offset, y = outputOffset; + while(y < targetSize && targetData[x++] == targetData[y++]) length++; + if(length > maxLength) maxLength = length, maxOffset = node.offset, mode = BPS_ACTION_TARGET_COPY; + node = node.next; + } + + //target tree append + node = new BPS_Node(); + node.offset = outputOffset; + node.next = targetTree[symbol]; + targetTree[symbol] = node; + } + + { //target read + if(maxLength < 4) { + maxLength = Math.min(Granularity, targetSize - outputOffset); + mode = BPS_ACTION_TARGET_READ; + } + } + + if(mode != BPS_ACTION_TARGET_READ) targetReadFlush(); + + switch(mode) { + case BPS_ACTION_SOURCE_READ: + //encode(BPS_ACTION_SOURCE_READ | ((maxLength - 1) << 2)); + patchActions.push({type:BPS_ACTION_SOURCE_READ, length:maxLength}); + break; + case BPS_ACTION_TARGET_READ: + //delay write to group sequential TargetRead commands into one + targetReadLength += maxLength; + break; + case BPS_ACTION_SOURCE_COPY: + case BPS_ACTION_TARGET_COPY: + //encode(mode | ((maxLength - 1) << 2)); + var relativeOffset; + if(mode == BPS_ACTION_SOURCE_COPY) { + relativeOffset = maxOffset - sourceRelativeOffset; + sourceRelativeOffset = maxOffset + maxLength; + } else { + relativeOffset = maxOffset - targetRelativeOffset; + targetRelativeOffset = maxOffset + maxLength; + } + //encode((relativeOffset < 0) | (abs(relativeOffset) << 1)); + patchActions.push({type:mode, length:maxLength, relativeOffset:relativeOffset}); + break; + } + + outputOffset += maxLength; + } + + targetReadFlush(); + + + return patchActions; +} \ No newline at end of file diff --git a/public/rom-patcher-js/modules/RomPatcher.format.ips.js b/public/rom-patcher-js/modules/RomPatcher.format.ips.js new file mode 100644 index 0000000..a7a4e87 --- /dev/null +++ b/public/rom-patcher-js/modules/RomPatcher.format.ips.js @@ -0,0 +1,282 @@ +/* IPS module for Rom Patcher JS v20250430 - Marc Robledo 2016-2025 - http://www.marcrobledo.com/license */ +/* File format specification: http://www.smwiki.net/wiki/IPS_file_format */ + +/* This file also acts as EBP (EarthBound Patch) module */ +/* EBP is actually just IPS with some JSON metadata stuck on the end (implementation: https://github.com/Lyrositor/EBPatcher) */ + +const IPS_MAGIC='PATCH'; +const IPS_MAX_ROM_SIZE=0x1000000; //16 megabytes +const IPS_RECORD_RLE=0x0000; +const IPS_RECORD_SIMPLE=0x01; + + +if(typeof module !== "undefined" && module.exports){ + module.exports = IPS; +} + + +function IPS(){ + this.records=[]; + this.truncate=false; + this.EBPmetadata=null; +} +IPS.prototype.addSimpleRecord=function(o, d){ + this.records.push({offset:o, type:IPS_RECORD_SIMPLE, length:d.length, data:d}) +} +IPS.prototype.addRLERecord=function(o, l, b){ + this.records.push({offset:o, type:IPS_RECORD_RLE, length:l, byte:b}) +} +IPS.prototype.setEBPMetadata=function(metadataObject){ + if(typeof metadataObject !== 'object') + throw new TypeError('metadataObject must be an object'); + for(var key in metadataObject){ + if(typeof metadataObject[key] !== 'string') + throw new TypeError('metadataObject values must be strings'); + } + + /* EBPatcher (linked above) expects the "patcher" field to be EBPatcher to read the metadata */ + /* CoilSnake (EB modding tool) inserts this manually too */ + /* So we also add it here for compatibility purposes */ + this.EBPmetadata={patcher:'EBPatcher', ...metadataObject}; +} +IPS.prototype.getDescription=function(){ + if(this.EBPmetadata){ + var description=''; + for(var key in this.EBPmetadata){ + if(key==='patcher') + continue; + + const keyPretty=key.charAt(0).toUpperCase() + key.slice(1); + description+=keyPretty+': '+this.EBPmetadata[key]+'\n'; + } + return description.trim(); + } + return null; +} +IPS.prototype.toString=function(){ + nSimpleRecords=0; + nRLERecords=0; + for(var i=0; iromFile.fileSize){ //expand (discussed here: https://github.com/marcrobledo/RomPatcher.js/pull/46) + tempFile=new BinFile(this.truncate); + romFile.copyTo(tempFile, 0, romFile.fileSize, 0); + }else{ //truncate + tempFile=romFile.slice(0, this.truncate); + } + }else{ + //calculate target ROM size, expanding it if any record offset is beyond target ROM size + var newFileSize=romFile.fileSize; + for(var i=0; inewFileSize){ + newFileSize=rec.offset+rec.length; + } + }else{ + if(rec.offset+rec.data.length>newFileSize){ + newFileSize=rec.offset+rec.data.length; + } + } + } + + if(newFileSize===romFile.fileSize){ + tempFile=romFile.slice(0, romFile.fileSize); + }else{ + tempFile=new BinFile(newFileSize); + romFile.copyTo(tempFile,0); + } + } + + + romFile.seek(0); + + for(var i=0; i6){ + // separate a potential RLE record + original.seek(startOffset); + modified.seek(startOffset); + previousRecord={type:0xdeadbeef,startOffset:0,length:0}; + }else{ + // merge both records + while(distance--){ + previousRecord.data.push(modified._u8array[previousRecord.offset+previousRecord.length]); + previousRecord.length++; + } + previousRecord.data=previousRecord.data.concat(differentData); + previousRecord.length=previousRecord.data.length; + } + }else{ + if(startOffset>=IPS_MAX_ROM_SIZE){ + throw new Error(`Files are too big for ${patch.EBPmetadata? 'EBP' : 'IPS'} format`); + return null; + } + + if(RLEmode && differentData.length>2){ + patch.addRLERecord(startOffset, differentData.length, differentData[0]); + }else{ + patch.addSimpleRecord(startOffset, differentData); + } + previousRecord=patch.records[patch.records.length-1]; + } + } + } + + + + + if(modified.fileSize>original.fileSize){ + var lastRecord=patch.records[patch.records.length-1]; + var lastOffset=lastRecord.offset+lastRecord.length; + + if(lastOffsetpatch.targetSize) + patch.targetSize=offset+length; + } + + return patch; +} + + + + + +/* to-do */ +//PMSR.prototype.export=function(fileName){return null} +//PMSR.buildFromRoms=function(original, modified){return null} + + +/* https://github.com/pho/WindViewer/wiki/Yaz0-and-Yay0 */ +PMSR.YAY0_decode=function(file){ + /* to-do */ +} \ No newline at end of file diff --git a/public/rom-patcher-js/modules/RomPatcher.format.ppf.js b/public/rom-patcher-js/modules/RomPatcher.format.ppf.js new file mode 100644 index 0000000..42a2c9b --- /dev/null +++ b/public/rom-patcher-js/modules/RomPatcher.format.ppf.js @@ -0,0 +1,269 @@ +/* PPF module for Rom Patcher JS v20200221 - Marc Robledo 2019-2020 - http://www.marcrobledo.com/license */ +/* File format specification: https://www.romhacking.net/utilities/353/ */ + + +const PPF_MAGIC='PPF'; +const PPF_IMAGETYPE_BIN=0x00; +const PPF_IMAGETYPE_GI=0x01; +const PPF_BEGIN_FILE_ID_DIZ_MAGIC='@BEG';//@BEGIN_FILE_ID.DIZ + +if(typeof module !== "undefined" && module.exports){ + module.exports = PPF; +} +function PPF(){ + this.version=3; + this.imageType=PPF_IMAGETYPE_BIN; + this.blockCheck=false; + this.undoData=false; + this.records=[]; +} +PPF.prototype.addRecord=function(offset, data, undoData){ + if(this.undoData){ + this.records.push({offset:offset, data:data, undoData:undoData}); + }else{ + this.records.push({offset:offset, data:data}); + } +} +PPF.prototype.toString=function(){ + var s=this.description; + s+='\nPPF version: '+this.version; + s+='\n#Records: '+this.records.length; + s+='\nImage type: '+this.imageType; + s+='\nBlock check: '+!!this.blockCheck; + s+='\nUndo data: '+this.undoData; + if(this.fileIdDiz) + s+='\nFILE_ID.DIZ: '+this.fileIdDiz; + return s +} +PPF.prototype.export=function(fileName){ + var patchFileSize=5+1+50; //PPFx0 + for(var i=0; i>>0); + tempFile.writeU32(offset2); + } + tempFile.writeU8(this.records[i].data.length); + tempFile.writeBytes(this.records[i].data); + if(this.undoData) + tempFile.writeBytes(this.records[i].undoData); + } + + if(this.fileIdDiz){ + tempFile.writeString('@BEGIN_FILE_ID.DIZ'); + tempFile.writeString(this.fileIdDiz); + tempFile.writeString('@END_FILE_ID.DIZ'); + tempFile.writeU16(this.fileIdDiz.length); + tempFile.writeU16(0x00); + } + + + + return tempFile +} +PPF.prototype.apply=function(romFile){ + var newFileSize=romFile.fileSize; + for(var i=0; inewFileSize) + newFileSize=this.records[i].offset+this.records[i].data.length; + } + if(newFileSize===romFile.fileSize){ + tempFile=romFile.slice(0, romFile.fileSize); + }else{ + tempFile=new BinFile(newFileSize); + romFile.copyTo(tempFile,0); + } + + //check if undoing + var undoingData=false; + if(this.undoData){ + tempFile.seek(this.records[0].offset); + var originalBytes=tempFile.readBytes(this.records[0].data.length); + var foundDifferences=false; + for(var i=0; i3){ + throw new Error('invalid PPF version'); + } + + patch.version=version1; + patch.description=patchFile.readString(50).replace(/ +$/,''); + + + + + if(patch.version===3){ + patch.imageType=patchFile.readU8(); + if(patchFile.readU8()) + patch.blockCheck=true; + if(patchFile.readU8()) + patch.undoData=true; + + patchFile.skip(1); + }else if(patch.version===2){ + patch.blockCheck=true; + patch.inputFileSize=patchFile.readU32(); + } + + if(patch.blockCheck){ + patch.blockCheck=patchFile.readBytes(1024); + } + + + + patchFile.littleEndian=true; + while(!patchFile.isEOF()){ + + if(patchFile.readString(4)===PPF_BEGIN_FILE_ID_DIZ_MAGIC){ + patchFile.skip(14); + //console.log('found file_id.diz begin'); + patch.fileIdDiz=patchFile.readString(3072); + patch.fileIdDiz=patch.fileIdDiz.substr(0, patch.fileIdDiz.indexOf('@END_FILE_ID.DIZ')); + break; + } + patchFile.skip(-4); + + var offset; + if(patch.version===3){ + var u64_1=patchFile.readU32(); + var u64_2=patchFile.readU32(); + offset=u64_1+(u64_2*0x100000000); + }else + offset=patchFile.readU32(); + + var len=patchFile.readU8(); + var data=patchFile.readBytes(len); + + var undoData=false; + if(patch.undoData){ + undoData=patchFile.readBytes(len); + } + + patch.addRecord(offset, data, undoData); + } + + + + return patch; +} + + +PPF.buildFromRoms=function(original, modified){ + var patch=new PPF(); + + patch.description='Patch description'; + + if(original.fileSize>modified.fileSize){ + var expandedModified=new BinFile(original.fileSize); + modified.copyTo(expandedModified,0); + modified=expandedModified; + } + + original.seek(0); + modified.seek(0); + while(!modified.isEOF()){ + var b1=original.isEOF()?0x00:original.readU8(); + var b2=modified.readU8(); + + if(b1!==b2){ + var differentData=[]; + var offset=modified.offset-1; + + while(b1!==b2 && differentData.length<0xff){ + differentData.push(b2); + + if(modified.isEOF() || differentData.length===0xff) + break; + + b1=original.isEOF()?0x00:original.readU8(); + b2=modified.readU8(); + } + + patch.addRecord(offset, differentData); + } + } + + if(original.fileSize byte ^ 0xff)); + }else if(patch.overflowMode==='M' && undo){ /* minify */ + tempFile.seek(patch.targetFileSize); + tempFile.writeBytes(patch.overflowData.map((byte) => byte ^ 0xff)); + } + + + if( + validate && + ( + (!undo && tempFile.hashMD5()!==patch.targetMD5) || + (undo && tempFile.hashMD5()!==patch.sourceMD5) + ) + ){ + throw new Error('Target ROM checksum mismatch'); + } + + if(undo) + tempFile.unpatched=true; + + return tempFile +} + +RUP.MAGIC=RUP_MAGIC; + +RUP.padZeroes=function(intVal, nBytes){ + var hexString=intVal.toString(16); + while(hexString.lengthtarget) or 'A' (source>=8; + } +} +function RUP_getVLVLen(data){ + var ret=1; + while(data){ + ret++; + data>>=8; + } + return ret; +} + + + + + + +RUP.prototype.export=function(fileName){ + var patchFileSize=2048; + for(var i=0; ifile.targetFileSize?'M':'A'); + patchFile.writeVLV(file.overflowData.length); + patchFile.writeBytes(file.overflowData); + } + + for(var j=0; j byte ^ 0xff); + modified=modified.slice(0, file.sourceFileSize); + }else if(file.sourceFileSize>file.targetFileSize){ + original.seek(file.targetFileSize); + file.overflowMode='M'; + file.overflowData=original.readBytes(file.sourceFileSize-file.targetFileSize).map((byte) => byte ^ 0xff); + original=original.slice(0, file.targetFileSize); + } + + + original.seek(0); + modified.seek(0); + + while(!modified.isEOF()){ + var b1=original.isEOF()?0x00:original.readU8(); + var b2=modified.readU8(); + + if(b1!==b2){ + var originalOffset=modified.offset-1; + var xorDifferences=[]; + + while(b1!==b2){ + xorDifferences.push(b1^b2); + + if(modified.isEOF()) + break; + + b1=original.isEOF()?0x00:original.readU8(); + b2=modified.readU8(); + } + + file.records.push({offset:originalOffset, xor:xorDifferences}); + } + } + + patch.files.push(file); + + return patch +} \ No newline at end of file diff --git a/public/rom-patcher-js/modules/RomPatcher.format.ups.js b/public/rom-patcher-js/modules/RomPatcher.format.ups.js new file mode 100644 index 0000000..56a91e9 --- /dev/null +++ b/public/rom-patcher-js/modules/RomPatcher.format.ups.js @@ -0,0 +1,224 @@ +/* UPS module for Rom Patcher JS v20240721 - Marc Robledo 2017-2024 - http://www.marcrobledo.com/license */ +/* File format specification: http://www.romhacking.net/documents/392/ */ + +const UPS_MAGIC='UPS1'; +if(typeof module !== "undefined" && module.exports){ + module.exports = UPS; +} +function UPS(){ + this.records=[]; + this.sizeInput=0; + this.sizeOutput=0; + this.checksumInput=0; + this.checksumOutput=0; +} +UPS.prototype.addRecord=function(relativeOffset, d){ + this.records.push({offset:relativeOffset, XORdata:d}) +} +UPS.prototype.toString=function(){ + var s='Records: '+this.records.length; + s+='\nInput file size: '+this.sizeInput; + s+='\nOutput file size: '+this.sizeOutput; + s+='\nInput file checksum: '+this.checksumInput.toString(16); + s+='\nOutput file checksum: '+this.checksumOutput.toString(16); + return s +} +UPS.prototype.export=function(fileName){ + var patchFileSize=UPS_MAGIC.length;//UPS1 string + patchFileSize+=UPS_getVLVLength(this.sizeInput); //input file size + patchFileSize+=UPS_getVLVLength(this.sizeOutput); //output file size + for(var i=0; i>7; + if(data===0){ + this.writeU8(0x80 | x); + break; + } + this.writeU8(x); + data=data-1; + } +} +function UPS_readVLV(){ + var data=0; + + var shift=1; + while(1){ + var x=this.readU8(); + + if(x==-1) + throw new Error('Can\'t read UPS VLV at 0x'+(this.offset-1).toString(16)); + + data+=(x&0x7f)*shift; + if((x&0x80)!==0) + break; + shift=shift<<7; + data+=shift; + } + return data +} +function UPS_getVLVLength(data){ + var len=0; + while(1){ + var x=data & 0x7f; + data=data>>7; + len++; + if(data===0){ + break; + } + data=data-1; + } + return len; +} + + +UPS.fromFile=function(file){ + var patch=new UPS(); + file.readVLV=UPS_readVLV; + + file.seek(UPS_MAGIC.length); + + patch.sizeInput=file.readVLV(); + patch.sizeOutput=file.readVLV(); + + + var nextOffset=0; + while(file.offset<(file.fileSize-12)){ + var relativeOffset=file.readVLV(); + + + var XORdifferences=[]; + while(file.readU8()){ + XORdifferences.push(file._lastRead); + } + patch.addRecord(relativeOffset, XORdifferences); + } + + file.littleEndian=true; + patch.checksumInput=file.readU32(); + patch.checksumOutput=file.readU32(); + + if(file.readU32()!==file.hashCRC32(0, file.fileSize - 4)){ + throw new Error('Patch checksum mismatch'); + } + + file.littleEndian=false; + return patch; +} + + + +UPS.buildFromRoms=function(original, modified){ + var patch=new UPS(); + patch.sizeInput=original.fileSize; + patch.sizeOutput=modified.fileSize; + + + var previousSeek=1; + while(!modified.isEOF()){ + var b1=original.isEOF()?0x00:original.readU8(); + var b2=modified.readU8(); + + if(b1!==b2){ + var currentSeek=modified.offset; + var XORdata=[]; + + while(b1!==b2){ + XORdata.push(b1 ^ b2); + + if(modified.isEOF()) + break; + b1=original.isEOF()?0x00:original.readU8(); + b2=modified.readU8(); + } + + patch.addRecord(currentSeek-previousSeek, XORdata); + previousSeek=currentSeek+XORdata.length+1; + } + } + + + patch.checksumInput=original.hashCRC32(); + patch.checksumOutput=modified.hashCRC32(); + return patch +} \ No newline at end of file diff --git a/public/rom-patcher-js/modules/RomPatcher.format.vcdiff.js b/public/rom-patcher-js/modules/RomPatcher.format.vcdiff.js new file mode 100644 index 0000000..ab0f3f9 --- /dev/null +++ b/public/rom-patcher-js/modules/RomPatcher.format.vcdiff.js @@ -0,0 +1,382 @@ +/* VCDIFF module for RomPatcher.js v20181021 - Marc Robledo 2018 - http://www.marcrobledo.com/license */ +/* File format specification: https://tools.ietf.org/html/rfc3284 */ +/* + Mostly based in: + https://github.com/vic-alexiev/TelerikAcademy/tree/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff + some code and ideas borrowed from: + https://hack64.net/jscripts/libpatch.js?6 +*/ +//const VCDIFF_MAGIC=0xd6c3c400; +const VCDIFF_MAGIC='\xd6\xc3\xc4'; +/* +const XDELTA_014_MAGIC='%XDELTA'; +const XDELTA_018_MAGIC='%XDZ000'; +const XDELTA_020_MAGIC='%XDZ001'; +const XDELTA_100_MAGIC='%XDZ002'; +const XDELTA_104_MAGIC='%XDZ003'; +const XDELTA_110_MAGIC='%XDZ004'; +*/ +if(typeof module !== "undefined" && module.exports){ + module.exports = VCDIFF; + BinFile = require("./BinFile"); +} + +function VCDIFF(patchFile){ + this.file=patchFile; +} +VCDIFF.prototype.toString=function(){ + return 'VCDIFF patch' +} + +VCDIFF.prototype.apply=function(romFile, validate){ + //romFile._u8array=new Uint8Array(romFile._dataView.buffer); + + //var t0=performance.now(); + var parser=new VCDIFF_Parser(this.file); + + //read header + parser.seek(4); + var headerIndicator=parser.readU8(); + + if(headerIndicator & VCD_DECOMPRESS){ + //has secondary decompressor, read its id + var secondaryDecompressorId=parser.readU8(); + + if(secondaryDecompressorId!==0) + throw new Error('not implemented: secondary decompressor'); + } + + + if(headerIndicator & VCD_CODETABLE){ + var codeTableDataLength=parser.read7BitEncodedInt(); + + if(codeTableDataLength!==0) + throw new Error('not implemented: custom code table'); // custom code table + } + + if(headerIndicator & VCD_APPHEADER){ + // ignore app header data + var appDataLength=parser.read7BitEncodedInt(); + parser.skip(appDataLength); + } + var headerEndOffset=parser.offset; + + //calculate target file size + var newFileSize=0; + while(!parser.isEOF()){ + var winHeader=parser.decodeWindowHeader(); + newFileSize+=winHeader.targetWindowLength; + parser.skip(winHeader.addRunDataLength + winHeader.addressesLength + winHeader.instructionsLength); + } + tempFile=new BinFile(newFileSize); + + + + + parser.seek(headerEndOffset); + + + + var cache = new VCD_AdressCache(4,3); + var codeTable = VCD_DEFAULT_CODE_TABLE; + + var targetWindowPosition = 0; //renombrar + + while(!parser.isEOF()){ + var winHeader = parser.decodeWindowHeader(); + + var addRunDataStream = new VCDIFF_Parser(this.file, parser.offset); + var instructionsStream = new VCDIFF_Parser(this.file, addRunDataStream.offset + winHeader.addRunDataLength); + var addressesStream = new VCDIFF_Parser(this.file, instructionsStream.offset + winHeader.instructionsLength); + + var addRunDataIndex = 0; + + cache.reset(addressesStream); + + var addressesStreamEndOffset = addressesStream.offset; + while(instructionsStream.offset0){ + this.near[this.nextNearSlot]=address; + this.nextNearSlot=(this.nextNearSlot+1)%this.nearSize; + } + + if(this.sameSize>0){ + this.same[address%(this.sameSize*256)]=address; + } +} \ No newline at end of file diff --git a/public/rom-patcher-js/modules/bz2/LICENSE b/public/rom-patcher-js/modules/bz2/LICENSE new file mode 100644 index 0000000..874dc3f --- /dev/null +++ b/public/rom-patcher-js/modules/bz2/LICENSE @@ -0,0 +1,19 @@ +Copyright 2019 SheetJS LLC + +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/public/rom-patcher-js/modules/bz2/bz2.js b/public/rom-patcher-js/modules/bz2/bz2.js new file mode 100644 index 0000000..93bafd3 --- /dev/null +++ b/public/rom-patcher-js/modules/bz2/bz2.js @@ -0,0 +1 @@ +/* code has been moved to RomPatcher.format.bdf.js because of incompatibility between webapp, web worker and CLI */ \ No newline at end of file diff --git a/public/rom-patcher-js/modules/zip.js/LICENSE b/public/rom-patcher-js/modules/zip.js/LICENSE new file mode 100644 index 0000000..d4b12d8 --- /dev/null +++ b/public/rom-patcher-js/modules/zip.js/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2013, Gildas Lormeau + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/public/rom-patcher-js/modules/zip.js/inflate.js b/public/rom-patcher-js/modules/zip.js/inflate.js new file mode 100644 index 0000000..3355821 --- /dev/null +++ b/public/rom-patcher-js/modules/zip.js/inflate.js @@ -0,0 +1,36 @@ +/* + Copyright (c) 2013 Gildas Lormeau. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, + INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * This program is based on JZlib 1.0.2 ymnk, JCraft,Inc. + * JZlib is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +!function(i){"use strict";var P=0,q=1,B=-2,C=-3,x=-4,F=-5,G=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535],H=1440,a=[96,7,256,0,8,80,0,8,16,84,8,115,82,7,31,0,8,112,0,8,48,0,9,192,80,7,10,0,8,96,0,8,32,0,9,160,0,8,0,0,8,128,0,8,64,0,9,224,80,7,6,0,8,88,0,8,24,0,9,144,83,7,59,0,8,120,0,8,56,0,9,208,81,7,17,0,8,104,0,8,40,0,9,176,0,8,8,0,8,136,0,8,72,0,9,240,80,7,4,0,8,84,0,8,20,85,8,227,83,7,43,0,8,116,0,8,52,0,9,200,81,7,13,0,8,100,0,8,36,0,9,168,0,8,4,0,8,132,0,8,68,0,9,232,80,7,8,0,8,92,0,8,28,0,9,152,84,7,83,0,8,124,0,8,60,0,9,216,82,7,23,0,8,108,0,8,44,0,9,184,0,8,12,0,8,140,0,8,76,0,9,248,80,7,3,0,8,82,0,8,18,85,8,163,83,7,35,0,8,114,0,8,50,0,9,196,81,7,11,0,8,98,0,8,34,0,9,164,0,8,2,0,8,130,0,8,66,0,9,228,80,7,7,0,8,90,0,8,26,0,9,148,84,7,67,0,8,122,0,8,58,0,9,212,82,7,19,0,8,106,0,8,42,0,9,180,0,8,10,0,8,138,0,8,74,0,9,244,80,7,5,0,8,86,0,8,22,192,8,0,83,7,51,0,8,118,0,8,54,0,9,204,81,7,15,0,8,102,0,8,38,0,9,172,0,8,6,0,8,134,0,8,70,0,9,236,80,7,9,0,8,94,0,8,30,0,9,156,84,7,99,0,8,126,0,8,62,0,9,220,82,7,27,0,8,110,0,8,46,0,9,188,0,8,14,0,8,142,0,8,78,0,9,252,96,7,256,0,8,81,0,8,17,85,8,131,82,7,31,0,8,113,0,8,49,0,9,194,80,7,10,0,8,97,0,8,33,0,9,162,0,8,1,0,8,129,0,8,65,0,9,226,80,7,6,0,8,89,0,8,25,0,9,146,83,7,59,0,8,121,0,8,57,0,9,210,81,7,17,0,8,105,0,8,41,0,9,178,0,8,9,0,8,137,0,8,73,0,9,242,80,7,4,0,8,85,0,8,21,80,8,258,83,7,43,0,8,117,0,8,53,0,9,202,81,7,13,0,8,101,0,8,37,0,9,170,0,8,5,0,8,133,0,8,69,0,9,234,80,7,8,0,8,93,0,8,29,0,9,154,84,7,83,0,8,125,0,8,61,0,9,218,82,7,23,0,8,109,0,8,45,0,9,186,0,8,13,0,8,141,0,8,77,0,9,250,80,7,3,0,8,83,0,8,19,85,8,195,83,7,35,0,8,115,0,8,51,0,9,198,81,7,11,0,8,99,0,8,35,0,9,166,0,8,3,0,8,131,0,8,67,0,9,230,80,7,7,0,8,91,0,8,27,0,9,150,84,7,67,0,8,123,0,8,59,0,9,214,82,7,19,0,8,107,0,8,43,0,9,182,0,8,11,0,8,139,0,8,75,0,9,246,80,7,5,0,8,87,0,8,23,192,8,0,83,7,51,0,8,119,0,8,55,0,9,206,81,7,15,0,8,103,0,8,39,0,9,174,0,8,7,0,8,135,0,8,71,0,9,238,80,7,9,0,8,95,0,8,31,0,9,158,84,7,99,0,8,127,0,8,63,0,9,222,82,7,27,0,8,111,0,8,47,0,9,190,0,8,15,0,8,143,0,8,79,0,9,254,96,7,256,0,8,80,0,8,16,84,8,115,82,7,31,0,8,112,0,8,48,0,9,193,80,7,10,0,8,96,0,8,32,0,9,161,0,8,0,0,8,128,0,8,64,0,9,225,80,7,6,0,8,88,0,8,24,0,9,145,83,7,59,0,8,120,0,8,56,0,9,209,81,7,17,0,8,104,0,8,40,0,9,177,0,8,8,0,8,136,0,8,72,0,9,241,80,7,4,0,8,84,0,8,20,85,8,227,83,7,43,0,8,116,0,8,52,0,9,201,81,7,13,0,8,100,0,8,36,0,9,169,0,8,4,0,8,132,0,8,68,0,9,233,80,7,8,0,8,92,0,8,28,0,9,153,84,7,83,0,8,124,0,8,60,0,9,217,82,7,23,0,8,108,0,8,44,0,9,185,0,8,12,0,8,140,0,8,76,0,9,249,80,7,3,0,8,82,0,8,18,85,8,163,83,7,35,0,8,114,0,8,50,0,9,197,81,7,11,0,8,98,0,8,34,0,9,165,0,8,2,0,8,130,0,8,66,0,9,229,80,7,7,0,8,90,0,8,26,0,9,149,84,7,67,0,8,122,0,8,58,0,9,213,82,7,19,0,8,106,0,8,42,0,9,181,0,8,10,0,8,138,0,8,74,0,9,245,80,7,5,0,8,86,0,8,22,192,8,0,83,7,51,0,8,118,0,8,54,0,9,205,81,7,15,0,8,102,0,8,38,0,9,173,0,8,6,0,8,134,0,8,70,0,9,237,80,7,9,0,8,94,0,8,30,0,9,157,84,7,99,0,8,126,0,8,62,0,9,221,82,7,27,0,8,110,0,8,46,0,9,189,0,8,14,0,8,142,0,8,78,0,9,253,96,7,256,0,8,81,0,8,17,85,8,131,82,7,31,0,8,113,0,8,49,0,9,195,80,7,10,0,8,97,0,8,33,0,9,163,0,8,1,0,8,129,0,8,65,0,9,227,80,7,6,0,8,89,0,8,25,0,9,147,83,7,59,0,8,121,0,8,57,0,9,211,81,7,17,0,8,105,0,8,41,0,9,179,0,8,9,0,8,137,0,8,73,0,9,243,80,7,4,0,8,85,0,8,21,80,8,258,83,7,43,0,8,117,0,8,53,0,9,203,81,7,13,0,8,101,0,8,37,0,9,171,0,8,5,0,8,133,0,8,69,0,9,235,80,7,8,0,8,93,0,8,29,0,9,155,84,7,83,0,8,125,0,8,61,0,9,219,82,7,23,0,8,109,0,8,45,0,9,187,0,8,13,0,8,141,0,8,77,0,9,251,80,7,3,0,8,83,0,8,19,85,8,195,83,7,35,0,8,115,0,8,51,0,9,199,81,7,11,0,8,99,0,8,35,0,9,167,0,8,3,0,8,131,0,8,67,0,9,231,80,7,7,0,8,91,0,8,27,0,9,151,84,7,67,0,8,123,0,8,59,0,9,215,82,7,19,0,8,107,0,8,43,0,9,183,0,8,11,0,8,139,0,8,75,0,9,247,80,7,5,0,8,87,0,8,23,192,8,0,83,7,51,0,8,119,0,8,55,0,9,207,81,7,15,0,8,103,0,8,39,0,9,175,0,8,7,0,8,135,0,8,71,0,9,239,80,7,9,0,8,95,0,8,31,0,9,159,84,7,99,0,8,127,0,8,63,0,9,223,82,7,27,0,8,111,0,8,47,0,9,191,0,8,15,0,8,143,0,8,79,0,9,255],r=[80,5,1,87,5,257,83,5,17,91,5,4097,81,5,5,89,5,1025,85,5,65,93,5,16385,80,5,3,88,5,513,84,5,33,92,5,8193,82,5,9,90,5,2049,86,5,129,192,5,24577,80,5,2,87,5,385,83,5,25,91,5,6145,81,5,7,89,5,1537,85,5,97,93,5,24577,80,5,4,88,5,769,84,5,49,92,5,12289,82,5,13,90,5,3073,86,5,193,192,5,24577],w=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],c=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,112,112],v=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],h=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],D=15;function J(){var f,o,E,S,U,z;function b(i,t,e,n,a,r,_,l,d,s,f){var o,b,u,x,w,c,v,h,k,m,y,g,p,I,A;for(m=0,w=e;E[i[t+m]]++,m++,0!==--w;);if(E[0]==e)return _[0]=-1,l[0]=0,P;for(h=l[0],c=1;c<=D&&0===E[c];c++);for(h<(v=c)&&(h=c),w=D;0!==w&&0===E[w];w--);for((u=w)o+1&&(b-=o+1,p=v,cH)return C;U[x]=y=s[0],s[0]+=A,0!==x?(z[x]=w,S[0]=c,c=w>>>g-(S[1]=h),S[2]=y-U[x-1]-c,d.set(S,3*(U[x-1]+c))):_[0]=y}for(S[1]=v-g,e<=m?S[0]=192:f[m]>>g;c>>=1)w^=c;for(w^=c,k=(1<>=s[p+1],u-=s[p+1],0!=(16&o)){for(o&=15,m=s[p+2]+(b&G[o]),b>>=o,u-=o;u<15;)w--,b|=(255&l.read_byte(x++))<>=s[p+1],u-=s[p+1],0!=(16&o)){for(o&=15;u>=o,u-=o,v-=m,y<=c)0>3<(m=l.avail_in-w)?u>>3:m,x-=m,u-=m<<3,_.bitb=b,_.bitk=u,l.avail_in=w,l.total_in+=x-l.next_in_index,l.next_in_index=x,_.write=c,C;d+=s[p+2],o=s[p=3*(f+(d+=b&G[o]))]}break}if(0!=(64&o))return 0!=(32&o)?(w+=m=u>>3<(m=l.avail_in-w)?u>>3:m,x-=m,u-=m<<3,_.bitb=b,_.bitk=u,l.avail_in=w,l.total_in+=x-l.next_in_index,l.next_in_index=x,_.write=c,q):(l.msg="invalid literal/length code",w+=m=u>>3<(m=l.avail_in-w)?u>>3:m,x-=m,u-=m<<3,_.bitb=b,_.bitk=u,l.avail_in=w,l.total_in+=x-l.next_in_index,l.next_in_index=x,_.write=c,C);if(d+=s[p+2],0===(o=s[p=3*(f+(d+=b&G[o]))])){b>>=s[p+1],u-=s[p+1],_.window[c++]=s[p+2],v--;break}}else b>>=s[p+1],u-=s[p+1],_.window[c++]=s[p+2],v--}while(258<=v&&10<=w);return w+=m=u>>3<(m=l.avail_in-w)?u>>3:m,x-=m,u-=m<<3,_.bitb=b,_.bitk=u,l.avail_in=w,l.total_in+=x-l.next_in_index,l.next_in_index=x,_.write=c,P}this.init=function(i,t,e,n,a,r){u=U,p=i,I=t,w=e,A=n,c=a,E=r,x=null},this.proc=function(i,t,e){var n,a,r,_,l,d,s,f=0,o=0,b=0;for(b=t.next_in_index,_=t.avail_in,f=i.bitb,o=i.bitk,d=(l=i.write)>>=x[a+1],o-=x[a+1],0===(r=x[a])){m=x[a+2],u=N;break}if(0!=(16&r)){y=15&r,v=x[a+2],u=j;break}if(0==(64&r)){k=r,h=a/3+x[a+2];break}if(0==(32&r))return u=R,t.msg="invalid literal/length code",e=C,i.bitb=f,i.bitk=o,t.avail_in=_,t.total_in+=b-t.next_in_index,t.next_in_index=b,i.write=l,i.inflate_flush(t,e);u=O;break;case j:for(n=y;o>=n,o-=n,k=I,x=c,h=E,u=K;case K:for(n=k;o>=x[a+1],o-=x[a+1],0!=(16&(r=x[a]))){y=15&r,g=x[a+2],u=L;break}if(0!=(64&r))return u=R,t.msg="invalid distance code",e=C,i.bitb=f,i.bitk=o,t.avail_in=_,t.total_in+=b-t.next_in_index,t.next_in_index=b,i.write=l,i.inflate_flush(t,e);k=r,h=a/3+x[a+2];break;case L:for(n=y;o>=n,o-=n,u=M;case M:for(s=l-g;s<0;)s+=i.end;for(;0!==v;){if(0===d&&(l==i.end&&0!==i.read&&(d=(l=0)i.avail_out&&(e=i.avail_out),0!==e&&t==F&&(t=P),i.avail_out-=e,i.total_out+=e,i.next_out.set(y.window.subarray(a,a+e),n),n+=e,(a+=e)==y.end&&(a=0,y.write==y.end&&(y.write=0),(e=y.write-a)>i.avail_out&&(e=i.avail_out),0!==e&&t==F&&(t=P),i.avail_out-=e,i.total_out+=e,i.next_out.set(y.window.subarray(a,a+e),n),n+=e,a+=e),i.next_out_index=n,y.read=a,t},y.proc=function(i,t){var e,n,a,r,_,l,d,s;for(r=i.next_in_index,_=i.avail_in,n=y.bitb,a=y.bitk,d=(l=y.write)>>1){case 0:n>>>=3,n>>>=e=7&(a-=3),a-=e,g=W;break;case 1:var f=[],o=[],b=[[]],u=[[]];J.inflate_trees_fixed(f,o,b,u),U.init(f[0],o[0],b[0],0,u[0],0),n>>>=3,a-=3,g=ii;break;case 2:n>>>=3,a-=3,g=Y;break;case 3:return n>>>=3,a-=3,g=ni,i.msg="invalid block type",t=C,y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t)}break;case W:for(;a<32;){if(0===_)return y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);t=P,_--,n|=(255&i.read_byte(r++))<>>16&65535)!=(65535&n))return g=ni,i.msg="invalid stored block lengths",t=C,y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);p=65535&n,n=a=0,g=0!==p?X:0!==z?ti:V;break;case X:if(0===_)return y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);if(0===d&&(l==y.end&&0!==y.read&&(d=(l=0)>5&31))return g=ni,i.msg="too many length or distance symbols",t=C,y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);if(e=258+(31&e)+(e>>5&31),!m||m.length>>=14,a-=14,A=0,g=Z;case Z:for(;A<4+(I>>>10);){for(;a<3;){if(0===_)return y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);t=P,_--,n|=(255&i.read_byte(r++))<>>=3,a-=3}for(;A<19;)m[T[A++]]=0;if(E[0]=7,(e=j.inflate_trees_bits(m,E,S,D,i))!=P)return(t=e)==C&&(m=null,g=ni),y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);A=0,g=$;case $:for(;!(258+(31&(e=I))+(e>>5&31)<=A);){var x,w;for(e=E[0];a>>=e,a-=e,m[A++]=w;else{for(s=18==w?7:w-14,x=18==w?11:3;a>>=e)&G[s],n>>>=s,a-=s,258+(31&(e=I))+(e>>5&31)<(s=A)+x||16==w&&s<1)return m=null,g=ni,i.msg="invalid bit length repeat",t=C,y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);for(w=16==w?m[s-1]:0;m[s++]=w,0!=--x;);A=s}}S[0]=-1;var c=[],v=[],h=[],k=[];if(c[0]=9,v[0]=6,e=I,(e=j.inflate_trees_dynamic(257+(31&e),1+(e>>5&31),m,c,v,h,k,D,i))!=P)return e==C&&(m=null,g=ni),t=e,y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,y.inflate_flush(i,t);U.init(c[0],v[0],D,h[0],D,k[0]),g=ii;case ii:if(y.bitb=n,y.bitk=a,i.avail_in=_,i.total_in+=r-i.next_in_index,i.next_in_index=r,y.write=l,(t=U.proc(y,i,t))!=q)return y.inflate_flush(i,t);if(t=P,U.free(i),r=i.next_in_index,_=i.avail_in,n=y.bitb,a=y.bitk,d=(l=y.write)>4)>i.istate.wbits){i.istate.mode=13,i.msg="invalid window size",i.istate.marker=5;break}i.istate.mode=1;case 1:if(0===i.avail_in)return e;if(e=t,i.avail_in--,i.total_in++,n=255&i.read_byte(i.next_in_index++),((i.istate.method<<8)+n)%31!=0){i.istate.mode=13,i.msg="incorrect header check",i.istate.marker=5;break}if(0==(32&n)){i.istate.mode=7;break}i.istate.mode=2;case 2:if(0===i.avail_in)return e;e=t,i.avail_in--,i.total_in++,i.istate.need=(255&i.read_byte(i.next_in_index++))<<24&4278190080,i.istate.mode=3;case 3:if(0===i.avail_in)return e;e=t,i.avail_in--,i.total_in++,i.istate.need+=(255&i.read_byte(i.next_in_index++))<<16&16711680,i.istate.mode=4;case 4:if(0===i.avail_in)return e;e=t,i.avail_in--,i.total_in++,i.istate.need+=(255&i.read_byte(i.next_in_index++))<<8&65280,i.istate.mode=5;case 5:return 0===i.avail_in?e:(e=t,i.avail_in--,i.total_in++,i.istate.need+=255&i.read_byte(i.next_in_index++),i.istate.mode=6,2);case 6:return i.istate.mode=13,i.msg="need dictionary",i.istate.marker=0,B;case 7:if((e=i.istate.blocks.proc(i,e))==C){i.istate.mode=13,i.istate.marker=0;break}if(e==P&&(e=t),e!=q)return e;e=t,i.istate.blocks.reset(i,i.istate.was),i.istate.mode=12;case 12:return q;case 13:return C;default:return B}},e.inflateSetDictionary=function(i,t,e){var n=0,a=e;return i&&i.istate&&6==i.istate.mode?(a>=1<>>8^r[255&(e^t[c])];this.crc=e},n.prototype.get=function(){return~this.crc},n.prototype.table=function(){var t,e,r,c=[];for(t=0;t<256;t++){for(r=t,e=0;e<8;e++)1&r?r=r>>>1^3988292384:r>>>=1;c[t]=r}return c}(),(c.NOOP=e).prototype.append=function(t,e){return t},e.prototype.flush=function(){}}(this); \ No newline at end of file diff --git a/public/rom-patcher-js/modules/zip.js/zip.min.js b/public/rom-patcher-js/modules/zip.js/zip.min.js new file mode 100644 index 0000000..620e021 --- /dev/null +++ b/public/rom-patcher-js/modules/zip.js/zip.min.js @@ -0,0 +1,28 @@ +/* + Copyright (c) 2013 Gildas Lormeau. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, + INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +!function(b){"use strict";var o,k="File format is not recognized.",a="File contains encrypted entry.",s="File is using Zip64 (4gb+ file size).",w="Error while reading zip file.",n="Error while reading file data.",y=524288,c="text/plain";try{o=0===new Blob([new DataView(new ArrayBuffer(0))]).size}catch(e){}function r(){this.crc=-1}function l(){}function A(e,t){var r,n;return r=new ArrayBuffer(e),n=new Uint8Array(r),t&&n.set(t,0),{buffer:r,array:n,view:new DataView(r)}}function e(){}function t(n){var i,o=this;o.size=0,o.init=function(e,t){var r=new Blob([n],{type:c});(i=new f(r)).init(function(){o.size=i.size,e()},t)},o.readUint8Array=function(e,t,r,n){i.readUint8Array(e,t,r,n)}}function i(f){var u,r=this;r.size=0,r.init=function(e){for(var t=f.length;"="==f.charAt(t-1);)t--;u=f.indexOf(",")+1,r.size=Math.floor(.75*(t-u)),e()},r.readUint8Array=function(e,t,r){var n,i=A(t),o=4*Math.floor(e/3),a=4*Math.ceil((e+t)/3),s=b.atob(f.substring(o+u,a+u)),c=e-3*Math.floor(o/4);for(n=c;ne.size)throw new RangeError("offset:"+t+", length:"+r+", size:"+e.size);return e.slice?e.slice(t,t+r):e.webkitSlice?e.webkitSlice(t,t+r):e.mozSlice?e.mozSlice(t,t+r):e.msSlice?e.msSlice(t,t+r):void 0}(o,e,t))}catch(e){n(e)}}}function u(){}function h(n){var i;this.init=function(e){i=new Blob([],{type:c}),e()},this.writeUint8Array=function(e,t){i=new Blob([i,o?e:e.buffer],{type:c}),t()},this.getData=function(t,e){var r=new FileReader;r.onload=function(e){t(e.target.result)},r.onerror=e,r.readAsText(i,n)}}function p(t){var o="",a="";this.init=function(e){o+="data:"+(t||"")+";base64,",e()},this.writeUint8Array=function(e,t){var r,n=a.length,i=a;for(a="",r=0;r<3*Math.floor((n+e.length)/3)-n;r++)i+=String.fromCharCode(e[r]);for(;r>16,r=65535&e;try{return new Date(1980+((65024&t)>>9),((480&t)>>5)-1,31&t,(63488&r)>>11,(2016&r)>>5,2*(31&r),0)}catch(e){}}(e.lastModDateRaw),1!=(1&e.bitFlag)?((n||8!=(8&e.bitFlag))&&(e.crc32=t.view.getUint32(r+10,!0),e.compressedSize=t.view.getUint32(r+14,!0),e.uncompressedSize=t.view.getUint32(r+18,!0)),4294967295!==e.compressedSize&&4294967295!==e.uncompressedSize?(e.filenameLength=t.view.getUint16(r+22,!0),e.extraFieldLength=t.view.getUint16(r+24,!0)):i(s)):i(a)}function m(m,t,U){var z=0;function l(){}l.prototype.getData=function(w,i,h,p){var v=this;function d(e,t){var r,n;p&&(r=t,(n=A(4)).view.setUint32(0,r),v.crc32!=n.view.getUint32(0))?U("CRC failed."):w.getData(function(e){i(e)})}function g(e){U(e||n)}function y(e){U(e||"Error while writing file data.")}m.readUint8Array(v.offset,30,function(e){var l,t=A(e.length,e);1347093252==t.view.getUint32(0)?(M(v,t,4,!1,U),l=v.offset+30+v.filenameLength+v.extraFieldLength,w.init(function(){var e,t,r,n,i,o,a,s,c,f,u;0===v.compressionMethod?D(v._worker,z++,m,w,l,v.compressedSize,p,d,h,g,y):(e=v._worker,t=z++,r=m,n=w,i=l,o=v.compressedSize,a=d,s=h,c=g,f=y,u=p?"output":"none",b.zip.useWebWorkers?S(e,{sn:t,codecClass:"Inflater",crcType:u},r,n,i,o,s,a,c,f):_(new b.zip.Inflater,r,n,i,o,u,s,a,c,f))},y)):U(k)},g)};var r={getEntries:function(f){var u=this._worker;!function(n){var i=22;if(m.size=m.size?U(k):m.readUint8Array(t,m.size-t,function(e){var t,r,n,i,o=0,a=[],s=A(e.length,e);for(t=0;t>>8^r[255&(t^e[n])];this.crc=t},r.prototype.get=function(){return~this.crc},r.prototype.table=function(){var e,t,r,n=[];for(e=0;e<256;e++){for(r=e,t=0;t<8;t++)1&r?r=r>>>1^3988292384:r>>>=1;n[e]=r}return n}(),l.prototype.append=function(e,t){return e},l.prototype.flush=function(){},(t.prototype=new e).constructor=t,(i.prototype=new e).constructor=i,(f.prototype=new e).constructor=f,u.prototype.getData=function(e){e(this.data)},(h.prototype=new u).constructor=h,(p.prototype=new u).constructor=p,(v.prototype=new u).constructor=v;var C={deflater:["z-worker.js","deflate.js"],inflater:["z-worker.js","inflate.js"]};function E(e,n,i){if(null===b.zip.workerScripts||null===b.zip.workerScriptsPath){var t,r,o;if(b.zip.workerScripts){if(t=b.zip.workerScripts[e],!Array.isArray(t))return void i(new Error("zip.workerScripts."+e+" is not an array!"));r=t,o=document.createElement("a"),t=r.map(function(e){return o.href=e,o.href})}else(t=C[e].slice(0))[0]=(b.zip.workerScriptsPath||"")+t[0];var a=new Worker(t[0]);a.codecTime=a.crcTime=0,a.postMessage({type:"importScripts",scripts:t.slice(1)}),a.addEventListener("message",function e(t){var r=t.data;if(r.error)return a.terminate(),void i(r.error);"importScripts"===r.type&&(a.removeEventListener("message",e),a.removeEventListener("error",s),n(a))}),a.addEventListener("error",s)}else i(new Error("Either zip.workerScripts or zip.workerScriptsPath may be set, not both."));function s(e){a.terminate(),i(e)}}function F(e){console.error(e)}b.zip={Reader:e,Writer:u,BlobReader:f,Data64URIReader:i,TextReader:t,BlobWriter:v,Data64URIWriter:p,TextWriter:h,createReader:function(e,t,r){r=r||F,e.init(function(){m(e,t,r)},r)},createWriter:function(e,t,r,n){r=r||F,n=!!n,e.init(function(){U(e,t,r,n)},r)},useWebWorkers:!0,workerScriptsPath:null,workerScripts:null}}(this); diff --git a/resources/css/app.css b/resources/css/app.css index 3aa3e17..60437ec 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -17,6 +17,9 @@ @import './components/notifications.css'; @import './components/settings.css'; @import './components/queue.css'; +@import './components/drafts.css'; +@import './components/modcp.css'; +@import './components/tools.css'; @import './components/easymde.css'; diff --git a/resources/css/components/common.css b/resources/css/components/common.css index fc98a24..c74aa62 100644 --- a/resources/css/components/common.css +++ b/resources/css/components/common.css @@ -182,3 +182,9 @@ .spin { animation: spin 1s infinite linear; } + +.search-button { + background: none; + border: none; + cursor: pointer; +} diff --git a/resources/css/components/drafts.css b/resources/css/components/drafts.css new file mode 100644 index 0000000..d2ce20d --- /dev/null +++ b/resources/css/components/drafts.css @@ -0,0 +1,160 @@ +.drafts-count { + font-size: 0.85rem; + color: var(--text2); + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid var(--border); +} + +.drafts-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 15px; + padding: 80px 20px; + background-color: var(--bg2); + border: 1px dashed var(--border); + text-align: center; + color: var(--text2); + + h3 { + font-size: 1.1rem; + color: var(--text); + margin: 0; + } + + p { + font-size: 0.9rem; + margin: 0; + } + +} + +.drafts-list { + display: flex; + flex-direction: column; + gap: 15px; +} + +.drafts-item { + display: flex; + gap: 20px; + background-color: var(--bg2); + border: 1px solid var(--border); + border-left: 3px solid var(--rhpz-orange); + padding: 20px; + transition: border-color 0.15s; + + &:hover { + border-color: var(--rhpz-orange); + } +} + +.drafts-cover { + width: 80px; + height: 80px; + flex-shrink: 0; + background-color: var(--bg); + border: 1px solid var(--border); + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + + img { + width: 100%; + height: 100%; + object-fit: contain; + } +} + +.drafts-cover-placeholder { + color: var(--border); +} + +.drafts-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 12px; +} + +.drafts-top { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 15px; +} + +.drafts-title { + font-size: 1rem; + font-weight: 600; + color: var(--text); + margin-bottom: 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.drafts-meta { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.drafts-dates { + display: flex; + flex-direction: column; + gap: 4px; + text-align: right; + font-size: 0.78rem; + color: var(--text2); + flex-shrink: 0; + + span { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 5px; + } +} + +.drafts-progress { + display: flex; + align-items: center; + gap: 10px; +} + +.drafts-progress-bar { + flex: 1; + height: 4px; + background-color: var(--bg4); + overflow: hidden; +} + +.drafts-progress-fill { + height: 100%; + background-color: var(--rhpz-orange); + transition: width 0.3s ease; + + .complete { + background-color: var(--success); + } +} +.drafts-progress-label { + font-size: 0.75rem; + color: var(--text2); + white-space: nowrap; +} +.drafts-actions { + display: flex; + flex-direction: column; + gap: 8px; + justify-content: center; + flex-shrink: 0; + .btn { + white-space: nowrap; + } +} diff --git a/resources/css/components/modcp.css b/resources/css/components/modcp.css new file mode 100644 index 0000000..e5cddb9 --- /dev/null +++ b/resources/css/components/modcp.css @@ -0,0 +1,320 @@ +.modcp-wrapper { + display: flex; + gap: 0; + align-items: flex-start; + min-height: calc(100vh - 60px); +} + +.modcp-sidebar { + width: 220px; + flex-shrink: 0; + background-color: var(--bg2); + border: 1px solid var(--border); + position: sticky; + top: 0; + align-self: flex-start; + margin-right: 15px; +} + +.modcp-sidebar-header { + display: flex; + align-items: center; + gap: 8px; + padding: 14px 16px; + font-weight: 600; + font-size: 0.88rem; + color: var(--text); + border-bottom: 1px solid var(--border); + background-color: var(--bg3); + text-transform: uppercase; + letter-spacing: 0.5px; +} + + +.modcp-nav { padding: 8px 0; } + +.modcp-nav-group { margin-bottom: 4px; } + +.modcp-nav-label { + display: block; + padding: 8px 16px 4px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--text2); +} + +.modcp-nav-item { + display: flex; + align-items: center; + gap: 9px; + padding: 8px 16px; + font-size: 0.88rem; + color: var(--text); + text-decoration: none; + border-left: 3px solid transparent; + transition: background-color 0.1s, border-color 0.1s; + + &:hover { + background-color: var(--bg3); + text-decoration: none; + } + .active { + background-color: var(--bg3); + border-left-color: var(--rhpz-orange); + color: var(--text); + font-weight: 600; + } +} + +.modcp-nav-badge { + margin-left: auto; + background-color: var(--rhpz-orange); + color: #111; + font-size: 0.65rem; + font-weight: 700; + min-width: 18px; + height: 18px; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 5px; +} + +.modcp-content { + flex: 1; + min-width: 0; + background-color: var(--bg2); + border: 1px solid var(--border); + padding: 25px; +} + +.modcp-page-title { + display: flex; + align-items: center; + gap: 10px; + font-size: 1.3rem; + font-weight: 600; + color: var(--text); + margin-bottom: 25px; + padding-bottom: 15px; + border-bottom: 1px solid var(--border); +} + +.modcp-count { + margin-left: auto; + font-size: 0.85rem; + font-weight: normal; + color: var(--text2); +} + +.modcp-section-title { + font-size: 0.78rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.7px; + color: var(--text2); + margin-bottom: 12px; +} + +.modcp-stats { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 12px; + margin-bottom: 25px; +} + +.modcp-stat-card { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + background-color: var(--bg3); + border: 1px solid var(--border); + border-left: 3px solid var(--border); + text-decoration: none; + transition: border-color 0.15s, background-color 0.15s; + color: var(--text); + + &:hover { + background-color: var(--bg4); + text-decoration: none; + } +} + +.modcp-stat-card--orange { border-left-color: var(--rhpz-orange); } +.modcp-stat-card--danger { border-left-color: var(--error); } +.modcp-stat-card--muted { cursor: default; } + +.modcp-stat-icon { color: var(--text2); } +.modcp-stat-card--orange .modcp-stat-icon { color: var(--rhpz-orange); } +.modcp-stat-card--danger .modcp-stat-icon { color: var(--error); } + +.modcp-stat-info { display: flex; flex-direction: column; } +.modcp-stat-value { font-size: 1.4rem; font-weight: 700; color: var(--text); line-height: 1; } +.modcp-stat-label { font-size: 0.75rem; color: var(--text2); margin-top: 3px; } + +.modcp-quick-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 25px; +} + +.modcp-quick-btn { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 8px 14px; + background-color: var(--bg3); + border: 1px solid var(--border); + color: var(--text); + font-size: 0.85rem; + text-decoration: none; + transition: background-color 0.1s, border-color 0.1s; + + &:hover { + background-color: var(--bg4); + border-color: var(--rhpz-orange); + text-decoration: none; + } +} + +.modcp-list { display: flex; flex-direction: column; } + +.modcp-list-item { + display: flex; + align-items: center; + gap: 15px; + padding: 12px 15px; + border-bottom: 1px solid var(--border); + transition: background-color 0.1s; +} + +.modcp-list-item:last-child { border-bottom: none; } +.modcp-list-item:hover { background-color: var(--bg3); } +.modcp-list-item--deleted { opacity: 0.8; } + +.modcp-list-item-cover { + width: 44px; + height: 44px; + flex-shrink: 0; + background-color: var(--bg); + border: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + color: var(--border); +} + +.modcp-list-item-cover img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.modcp-list-item-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.modcp-list-item-title { + font-size: 0.92rem; + font-weight: 600; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.modcp-list-item-meta { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.78rem; + color: var(--text2); +} + +.modcp-list-item-actions { + display: flex; + gap: 6px; + flex-shrink: 0; +} + +.modcp-list-item-edit { + display: flex; + gap: 6px; + flex: 1; + align-items: center; +} + +.modcp-list-item-edit .form-input { + flex: 1; + padding: 5px 10px; + font-size: 0.88rem; +} + +.modcp-list-see-all { + display: block; + text-align: center; + padding: 10px; + font-size: 0.85rem; + color: var(--rhpz-orange); + border-top: 1px solid var(--border); + text-decoration: none; +} + +.modcp-add-form { + background-color: var(--bg3); + border: 1px solid var(--border); + padding: 15px; + margin-bottom: 20px; +} + +.modcp-add-form-inner { + display: flex; + gap: 8px; + align-items: center; +} + +.modcp-add-form-inner .form-input { flex: 1; } + +.modcp-empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 50px 20px; + color: var(--text2); + text-align: center; +} + +.mod-alert { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 15px; + margin-bottom: 20px; + font-size: 0.88rem; + border: 1px solid; +} + +.mod-alert--success { + background-color: rgba(129, 199, 132, 0.08); + border-color: rgba(129, 199, 132, 0.3); + color: var(--success); +} + +.modcp-list-item-edit--game { + flex-wrap: wrap; + gap: 6px; +} + +.modcp-list-item-edit--game .form-input { min-width: 180px; flex: 2; } +.modcp-list-item-edit--game .form-select { flex: 1; min-width: 120px; } diff --git a/resources/css/components/tools.css b/resources/css/components/tools.css new file mode 100644 index 0000000..2ad0697 --- /dev/null +++ b/resources/css/components/tools.css @@ -0,0 +1,83 @@ +.patcher-container { + background-color: var(--bg2); + border: 1px solid var(--border); + padding: 25px; + margin-bottom: 20px; +} + +.patcher-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +@media (max-width: 768px) { + .patcher-grid { + grid-template-columns: 1fr; + } +} + +.patcher-dropzone { + border: 2px dashed var(--border); + background-color: var(--bg3); + padding: 55px 20px; + text-align: center; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +.patcher-dropzone:hover, .patcher-dropzone.dragover { + border-color: var(--rhpz-orange); + background-color: var(--bg4); +} + +.patcher-dropzone.has-file { + border-color: var(--success); + background-color: rgba(129, 199, 132, 0.02); +} + +.patcher-status-box { + margin-top: 20px; + padding: 15px; + border: 1px solid var(--border); + background-color: var(--bg3); + font-size: 0.95rem; + line-height: 1.4; +} + +.btn:disabled { + opacity: 0.4; + cursor: not-allowed; + background-color: var(--bg3); + border-color: var(--border); + color: var(--text2); +} + +.embed-patch-box { + border: 1px solid var(--border); + background-color: var(--bg3); + padding: 25px; + height: 85%; + display: flex; + flex-direction: column; + justify-content: center; + gap: 15px; +} +.embed-patch-box-icon { + display: flex; + align-items: center; + gap: 15px; +} +.embed-patch-box-icon-block { + width: 48px; + height: 48px; + background-color: var(--bg2); + border: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: center; +} diff --git a/resources/css/layout/content.css b/resources/css/layout/content.css index 003f080..d13c635 100644 --- a/resources/css/layout/content.css +++ b/resources/css/layout/content.css @@ -23,24 +23,6 @@ cursor: pointer; } - .search-bar { - display: flex; - align-items: center; - background-color: var(--bg); - border: 1px solid var(--border); - border-radius: 2px; - padding: 5px 10px; - width: 300px; - input { - background: none; - border: none; - color: var(--text); - outline: none; - margin-left: 8px; - width: 100%; - } - } - .topbar-actions { display: flex; gap: 8px; @@ -53,6 +35,24 @@ } +.search-bar { + display: flex; + align-items: center; + background-color: var(--bg); + border: 1px solid var(--border); + border-radius: 2px; + padding: 5px 10px; + width: 300px; + input { + background: none; + border: none; + color: var(--text); + outline: none; + margin-left: 8px; + width: 100%; + } +} + #content { flex-grow: 1; padding: 30px; diff --git a/resources/css/layout/entry.css b/resources/css/layout/entry.css index 8a41758..958262f 100644 --- a/resources/css/layout/entry.css +++ b/resources/css/layout/entry.css @@ -14,8 +14,8 @@ gap: 30px; .entry-cover { - width: 200px; - height: 280px; + width: 220px; + height: 220px; background-color: var(--bg); border: 1px solid var(--border); display: flex; @@ -29,7 +29,8 @@ img { width: 100%; height: 100%; - object-fit: cover; + object-fit: contain; + padding: 8px; } } diff --git a/resources/js/RomPatcher.js b/resources/js/RomPatcher.js new file mode 100644 index 0000000..185d0dc --- /dev/null +++ b/resources/js/RomPatcher.js @@ -0,0 +1,114 @@ +export function RomPatcher( initialPatches = {} ) { + + let patchesArray = []; + if (initialPatches) { + patchesArray = Array.isArray(initialPatches) ? initialPatches : [initialPatches]; + } + patchesArray = patchesArray.filter(p => p && p.file); + + return { + + /** + * @type {string} + */ + romFileName: '', + + /** + * @type {string} + */ + patchFileName: '', + + /** + * @type {boolean} + */ + isRomDragOver: false, + + /** + * @type {boolean} + */ + isPatchDragOver: false, + + /** + * @type {boolean} + */ + showStatusBox: false, + + /** + * @type {object} + */ + patchesData: patchesArray, + hasEmbedded: patchesArray.length > 0, + + init() { + + const CONFIG = {language: 'en', requireValidation: false}; + + if (!RomPatcherWeb.isInitialized()){ + if (this.hasEmbedded) { + RomPatcherWeb.initialize(CONFIG, ...this.patchesData); + } else { + RomPatcherWeb.initialize(CONFIG); + } + } + + const STAT_BOX = document.getElementById('patcher-status'); + const OBSERVER = new MutationObserver(() => { + const CRC = document.getElementById('rom-patcher-span-crc32').textContent; + const DESC = document.getElementById('rom-patcher-patch-description').textContent; + const PATCH = document.getElementById('rom-patcher-patch-requirements-value').textContent; + + this.showStatusBox = CRC.trim().length > 0 || DESC.trim().length > 0 || PATCH.trim().length > 0; + }); + + OBSERVER.observe(STAT_BOX, { childList: true, subtree: true, characterData: true }); + }, + + /** + * + * @param {string} id + */ + triggerFileInput(id){ + const I = document.getElementById(id); + if( !I.disabled ) + I.click(); + }, + + /** + * + * @param {Event} e + * @param {string} type + */ + handleInputChange(e, type){ + const file = e.target.files[0]; + if(file){ + if( type === 'rom' ) this.romFileName = file.name; + if( type === 'patch' ) this.patchFileName = file.name; + } + }, + + /** + * + * @param {Event} e + * @param {string} type + */ + handleDrop(e, type ){ + + const file = e.dataTransfer.files[0]; + if( !file ) + return; + + const ID = type === 'rom' ? 'rom-patcher-input-file-rom' : 'rom-patcher-input-file-patch'; + const I = document.getElementById(ID); + + if( I.disabled ) + return; + + I.files = e.dataTransfer.files; + + if( type === 'rom' ) this.romFileName = file.name; + if( type === 'patch' ) this.patchFileName = file.name; + + I.dispatchEvent(new Event('change', { bubbles: true })); + } + } +} diff --git a/resources/js/SubmissionsClass/FSFileData.js b/resources/js/SubmissionsClass/FSFileData.js index 8fffbdd..a44b3c5 100644 --- a/resources/js/SubmissionsClass/FSFileData.js +++ b/resources/js/SubmissionsClass/FSFileData.js @@ -66,6 +66,16 @@ export function FSFileData(name, totalChunks, rawFile ) { */ state: 'public', + /** + * If the online patcher is enabled + */ + meta_online_patcher: false, + + /** + * If this patch is a secondary patch. + */ + meta_secondary_online_patcher: false, + /** * Look if this file is currently uploading. * @returns {boolean} diff --git a/resources/js/app.js b/resources/js/app.js index 860af44..068c39a 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -7,6 +7,7 @@ import hovercard from "./hovercard.js"; import notifications from "./notifications.js"; import conversations from "./conversations.js"; import settings from "./settings.js"; +import {RomPatcher} from "./RomPatcher.js"; /** * Get config defined in meta.blade.php @@ -43,3 +44,6 @@ Alpine.store('conversations', conversations() ); // Settings Alpine.store('settings', settings() ); + +// ROMPatcher +window.RomPatcher = RomPatcher; diff --git a/resources/js/submissions.js b/resources/js/submissions.js index 95a34c5..e9b0a67 100644 --- a/resources/js/submissions.js +++ b/resources/js/submissions.js @@ -372,6 +372,12 @@ window.Submission = function(){ this.errorKey = null; // Reset. this.duringSubmissionProcess = true; + const STATE = document.querySelector('select[name="submit-state"]')?.value; + if( STATE === 'draft' ){ + e.target.submit(); + return; + } + if( !this.verifyForm() ){ this.scrollToError(); diff --git a/resources/views/components/hovercard.blade.php b/resources/views/components/hovercard.blade.php index d62b801..0b17cbc 100644 --- a/resources/views/components/hovercard.blade.php +++ b/resources/views/components/hovercard.blade.php @@ -63,10 +63,10 @@ diff --git a/resources/views/components/menu.blade.php b/resources/views/components/menu.blade.php index 36e6559..c19add6 100644 --- a/resources/views/components/menu.blade.php +++ b/resources/views/components/menu.blade.php @@ -15,10 +15,12 @@ @endforeach diff --git a/resources/views/components/modcp-search.blade.php b/resources/views/components/modcp-search.blade.php new file mode 100644 index 0000000..241dc72 --- /dev/null +++ b/resources/views/components/modcp-search.blade.php @@ -0,0 +1,6 @@ + diff --git a/resources/views/components/submit-entry-status.blade.php b/resources/views/components/submit-entry-status.blade.php index 37d8503..368796f 100644 --- a/resources/views/components/submit-entry-status.blade.php +++ b/resources/views/components/submit-entry-status.blade.php @@ -1,6 +1,7 @@
+ @if($isEdit) +
+ +
+ @endif
@if( section_must_be( [ 'romhacks', 'homebrew' ], $section ) && !$isEdit ) @@ -25,4 +33,49 @@ @endif @endforeach + + @if($isEdit) + + @endif
diff --git a/resources/views/components/topbar.blade.php b/resources/views/components/topbar.blade.php index 39afe8f..98559d9 100644 --- a/resources/views/components/topbar.blade.php +++ b/resources/views/components/topbar.blade.php @@ -4,14 +4,16 @@ - +
- @if( !\Auth::guest() && \Auth::user()->is_admin === 1 ) + @can('is-admin') @php $topbarAdminSeparator = true; @endphp @@ -19,15 +21,15 @@ - @endif + @endcan @if( $topbarAdminSeparator )
@endif - @if( !\Auth::guest() && \Auth::user()->is_moderator === 1 ) + @can('is-mod') @php $topbarModSeparator = true; @endphp - + @@ -36,7 +38,7 @@ - @endif + @endcan @if( $topbarModSeparator )
diff --git a/resources/views/entries/draft_item.blade.php b/resources/views/entries/draft_item.blade.php new file mode 100644 index 0000000..914c58a --- /dev/null +++ b/resources/views/entries/draft_item.blade.php @@ -0,0 +1,54 @@ +
+
+ @if($entry->main_image) + + @else +
+ +
+ @endif +
+ +
+
+
+

+ {{ $entry->complete_title }} +

+
+ + {{ \App\Livewire\Database::ENTRY_TYPES[$entry->type] }} + + @if( $entry->getRealPlatform() ) + {{ $entry->getRealPlatform()->name }} + @endif + @if( $entry->version ) + {{ $entry->version }} + @endif +
+
+
+ + + Last edited {{ $draft->updated_at->diffForHumans() }} + + + + Created {{ $draft->created_at->format('d M Y') }} + +
+
+ + + +
+
diff --git a/resources/views/entries/drafts.blade.php b/resources/views/entries/drafts.blade.php new file mode 100644 index 0000000..91d7093 --- /dev/null +++ b/resources/views/entries/drafts.blade.php @@ -0,0 +1,25 @@ +@extends('layouts.app') + +@section('page-title', "My Drafts - " . config('app.name')) + +@section('content') +
+ My Drafts +
+ @if($drafts->isEmpty()) +
+ +

No drafts here

+
+ @else +
+ {{ $drafts->total() }} draft{{ $drafts->total() > 1 ? 's' : '' }} +
+
+ @foreach($drafts as $draft) + @include('entries.draft_item', ['entry' => $draft]) + @endforeach +
+ {{ $drafts->links() }} + @endif +@endsection diff --git a/resources/views/entries/show.blade.php b/resources/views/entries/show.blade.php index d1c49f5..11599e9 100644 --- a/resources/views/entries/show.blade.php +++ b/resources/views/entries/show.blade.php @@ -83,28 +83,28 @@
@@ -206,7 +206,7 @@ @endforeach
@endif - @if( $entry->staff_credits ) + @if( $entry->parseStaffCredits() )
    diff --git a/resources/views/layouts/modcp.blade.php b/resources/views/layouts/modcp.blade.php new file mode 100644 index 0000000..6a64097 --- /dev/null +++ b/resources/views/layouts/modcp.blade.php @@ -0,0 +1,95 @@ +@extends('layouts.app') + +@section('content') + +@endsection diff --git a/resources/views/livewire/database.blade.php b/resources/views/livewire/database.blade.php index 835db5a..2b977ce 100644 --- a/resources/views/livewire/database.blade.php +++ b/resources/views/livewire/database.blade.php @@ -37,6 +37,9 @@ {{-- Platforms --}} + {{-- Genres --}} + + {{-- Statuses --}} diff --git a/resources/views/modcp/authors.blade.php b/resources/views/modcp/authors.blade.php new file mode 100644 index 0000000..17708ba --- /dev/null +++ b/resources/views/modcp/authors.blade.php @@ -0,0 +1,89 @@ +@extends('layouts.modcp') + +@section('modcp-content') + +
    + Authors + {{ $items->total() }} +
    + + + +
    +
    + @csrf +
    + +
    + +
    + + +
    +
    +
    + +
    + @forelse($items as $author) +
    + +
    + {{ $author->name }} + + {{ $author->website ?? '—' }} + @if(($xfUser = $author->user()) !== null ) + + + + @endif + · {{ $author->entries_count }} {{ Str::plural('entry', $author->entries_count) }} + +
    + +
    + @csrf @method('PATCH') + +
    + +
    + + + +
    + +
    + +
    + @csrf @method('DELETE') + +
    +
    + +
    + @empty +

    No authors yet.

    + @endforelse +
    + + {{ $items->links() }} + +@endsection diff --git a/resources/views/modcp/deleted.blade.php b/resources/views/modcp/deleted.blade.php new file mode 100644 index 0000000..2737c7b --- /dev/null +++ b/resources/views/modcp/deleted.blade.php @@ -0,0 +1,61 @@ +@extends('layouts.modcp') + +@section('page-title', 'Deleted entries - ' . config('app.name') ) + +@section('modcp-content') + +
    + + Deleted entries + {{ $entries->total() }} +
    + + @if($entries->isEmpty()) +
    + +

    No deleted entries.

    +
    + @else +
    + @foreach($entries as $entry) +
    +
    + @if($entry->main_image) + + @else + + @endif +
    +
    + {{ $entry->complete_title ?? $entry->title }} + + {{ $entry->type }} + @php $daysLeft = max(0, 7 - (int) now()->diffInDays($entry->deleted_at)) @endphp + + Deleted {{ $entry->deleted_at->diffForHumans() }} + @if($daysLeft > 0) · purged in {{ $daysLeft }}d @endif + + +
    +
    +
    + @csrf @method('PATCH') + +
    +
    + @csrf @method('DELETE') + +
    +
    +
    + @endforeach +
    + {{ $entries->links() }} + @endif + +@endsection diff --git a/resources/views/modcp/entries.blade.php b/resources/views/modcp/entries.blade.php new file mode 100644 index 0000000..4089d75 --- /dev/null +++ b/resources/views/modcp/entries.blade.php @@ -0,0 +1,52 @@ +@extends('layouts.modcp') + +@section('page-title', $pageTitle . ' - ' . config('app.name') ) + +@section('modcp-content') +
    + {{ $pageTitle }} + {{ $entries->count() }} +
    + + @if($entries->isEmpty()) +
    + +

    No {{ $state }} entries.

    +
    + @else +
    + @foreach($entries as $entry) +
    +
    + @if($entry->main_image) + + @else + + @endif +
    +
    + {{ $entry->complete_title }} + + {{ \App\Livewire\Database::ENTRY_TYPES[$entry->type] }} + @if($entry->getRealPlatform()) + {{ $entry->getRealPlatform()->name }} + @endif + Added {{ $entry->created_at->format('d M Y') }} by + +
    + +
    + @endforeach +
    + {{ $entries->links() }} + @endif +@endsection diff --git a/resources/views/modcp/games.blade.php b/resources/views/modcp/games.blade.php new file mode 100644 index 0000000..16c6347 --- /dev/null +++ b/resources/views/modcp/games.blade.php @@ -0,0 +1,103 @@ +@extends('layouts.modcp') + +@section('modcp-content') + +
    + Games + {{ $items->total() }} +
    + + + +
    +
    + @csrf +
    + + + + +
    +
    +
    + +
    + @forelse($items as $game) +
    + +
    + {{ $game->name }} + + {{ $game->platform->name ?? '—' }} + {{ $game->genre->name ?? '—' }} + · {{ $game->entries_count }} {{ Str::plural('entry', $game->entries_count) }} + +
    + +
    + @csrf @method('PATCH') + + + + + +
    + +
    + +
    + @csrf @method('DELETE') + +
    +
    + +
    + @empty +

    No games yet.

    + @endforelse +
    + + {{ $items->links() }} + +@endsection diff --git a/resources/views/modcp/index.blade.php b/resources/views/modcp/index.blade.php new file mode 100644 index 0000000..1e881cb --- /dev/null +++ b/resources/views/modcp/index.blade.php @@ -0,0 +1,91 @@ +@extends('layouts.modcp') + +@section('page-title', "Dashboard - " . config('app.name') ) + +@section('modcp-content') +
    + Dashboard +
    + + + @if($recentDeleted->isNotEmpty()) +
    Recently deleted
    +
    + @foreach($recentDeleted as $entry) +
    +
    + {{ $entry->complete_title ?? $entry->title }} + + {{ $entry->type }} + Deleted {{ $entry->deleted_at->diffForHumans() }} + +
    +
    +
    + @csrf @method('PATCH') + +
    +
    + @csrf @method('DELETE') + +
    +
    +
    + @endforeach + + See all deleted entries → + +
    + @endif + +@endsection diff --git a/resources/views/modcp/resources.blade.php b/resources/views/modcp/resources.blade.php new file mode 100644 index 0000000..37aae3d --- /dev/null +++ b/resources/views/modcp/resources.blade.php @@ -0,0 +1,76 @@ +@extends('layouts.modcp') + +@section('page-title', $title . ' - ' . config('app.name')) + +@section('modcp-content') + +
    + {{ $title }} + {{ $items->total() }} +
    + + + +
    +
    + @csrf + + @if(isset($extraFields)) + @foreach($extraFields as $field) + + @endforeach + @endif + +
    +
    + +
    + @forelse($items as $item) +
    +
    + {{ $item->name }} + + slug: {{ $item->slug }} + @isset($item->entries_count) + · {{ $item->entries_count }} {{ Str::plural('entry', $item->entries_count) }} + @endisset + +
    + +
    + @csrf @method('PATCH') + + + +
    + +
    + +
    + @csrf @method('DELETE') + +
    +
    +
    + @empty +
    +

    No {{ strtolower($title) }} yet.

    +
    + @endforelse +
    + + {{ $items->links() }} + +@endsection diff --git a/resources/views/submissions/form.blade.php b/resources/views/submissions/form.blade.php index f57ee45..6293f7c 100644 --- a/resources/views/submissions/form.blade.php +++ b/resources/views/submissions/form.blade.php @@ -137,7 +137,7 @@ @endif - + @error('gallery') @@ -181,6 +181,18 @@
+
+
+ + +
+
+
+ +
+ +
+
@endcan @cannot('moderate', $entry) diff --git a/resources/views/submissions/fs-upload.blade.php b/resources/views/submissions/fs-upload.blade.php index 9027dab..9030f63 100644 --- a/resources/views/submissions/fs-upload.blade.php +++ b/resources/views/submissions/fs-upload.blade.php @@ -36,21 +36,18 @@ 'upload-item-done': file.done, 'upload-item-error': file.error }"> - - - - - + +
+ + + + + + + + + +
@@ -103,13 +100,46 @@
@endif -
+
+ @if($isEdit) + + @endif +
@if($isEdit) diff --git a/resources/views/tools/patcher.blade.php b/resources/views/tools/patcher.blade.php new file mode 100644 index 0000000..2bf120a --- /dev/null +++ b/resources/views/tools/patcher.blade.php @@ -0,0 +1,92 @@ +@extends('layouts.app') + +@section('page-title', "ROM Patcher - " . config('app.name')) + +@push('scripts') + +@endpush + +@section('content') +
+ ROM Patcher +
+ +
+ +
+
+ +
+ + + + +
+
+ +
+ + +
+ + + + +
+ +
+
+
+ +
+
+
Patch file
+
Select if there is multiple patch files
+
+
+ +
+ +
+
+
+
+ +
+
+
Checksums:
+
    +
  • CRC32:
  • +
  • MD5:
  • +
  • SHA-1:
  • +
+
+ +
+
Description:
+
+
+
+
ROM requirements:
+
+
+
+ +
+ +
+
+@endsection diff --git a/routes/web.php b/routes/web.php index 62fae69..9f12a07 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ name Route::name('entries.')->controller(EntryController::class)->group(function () { Route::get('/database', 'index' )->name('index'); + Route::get('/{section}', 'section_redirect' )->name('section_redirect') + ->where(['section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials']); + Route::get('/{section}/{entry:slug}', 'show' )->name('show')->where( [ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials', @@ -19,6 +23,8 @@ Route::name('entries.')->controller(EntryController::class)->group(function () { ] ); + Route::get('/my-drafts', 'drafts' )->middleware('xf.auth')->name('drafts'); + }); // SubmissionController. @@ -34,6 +40,8 @@ Route::name('submit.')->prefix('/edit')->controller(\App\Http\Controllers\Submis ->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials', 'entry' => '[0-9\-]+' ]); Route::post('/{section}/{entry:id}', 'update' )->name('update') ->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials', 'entry' => '[0-9\-]+' ]); + Route::delete('/{section}/{entry:id}', 'destroy' )->name('destroy') + ->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials', 'entry' => '[0-9\-]+' ]); }); // QueueController @@ -56,6 +64,30 @@ Route::name('queue.')->prefix('/queue')->controller(\App\Http\Controllers\QueueC ->name('reject'); }); +// ToolsController +Route::name('tools.')->controller(\App\Http\Controllers\ToolsController::class)->group(function () { + Route::get('/patch', 'patcher' )->name('patcher'); +}); + +// ModeratorCPController +Route::name('modcp.')->prefix('/modcp')->controller(\App\Http\Controllers\ModCPController::class)->middleware(['xf.auth','can:is-mod'])->group(function () { + + Route::get('/', 'index' )->name('index'); + Route::get('/locked-entries', 'locked' )->name('locked'); + Route::get('/draft-entries', 'draft' )->name('draft')->middleware('can:is-admin'); + Route::get('/hidden-entries', 'hidden' )->name('hidden')->middleware('can:is-admin'); + Route::get('/deleted-entries', 'deleted' )->name('deleted')->middleware('can:is-admin'); + + Route::patch('/restore/{entry}', 'restore' )->name('restore')->where(['entry' => '[0-9\-]+'])->withTrashed()->middleware('can:is-admin'); + Route::delete('/purge/{entry}', 'destroy' )->name('destroy')->where(['entry' => '[0-9\-]+'])->withTrashed()->middleware('can:is-admin'); + + Route::resource('games', \App\Http\Controllers\ModCP\GameController::class)->only(['index', 'store','update','destroy']); + Route::resource('languages', LanguageController::class)->only(['index', 'store','update','destroy']); + Route::resource('authors', \App\Http\Controllers\ModCP\AuthorController::class)->only(['index', 'store','update','destroy']); + Route::resource('platforms', \App\Http\Controllers\ModCP\PlatformController::class )->middleware('can:is-admin')->only(['index', 'store','update','destroy']); + Route::resource('genres', \App\Http\Controllers\ModCP\GenreController::class )->middleware('can:is-admin')->only(['index', 'store','update','destroy']); +}); + // RedirectController Route::name('redirect.')->controller(\App\Http\Controllers\RedirectController::class)->group(function () { Route::get('/entry/report_redirect', 'entryReportRedirect' )->name('entry_report'); diff --git a/vite.config.js b/vite.config.js index 5981261..7b5ed4b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -22,8 +22,8 @@ export default defineConfig({ ignored: ['**/storage/framework/views/**'], }, https: { - cert: '/mnt/01D9BE39AF0FC580/romhackplaza/rhpz.local+1.pem', - key: '/mnt/01D9BE39AF0FC580/romhackplaza/rhpz.local+1-key.pem', + cert: '/mnt/ssd-data/sites/romhackplaza/rhpz.local+1.pem', + key: '/mnt/ssd-data/sites/romhackplaza/rhpz.local+1-key.pem', }, }, });