A lot of things.
This commit is contained in:
@@ -74,4 +74,9 @@ class XenForoGuard implements Guard
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
{
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']),
|
||||
|
||||
@@ -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 ),
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
72
app/Http/Controllers/ModCP/AuthorController.php
Normal file
72
app/Http/Controllers/ModCP/AuthorController.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\ModCP;
|
||||
|
||||
use App\Helpers\EntryHelpers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Author;
|
||||
use App\Models\Language;
|
||||
use App\Rules\XfUserExists;
|
||||
use App\Traits\ModCPSearch;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AuthorController extends Controller
|
||||
{
|
||||
|
||||
use ModCPSearch;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$items = Author::withCount('entries')
|
||||
->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.');
|
||||
}
|
||||
}
|
||||
75
app/Http/Controllers/ModCP/GameController.php
Normal file
75
app/Http/Controllers/ModCP/GameController.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\ModCP;
|
||||
|
||||
use App\Helpers\EntryHelpers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Game;
|
||||
use App\Models\Genre;
|
||||
use App\Models\Platform;
|
||||
use App\Traits\ModCPSearch;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GameController extends Controller
|
||||
{
|
||||
|
||||
use ModCPSearch;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$items = Game::withCount('entries')->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.');
|
||||
}
|
||||
}
|
||||
68
app/Http/Controllers/ModCP/GenreController.php
Normal file
68
app/Http/Controllers/ModCP/GenreController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\ModCP;
|
||||
|
||||
use App\Helpers\EntryHelpers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Genre;
|
||||
use App\Models\Language;
|
||||
use App\Traits\ModCPSearch;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GenreController extends Controller
|
||||
{
|
||||
|
||||
use ModCPSearch;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$items = Genre::withCount('games')
|
||||
->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.');
|
||||
}
|
||||
}
|
||||
67
app/Http/Controllers/ModCP/LanguageController.php
Normal file
67
app/Http/Controllers/ModCP/LanguageController.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\ModCP;
|
||||
|
||||
use App\Helpers\EntryHelpers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Language;
|
||||
use App\Traits\ModCPSearch;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LanguageController extends Controller
|
||||
{
|
||||
|
||||
use ModCPSearch;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$items = Language::withCount('entries')
|
||||
->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.');
|
||||
}
|
||||
}
|
||||
68
app/Http/Controllers/ModCP/PlatformController.php
Normal file
68
app/Http/Controllers/ModCP/PlatformController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\ModCP;
|
||||
|
||||
use App\Helpers\EntryHelpers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Platform;
|
||||
use App\Models\Language;
|
||||
use App\Traits\ModCPSearch;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PlatformController extends Controller
|
||||
{
|
||||
|
||||
use ModCPSearch;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$items = Platform::withCount(['games','entries'])
|
||||
->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.');
|
||||
}
|
||||
}
|
||||
78
app/Http/Controllers/ModCPController.php
Normal file
78
app/Http/Controllers/ModCPController.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\RestoreXenForoCommentsThread;
|
||||
use App\Models\Entry;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ModCPController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$stats = [
|
||||
'pending' => 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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
22
app/Http/Controllers/ToolsController.php
Normal file
22
app/Http/Controllers/ToolsController.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ToolsController extends Controller
|
||||
{
|
||||
|
||||
public function patcher()
|
||||
{
|
||||
|
||||
$patches = [
|
||||
'file' => 'ZELDA.ips',
|
||||
'name' => "Meltin",
|
||||
'description' => 'Blablabla',
|
||||
'outputName' => 'Game...'
|
||||
];
|
||||
|
||||
return view('tools.patcher', compact('patches'));
|
||||
}
|
||||
}
|
||||
26
app/Http/Requests/StoreDraftRequest.php
Normal file
26
app/Http/Requests/StoreDraftRequest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class StoreDraftRequest extends StoreEntryRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = parent::rules();
|
||||
$rules['submit-state'] = 'required|string|in:draft';
|
||||
|
||||
$rules = array_map(function($rule){
|
||||
if( is_array($rule) ){
|
||||
return array_map( fn($r) => $r === 'required' ? 'nullable' : $r, $rule);
|
||||
}
|
||||
|
||||
return preg_replace(
|
||||
['/\brequired_without\S*/', '/required_with\S*/', '/\brequired\b/'],
|
||||
['nullable', 'nullable', 'nullable'],
|
||||
$rule
|
||||
);
|
||||
}, $rules );
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
34
app/Jobs/DeleteXenForoCommentsThread.php
Normal file
34
app/Jobs/DeleteXenForoCommentsThread.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Entry;
|
||||
use App\Services\XenforoApiService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
class DeleteXenForoCommentsThread implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 3;
|
||||
public $backoff = 10;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
protected int $threadId,
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(XenforoApiService $service): void
|
||||
{
|
||||
$service->deleteThreadWithEntry($this->threadId);
|
||||
}
|
||||
}
|
||||
34
app/Jobs/RestoreXenForoCommentsThread.php
Normal file
34
app/Jobs/RestoreXenForoCommentsThread.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Entry;
|
||||
use App\Services\XenforoApiService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
class RestoreXenForoCommentsThread implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 3;
|
||||
public $backoff = 10;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
protected int $threadId,
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(XenforoApiService $service): void
|
||||
{
|
||||
$service->restoreThreadWithEntry($this->threadId);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 ){
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' );
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class ManagePanelProvider extends PanelProvider
|
||||
->default()
|
||||
->id('manage')
|
||||
->path('manage')
|
||||
->login()
|
||||
->authGuard('xenforo')
|
||||
->colors([
|
||||
'primary' => Color::Amber,
|
||||
])
|
||||
|
||||
@@ -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,6 +193,7 @@ class SubmissionsService {
|
||||
$this->Step13_CreateCommentsThread( $entry );
|
||||
|
||||
// Step 14: Refresh XF count.
|
||||
if( $entry->state !== 'draft')
|
||||
XenForoHelpers::updateEntriesCount( $entry->user_id );
|
||||
|
||||
return $entry;
|
||||
@@ -195,16 +201,22 @@ class SubmissionsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* @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( $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 );
|
||||
|
||||
@@ -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" );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
23
app/Traits/ModCPSearch.php
Normal file
23
app/Traits/ModCPSearch.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
trait ModCPSearch
|
||||
{
|
||||
protected function applySearch(Builder $query, array $columns = ['name'] ): Builder
|
||||
{
|
||||
$search = request('s', '');
|
||||
if( !$search )
|
||||
return $query;
|
||||
|
||||
return $query->where(function ($query) use ($columns, $search) {
|
||||
foreach ($columns as $i => $column) {
|
||||
$method = $i === 0 ? 'where' : 'orWhere';
|
||||
$query->{$method}($column, 'LIKE', "%{$search}%");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
29
app/View/Components/ModCPSearch.php
Normal file
29
app/View/Components/ModCPSearch.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ModCPSearch extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $placeholder = "Search...",
|
||||
public string $param = 's'
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.modcp-search');
|
||||
}
|
||||
}
|
||||
@@ -24,3 +24,40 @@ if( !function_exists( 'section_must_not_be' ) ){
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if( !function_exists( 'databaseRoute' ) ){
|
||||
|
||||
function databaseRoute( array $params = [] ): string
|
||||
{
|
||||
$defaults = [
|
||||
'types' => [],
|
||||
'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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
//
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('entries', function (Blueprint $table) {
|
||||
$table->string('title')->nullable()->change();
|
||||
$table->longText('description')->nullable()->change();
|
||||
$table->string('slug')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('entry_files', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
528
extra.less
528
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 {
|
||||
|
||||
BIN
public/ZELDA.ips
Normal file
BIN
public/ZELDA.ips
Normal file
Binary file not shown.
412
public/rom-patcher-js/RomPatcher.js
Normal file
412
public/rom-patcher-js/RomPatcher.js
Normal file
@@ -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');
|
||||
}
|
||||
2167
public/rom-patcher-js/RomPatcher.webapp.js
Normal file
2167
public/rom-patcher-js/RomPatcher.webapp.js
Normal file
File diff suppressed because it is too large
Load Diff
83
public/rom-patcher-js/RomPatcher.webworker.apply.js
Normal file
83
public/rom-patcher-js/RomPatcher.webworker.apply.js
Normal file
@@ -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
|
||||
]
|
||||
);
|
||||
}
|
||||
};
|
||||
26
public/rom-patcher-js/RomPatcher.webworker.crc.js
Normal file
26
public/rom-patcher-js/RomPatcher.webworker.crc.js
Normal file
@@ -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]
|
||||
);
|
||||
};
|
||||
37
public/rom-patcher-js/RomPatcher.webworker.create.js
Normal file
37
public/rom-patcher-js/RomPatcher.webworker.create.js
Normal file
@@ -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
|
||||
]
|
||||
);
|
||||
};
|
||||
483
public/rom-patcher-js/modules/BinFile.js
Normal file
483
public/rom-patcher-js/modules/BinFile.js
Normal file
@@ -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');
|
||||
}
|
||||
179
public/rom-patcher-js/modules/HashCalculator.js
Normal file
179
public/rom-patcher-js/modules/HashCalculator.js
Normal file
@@ -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;
|
||||
}
|
||||
114
public/rom-patcher-js/modules/RomPatcher.format.aps_gba.js
Normal file
114
public/rom-patcher-js/modules/RomPatcher.format.aps_gba.js
Normal file
@@ -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; i<this.records.length; i++){
|
||||
sourceFile.seek(this.records[i].offset);
|
||||
var bytes=sourceFile.readBytes(APS_GBA_BLOCK_SIZE);
|
||||
if(sourceFile.hashCRC16(this.records[i].offset, APS_GBA_BLOCK_SIZE) !== this.records[i].sourceCrc16)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
APSGBA.prototype.export=function(fileName){
|
||||
var patchFileSize=12 + (this.records.length * APS_GBA_RECORD_SIZE);
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.littleEndian=true;
|
||||
tempFile.fileName=fileName+'.aps';
|
||||
tempFile.writeString(APS_GBA_MAGIC, APS_GBA_MAGIC.length);
|
||||
tempFile.writeU32(this.sourceSize);
|
||||
tempFile.writeU32(this.targetSize);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.writeU32(this.records[i].offset);
|
||||
tempFile.writeU16(this.records[i].sourceCrc16);
|
||||
tempFile.writeU16(this.records[i].targetCrc16);
|
||||
tempFile.writeBytes(this.records[i].xorBytes);
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APSGBA.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
tempFile=new BinFile(this.targetSize);
|
||||
romFile.copyTo(tempFile, 0, romFile.fileSize);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
romFile.seek(this.records[i].offset);
|
||||
tempFile.seek(this.records[i].offset);
|
||||
for(var j=0; j<APS_GBA_BLOCK_SIZE; j++){
|
||||
tempFile.writeU8(romFile.readU8() ^ this.records[i].xorBytes[j]);
|
||||
}
|
||||
|
||||
if(validate && tempFile.hashCRC16(this.records[i].offset, APS_GBA_BLOCK_SIZE)!==this.records[i].targetCrc16){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APSGBA.MAGIC=APS_GBA_MAGIC;
|
||||
|
||||
APSGBA.fromFile=function(patchFile){
|
||||
patchFile.seek(0);
|
||||
patchFile.littleEndian=true;
|
||||
|
||||
if(
|
||||
patchFile.readString(APS_GBA_MAGIC.length)!==APS_GBA_MAGIC ||
|
||||
patchFile.fileSize < (12 + APS_GBA_RECORD_SIZE) ||
|
||||
(patchFile.fileSize-12)%APS_GBA_RECORD_SIZE!==0
|
||||
)
|
||||
return null;
|
||||
|
||||
var patch=new APSGBA();
|
||||
|
||||
patch.sourceSize=patchFile.readU32();
|
||||
patch.targetSize=patchFile.readU32();
|
||||
|
||||
while(!patchFile.isEOF()){
|
||||
var offset=patchFile.readU32();
|
||||
var sourceCrc16=patchFile.readU16();
|
||||
var targetCrc16=patchFile.readU16();
|
||||
var xorBytes=patchFile.readBytes(APS_GBA_BLOCK_SIZE);
|
||||
|
||||
patch.addRecord(offset, sourceCrc16, targetCrc16, xorBytes);
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
211
public/rom-patcher-js/modules/RomPatcher.format.aps_n64.js
Normal file
211
public/rom-patcher-js/modules/RomPatcher.format.aps_n64.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/* APS (N64) module for Rom Patcher JS v20180930 - Marc Robledo 2017-2018 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: https://github.com/btimofeev/UniPatcher/wiki/APS-(N64) */
|
||||
|
||||
const APS_N64_MAGIC='APS10';
|
||||
const APS_RECORD_RLE=0x0000;
|
||||
const APS_RECORD_SIMPLE=0x01;
|
||||
const APS_N64_MODE=0x01;
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = APS;
|
||||
}
|
||||
function APS(){
|
||||
this.records=[];
|
||||
this.headerType=0;
|
||||
this.encodingMethod=0;
|
||||
this.description='no description';
|
||||
|
||||
this.header={};
|
||||
}
|
||||
APS.prototype.addRecord=function(o, d){
|
||||
this.records.push({offset:o, type:APS_RECORD_SIMPLE, data:d})
|
||||
}
|
||||
APS.prototype.addRLERecord=function(o, b, l){
|
||||
this.records.push({offset:o, type:APS_RECORD_RLE, length:l, byte:b})
|
||||
}
|
||||
APS.prototype.toString=function(){
|
||||
var s='Total records: '+this.records.length;
|
||||
s+='\nHeader type: '+this.headerType;
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
s+=' (N64)';
|
||||
}
|
||||
s+='\nEncoding method: '+this.encodingMethod;
|
||||
s+='\nDescription: '+this.description;
|
||||
s+='\nHeader: '+JSON.stringify(this.header);
|
||||
return s
|
||||
}
|
||||
APS.prototype.validateSource=function(sourceFile){
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
sourceFile.seek(0x3c);
|
||||
if(sourceFile.readString(3)!==this.header.cartId)
|
||||
return false;
|
||||
|
||||
sourceFile.seek(0x10);
|
||||
var crc=sourceFile.readBytes(8);
|
||||
for(var i=0; i<8; i++){
|
||||
if(crc[i]!==this.header.crc[i])
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
APS.prototype.getValidationInfo=function(){
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
return this.header.cartId + ' (' + this.header.crc.reduce(function(hex, b){
|
||||
if(b<16)
|
||||
return hex + '0' + b.toString(16);
|
||||
else
|
||||
return hex + b.toString(16);
|
||||
}, '') + ')';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
APS.prototype.export=function(fileName){
|
||||
var patchFileSize=61;
|
||||
if(this.headerType===APS_N64_MODE)
|
||||
patchFileSize+=17;
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
if(this.records[i].type===APS_RECORD_RLE)
|
||||
patchFileSize+=7;
|
||||
else
|
||||
patchFileSize+=5+this.records[i].data.length; //offset+length+data
|
||||
}
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.littleEndian=true;
|
||||
tempFile.fileName=fileName+'.aps';
|
||||
tempFile.writeString(APS_N64_MAGIC, APS_N64_MAGIC.length);
|
||||
tempFile.writeU8(this.headerType);
|
||||
tempFile.writeU8(this.encodingMethod);
|
||||
tempFile.writeString(this.description, 50);
|
||||
|
||||
if(this.headerType===APS_N64_MODE){
|
||||
tempFile.writeU8(this.header.originalN64Format);
|
||||
tempFile.writeString(this.header.cartId, 3);
|
||||
tempFile.writeBytes(this.header.crc);
|
||||
tempFile.writeBytes(this.header.pad);
|
||||
}
|
||||
tempFile.writeU32(this.header.sizeOutput);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
var rec=this.records[i];
|
||||
tempFile.writeU32(rec.offset);
|
||||
if(rec.type===APS_RECORD_RLE){
|
||||
tempFile.writeU8(0x00);
|
||||
tempFile.writeU8(rec.byte);
|
||||
tempFile.writeU8(rec.length);
|
||||
}else{
|
||||
tempFile.writeU8(rec.data.length);
|
||||
tempFile.writeBytes(rec.data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APS.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
tempFile=new BinFile(this.header.sizeOutput);
|
||||
romFile.copyTo(tempFile, 0, tempFile.fileSize);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
if(this.records[i].type===APS_RECORD_RLE){
|
||||
for(var j=0; j<this.records[i].length; j++)
|
||||
tempFile.writeU8(this.records[i].byte);
|
||||
}else{
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
APS.MAGIC=APS_N64_MAGIC;
|
||||
|
||||
|
||||
|
||||
APS.fromFile=function(patchFile){
|
||||
var patch=new APS();
|
||||
patchFile.littleEndian=true;
|
||||
|
||||
patchFile.seek(5);
|
||||
patch.headerType=patchFile.readU8();
|
||||
patch.encodingMethod=patchFile.readU8();
|
||||
patch.description=patchFile.readString(50);
|
||||
|
||||
var seek;
|
||||
if(patch.headerType===APS_N64_MODE){
|
||||
patch.header.originalN64Format=patchFile.readU8();
|
||||
patch.header.cartId=patchFile.readString(3);
|
||||
patch.header.crc=patchFile.readBytes(8);
|
||||
patch.header.pad=patchFile.readBytes(5);
|
||||
}
|
||||
patch.header.sizeOutput=patchFile.readU32();
|
||||
|
||||
while(!patchFile.isEOF()){
|
||||
var offset=patchFile.readU32();
|
||||
var length=patchFile.readU8();
|
||||
|
||||
if(length===APS_RECORD_RLE)
|
||||
patch.addRLERecord(offset, patchFile.readU8(seek), patchFile.readU8(seek+1));
|
||||
else
|
||||
patch.addRecord(offset, patchFile.readBytes(length));
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
|
||||
APS.buildFromRoms=function(original, modified){
|
||||
var patch=new APS();
|
||||
|
||||
|
||||
|
||||
if(original.readU32()===0x80371240){ //is N64 ROM
|
||||
patch.headerType=APS_N64_MODE;
|
||||
|
||||
patch.header.originalN64Format=/\.v64$/i.test(original.fileName)?0:1;
|
||||
original.seek(0x3c);
|
||||
patch.header.cartId=original.readString(3);
|
||||
original.seek(0x10);
|
||||
patch.header.crc=original.readBytes(8);
|
||||
patch.header.pad=[0,0,0,0,0];
|
||||
}
|
||||
patch.header.sizeOutput=modified.fileSize;
|
||||
|
||||
original.seek(0);
|
||||
modified.seek(0);
|
||||
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var RLERecord=true;
|
||||
var differentBytes=[];
|
||||
var offset=modified.offset-1;
|
||||
|
||||
while(b1!==b2 && differentBytes.length<0xff){
|
||||
differentBytes.push(b2);
|
||||
if(b2!==differentBytes[0])
|
||||
RLERecord=false;
|
||||
|
||||
if(modified.isEOF() || differentBytes.length===0xff)
|
||||
break;
|
||||
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
if(RLERecord && differentBytes.length>2){
|
||||
patch.addRLERecord(offset, differentBytes[0], differentBytes.length);
|
||||
}else{
|
||||
patch.addRecord(offset, differentBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return patch
|
||||
}
|
||||
90
public/rom-patcher-js/modules/RomPatcher.format.bdf.js
Normal file
90
public/rom-patcher-js/modules/RomPatcher.format.bdf.js
Normal file
File diff suppressed because one or more lines are too long
466
public/rom-patcher-js/modules/RomPatcher.format.bps.js
Normal file
466
public/rom-patcher-js/modules/RomPatcher.format.bps.js
Normal file
@@ -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<this.actions.length; i++){
|
||||
var action=this.actions[i];
|
||||
|
||||
if(action.type===BPS_ACTION_SOURCE_READ){
|
||||
romFile.copyTo(tempFile, tempFile.offset, action.length);
|
||||
tempFile.skip(action.length);
|
||||
|
||||
}else if(action.type===BPS_ACTION_TARGET_READ){
|
||||
tempFile.writeBytes(action.bytes);
|
||||
|
||||
}else if(action.type===BPS_ACTION_SOURCE_COPY){
|
||||
sourceRelativeOffset+=action.relativeOffset;
|
||||
var actionLength=action.length;
|
||||
while(actionLength--){
|
||||
tempFile.writeU8(romFile._u8array[sourceRelativeOffset]);
|
||||
sourceRelativeOffset++;
|
||||
}
|
||||
}else if(action.type===BPS_ACTION_TARGET_COPY){
|
||||
targetRelativeOffset+=action.relativeOffset;
|
||||
var actionLength=action.length;
|
||||
while(actionLength--) {
|
||||
tempFile.writeU8(tempFile._u8array[targetRelativeOffset]);
|
||||
targetRelativeOffset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(validate && this.targetChecksum!==tempFile.hashCRC32()){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
BPS.MAGIC=BPS_MAGIC;
|
||||
|
||||
|
||||
BPS.fromFile=function(file){
|
||||
file.readVLV=BPS_readVLV;
|
||||
|
||||
file.littleEndian=true;
|
||||
var patch=new BPS();
|
||||
|
||||
|
||||
file.seek(4); //skip BPS1
|
||||
|
||||
patch.sourceSize=file.readVLV();
|
||||
patch.targetSize=file.readVLV();
|
||||
|
||||
var metaDataLength=file.readVLV();
|
||||
if(metaDataLength){
|
||||
patch.metaData=file.readString(metaDataLength);
|
||||
}
|
||||
|
||||
|
||||
var endActionsOffset=file.fileSize-12;
|
||||
while(file.offset<endActionsOffset){
|
||||
var data=file.readVLV();
|
||||
var action={type: data & 3, length: (data >> 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<this.actions.length; i++){
|
||||
var action=this.actions[i];
|
||||
patchFileSize+=BPS_getVLVLen(((action.length-1)<<2) + action.type);
|
||||
|
||||
if(action.type===BPS_ACTION_TARGET_READ){
|
||||
patchFileSize+=action.length;
|
||||
}else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){
|
||||
patchFileSize+=BPS_getVLVLen((Math.abs(action.relativeOffset)<<1)+(action.relativeOffset<0?1:0));
|
||||
}
|
||||
}
|
||||
patchFileSize+=12;
|
||||
|
||||
var patchFile=new BinFile(patchFileSize);
|
||||
patchFile.fileName=fileName+'.bps';
|
||||
patchFile.littleEndian=true;
|
||||
patchFile.writeVLV=BPS_writeVLV;
|
||||
|
||||
patchFile.writeString(BPS_MAGIC);
|
||||
patchFile.writeVLV(this.sourceSize);
|
||||
patchFile.writeVLV(this.targetSize);
|
||||
patchFile.writeVLV(this.metaData.length);
|
||||
patchFile.writeString(this.metaData, this.metaData.length);
|
||||
|
||||
for(var i=0; i<this.actions.length; i++){
|
||||
var action=this.actions[i];
|
||||
patchFile.writeVLV(((action.length-1)<<2) + action.type);
|
||||
|
||||
if(action.type===BPS_ACTION_TARGET_READ){
|
||||
patchFile.writeBytes(action.bytes);
|
||||
}else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){
|
||||
patchFile.writeVLV((Math.abs(action.relativeOffset)<<1)+(action.relativeOffset<0?1:0));
|
||||
}
|
||||
}
|
||||
patchFile.writeU32(this.sourceChecksum);
|
||||
patchFile.writeU32(this.targetChecksum);
|
||||
patchFile.writeU32(this.patchChecksum);
|
||||
|
||||
return patchFile;
|
||||
}
|
||||
|
||||
function BPS_Node(){
|
||||
this.offset=0;
|
||||
this.next=null;
|
||||
};
|
||||
BPS_Node.prototype.delete=function(){
|
||||
if(this.next)
|
||||
delete this.next;
|
||||
}
|
||||
BPS.buildFromRoms=function(original, modified, deltaMode){
|
||||
var patch=new BPS();
|
||||
patch.sourceSize=original.fileSize;
|
||||
patch.targetSize=modified.fileSize;
|
||||
|
||||
if(deltaMode){
|
||||
patch.actions=createBPSFromFilesDelta(original, modified);
|
||||
}else{
|
||||
patch.actions=createBPSFromFilesLinear(original, modified);
|
||||
}
|
||||
|
||||
patch.sourceChecksum=original.hashCRC32();
|
||||
patch.targetChecksum=modified.hashCRC32();
|
||||
patch.patchChecksum = patch.calculateFileChecksum();
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
/* delta implementation from https://github.com/chiya/beat/blob/master/nall/beat/linear.hpp */
|
||||
function createBPSFromFilesLinear(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 targetRelativeOffset=0;
|
||||
var outputOffset=0;
|
||||
var targetReadLength=0;
|
||||
|
||||
function targetReadFlush(){
|
||||
if(targetReadLength){
|
||||
//encode(TargetRead | ((targetReadLength - 1) << 2));
|
||||
var action={type:BPS_ACTION_TARGET_READ, length:targetReadLength, bytes:[]};
|
||||
patchActions.push(action);
|
||||
var offset = outputOffset - targetReadLength;
|
||||
while(targetReadLength){
|
||||
//write(targetData[offset++]);
|
||||
action.bytes.push(targetData[offset++]);
|
||||
targetReadLength--;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while(outputOffset < targetSize) {
|
||||
var sourceLength = 0;
|
||||
for(var n = 0; outputOffset + n < Math.min(sourceSize, targetSize); n++) {
|
||||
if(sourceData[outputOffset + n] != targetData[outputOffset + n]) break;
|
||||
sourceLength++;
|
||||
}
|
||||
|
||||
var rleLength = 0;
|
||||
for(var n = 1; outputOffset + n < targetSize; n++) {
|
||||
if(targetData[outputOffset] != targetData[outputOffset + n]) break;
|
||||
rleLength++;
|
||||
}
|
||||
|
||||
if(rleLength >= 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<sourceSize; offset++) {
|
||||
var symbol = sourceData[offset + 0];
|
||||
//sourceChecksum = crc32_adjust(sourceChecksum, symbol);
|
||||
if(offset < sourceSize - 1)
|
||||
symbol |= sourceData[offset + 1] << 8;
|
||||
var node=new BPS_Node();
|
||||
node.offset=offset;
|
||||
node.next=sourceTree[symbol];
|
||||
sourceTree[symbol] = node;
|
||||
}
|
||||
|
||||
var targetReadLength=0;
|
||||
|
||||
function targetReadFlush(){
|
||||
if(targetReadLength) {
|
||||
//encode(TargetRead | ((targetReadLength - 1) << 2));
|
||||
var action={type:BPS_ACTION_TARGET_READ, length:targetReadLength, bytes:[]};
|
||||
patchActions.push(action);
|
||||
var offset = outputOffset - targetReadLength;
|
||||
while(targetReadLength){
|
||||
//write(targetData[offset++]);
|
||||
action.bytes.push(targetData[offset++]);
|
||||
targetReadLength--;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while(outputOffset<modified.fileSize){
|
||||
var maxLength = 0, maxOffset = 0, mode = BPS_ACTION_TARGET_READ;
|
||||
|
||||
var symbol = targetData[outputOffset + 0];
|
||||
if(outputOffset < targetSize - 1) symbol |= targetData[outputOffset + 1] << 8;
|
||||
|
||||
{ //source read
|
||||
var length = 0, offset = outputOffset;
|
||||
while(offset < sourceSize && offset < targetSize && sourceData[offset] == targetData[offset]) {
|
||||
length++;
|
||||
offset++;
|
||||
}
|
||||
if(length > 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;
|
||||
}
|
||||
282
public/rom-patcher-js/modules/RomPatcher.format.ips.js
Normal file
282
public/rom-patcher-js/modules/RomPatcher.format.ips.js
Normal file
@@ -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; i<this.records.length; i++){
|
||||
if(this.records[i].type===IPS_RECORD_RLE)
|
||||
nRLERecords++;
|
||||
else
|
||||
nSimpleRecords++;
|
||||
}
|
||||
var s='Simple records: '+nSimpleRecords;
|
||||
s+='\nRLE records: '+nRLERecords;
|
||||
s+='\nTotal records: '+this.records.length;
|
||||
if(this.truncate && !this.EBPmetadata)
|
||||
s+='\nTruncate at: 0x'+this.truncate.toString(16);
|
||||
else if(this.EBPmetadata)
|
||||
s+='\nEBP Metadata: '+JSON.stringify(this.EBPmetadata);
|
||||
return s
|
||||
}
|
||||
IPS.prototype.export=function(fileName){
|
||||
var patchFileSize=5; //PATCH string
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
if(this.records[i].type===IPS_RECORD_RLE)
|
||||
patchFileSize+=(3+2+2+1); //offset+0x0000+length+RLE byte to be written
|
||||
else
|
||||
patchFileSize+=(3+2+this.records[i].data.length); //offset+length+data
|
||||
}
|
||||
patchFileSize+=3; //EOF string
|
||||
if(this.truncate && !this.EBPmetadata)
|
||||
patchFileSize+=3; //truncate
|
||||
else if(this.EBPmetadata)
|
||||
patchFileSize+=JSON.stringify(this.EBPmetadata).length;
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.fileName=fileName+(this.EBPmetadata? '.ebp' : '.ips');
|
||||
tempFile.writeString(IPS_MAGIC);
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
var rec=this.records[i];
|
||||
tempFile.writeU24(rec.offset);
|
||||
if(rec.type===IPS_RECORD_RLE){
|
||||
tempFile.writeU16(0x0000);
|
||||
tempFile.writeU16(rec.length);
|
||||
tempFile.writeU8(rec.byte);
|
||||
}else{
|
||||
tempFile.writeU16(rec.data.length);
|
||||
tempFile.writeBytes(rec.data);
|
||||
}
|
||||
}
|
||||
|
||||
tempFile.writeString('EOF');
|
||||
if(this.truncate && !this.EBPmetadata)
|
||||
tempFile.writeU24(this.truncate);
|
||||
else if(this.EBPmetadata)
|
||||
tempFile.writeString(JSON.stringify(this.EBPmetadata));
|
||||
|
||||
return tempFile
|
||||
}
|
||||
IPS.prototype.apply=function(romFile){
|
||||
if(this.truncate && !this.EBPmetadata){
|
||||
if(this.truncate>romFile.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; i<this.records.length; i++){
|
||||
var rec=this.records[i];
|
||||
if(rec.type===IPS_RECORD_RLE){
|
||||
if(rec.offset+rec.length>newFileSize){
|
||||
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; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
if(this.records[i].type===IPS_RECORD_RLE){
|
||||
for(var j=0; j<this.records[i].length; j++)
|
||||
tempFile.writeU8(this.records[i].byte);
|
||||
}else{
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
IPS.MAGIC=IPS_MAGIC;
|
||||
|
||||
|
||||
IPS.fromFile=function(file){
|
||||
var patchFile=new IPS();
|
||||
file.seek(5);
|
||||
|
||||
while(!file.isEOF()){
|
||||
var offset=file.readU24();
|
||||
|
||||
if(offset===0x454f46){ /* EOF */
|
||||
if(file.isEOF()){
|
||||
break;
|
||||
}else if((file.offset+3)===file.fileSize){
|
||||
patchFile.truncate=file.readU24();
|
||||
break;
|
||||
}else if (file.readU8()==='{'.charCodeAt(0)) {
|
||||
file.skip(-1);
|
||||
patchFile.setEBPMetadata(JSON.parse(file.readString(file.fileSize-file.offset)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var length=file.readU16();
|
||||
|
||||
if(length===IPS_RECORD_RLE){
|
||||
patchFile.addRLERecord(offset, file.readU16(), file.readU8());
|
||||
}else{
|
||||
patchFile.addSimpleRecord(offset, file.readBytes(length));
|
||||
}
|
||||
}
|
||||
return patchFile;
|
||||
}
|
||||
|
||||
|
||||
IPS.buildFromRoms=function(original, modified, asEBP=false){
|
||||
var patch=new IPS();
|
||||
|
||||
if(!asEBP && modified.fileSize<original.fileSize){
|
||||
patch.truncate=modified.fileSize;
|
||||
}else if(asEBP){
|
||||
patch.setEBPMetadata(typeof asEBP==='object'? asEBP : {
|
||||
'Author':'Unknown',
|
||||
'Title':'Untitled',
|
||||
'Description':'No description',
|
||||
});
|
||||
}
|
||||
|
||||
//solucion: guardar startOffset y endOffset (ir mirando de 6 en 6 hacia atrás)
|
||||
var previousRecord={type:0xdeadbeef,startOffset:0,length:0};
|
||||
while(!modified.isEOF()){
|
||||
var b1=original.isEOF()?0x00:original.readU8();
|
||||
var b2=modified.readU8();
|
||||
|
||||
if(b1!==b2){
|
||||
var RLEmode=true;
|
||||
var differentData=[];
|
||||
var startOffset=modified.offset-1;
|
||||
|
||||
while(b1!==b2 && differentData.length<0xffff){
|
||||
differentData.push(b2);
|
||||
if(b2!==differentData[0])
|
||||
RLEmode=false;
|
||||
|
||||
if(modified.isEOF() || differentData.length===0xffff)
|
||||
break;
|
||||
|
||||
b1=original.isEOF()?0x00:original.readU8();
|
||||
b2=modified.readU8();
|
||||
}
|
||||
|
||||
|
||||
//check if this record is near the previous one
|
||||
var distance=startOffset-(previousRecord.offset+previousRecord.length);
|
||||
if(
|
||||
previousRecord.type===IPS_RECORD_SIMPLE &&
|
||||
distance<6 && (previousRecord.length+distance+differentData.length)<0xffff
|
||||
){
|
||||
if(RLEmode && differentData.length>6){
|
||||
// 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(lastOffset<modified.fileSize){
|
||||
patch.addSimpleRecord(modified.fileSize-1, [0x00]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return patch
|
||||
}
|
||||
97
public/rom-patcher-js/modules/RomPatcher.format.pmsr.js
Normal file
97
public/rom-patcher-js/modules/RomPatcher.format.pmsr.js
Normal file
@@ -0,0 +1,97 @@
|
||||
/* PMSR (Paper Mario Star Rod) module for Rom Patcher JS v20240721 - Marc Robledo 2020-2024 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: http://origami64.net/attachment.php?aid=790 (dead link) */
|
||||
|
||||
const PMSR_MAGIC='PMSR';
|
||||
const YAY0_MAGIC='Yay0';
|
||||
const PAPER_MARIO_USA10_CRC32=0xa7f5cd7e;
|
||||
const PAPER_MARIO_USA10_FILE_SIZE=41943040;
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = PMSR;
|
||||
}
|
||||
|
||||
function PMSR(){
|
||||
this.targetSize=0;
|
||||
this.records=[];
|
||||
}
|
||||
PMSR.prototype.addRecord=function(offset, data){
|
||||
this.records.push({offset:offset, data:data})
|
||||
}
|
||||
PMSR.prototype.toString=function(){
|
||||
var s='Star Rod patch';
|
||||
s+='\nTarget file size: '+this.targetSize;
|
||||
s+='\n#Records: '+this.records.length;
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
PMSR.prototype.validateSource=function(romFile){
|
||||
return romFile.fileSize===PAPER_MARIO_USA10_FILE_SIZE && romFile.hashCRC32()===PAPER_MARIO_USA10_CRC32;
|
||||
}
|
||||
PMSR.prototype.getValidationInfo=function(){
|
||||
return {
|
||||
'type':'CRC32',
|
||||
'value':PAPER_MARIO_USA10_CRC32
|
||||
}
|
||||
}
|
||||
PMSR.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
console.log('a');
|
||||
if(this.targetSize===romFile.fileSize){
|
||||
tempFile=romFile.slice(0, romFile.fileSize);
|
||||
}else{
|
||||
tempFile=new BinFile(this.targetSize);
|
||||
romFile.copyTo(tempFile,0);
|
||||
}
|
||||
|
||||
console.log('b');
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
PMSR.MAGIC=PMSR_MAGIC;
|
||||
|
||||
PMSR.fromFile=function(file){
|
||||
var patch=new PMSR();
|
||||
|
||||
/*file.seek(0);
|
||||
if(file.readString(YAY0_MAGIC.length)===YAY0_MAGIC){
|
||||
file=PMSR.YAY0_decode(file);
|
||||
}*/
|
||||
|
||||
patch.targetSize=PAPER_MARIO_USA10_FILE_SIZE;
|
||||
|
||||
file.seek(4);
|
||||
var nRecords=file.readU32();
|
||||
|
||||
for(var i=0; i<nRecords; i++){
|
||||
var offset=file.readU32();
|
||||
var length=file.readU32();
|
||||
patch.addRecord(offset, file.readBytes(length));
|
||||
|
||||
if((offset+length)>patch.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 */
|
||||
}
|
||||
269
public/rom-patcher-js/modules/RomPatcher.format.ppf.js
Normal file
269
public/rom-patcher-js/modules/RomPatcher.format.ppf.js
Normal file
@@ -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<this.records.length; i++){
|
||||
patchFileSize+=4+1+this.records[i].data.length;
|
||||
if(this.version===3)
|
||||
patchFileSize+=4; //offsets are u64
|
||||
}
|
||||
|
||||
if(this.version===3 || this.version===2){
|
||||
patchFileSize+=4;
|
||||
}
|
||||
if(this.blockCheck){
|
||||
patchFileSize+=1024;
|
||||
}
|
||||
if(this.fileIdDiz){
|
||||
patchFileSize+=18+this.fileIdDiz.length+16+4;
|
||||
}
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.fileName=fileName+'.ppf';
|
||||
tempFile.writeString(PPF_MAGIC);
|
||||
tempFile.writeString((this.version*10).toString());
|
||||
tempFile.writeU8(parseInt(this.version)-1);
|
||||
tempFile.writeString(this.description, 50);
|
||||
|
||||
|
||||
if(this.version===3){
|
||||
tempFile.writeU8(this.imageType);
|
||||
tempFile.writeU8(this.blockCheck?0x01:0x00);
|
||||
tempFile.writeU8(this.undoData?0x01:0x00);
|
||||
tempFile.writeU8(0x00); //dummy
|
||||
|
||||
}else if(this.version===2){
|
||||
tempFile.writeU32(this.inputFileSize);
|
||||
}
|
||||
|
||||
if(this.blockCheck){
|
||||
tempFile.writeBytes(this.blockCheck);
|
||||
}
|
||||
|
||||
|
||||
|
||||
tempFile.littleEndian=true;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.writeU32(this.records[i].offset & 0xffffffff);
|
||||
|
||||
if(this.version===3){
|
||||
var offset2=this.records[i].offset;
|
||||
for(var j=0; j<32; j++)
|
||||
offset2=parseInt((offset2/2)>>>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; i<this.records.length; i++){
|
||||
if(this.records[i].offset+this.records[i].data.length>newFileSize)
|
||||
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; i<originalBytes.length && !foundDifferences; i++){
|
||||
if(originalBytes[i]!==this.records[0].data[i]){
|
||||
foundDifferences=true;
|
||||
}
|
||||
}
|
||||
if(!foundDifferences){
|
||||
undoingData=true;
|
||||
}
|
||||
}
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.seek(this.records[i].offset);
|
||||
|
||||
if(undoingData){
|
||||
tempFile.writeBytes(this.records[i].undoData);
|
||||
}else{
|
||||
tempFile.writeBytes(this.records[i].data);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
PPF.MAGIC=PPF_MAGIC;
|
||||
|
||||
|
||||
|
||||
PPF.fromFile=function(patchFile){
|
||||
var patch=new PPF();
|
||||
|
||||
|
||||
patchFile.seek(3);
|
||||
var version1=parseInt(patchFile.readString(2))/10;
|
||||
var version2=patchFile.readU8()+1;
|
||||
if(version1!==version2 || version1>3){
|
||||
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<modified.fileSize){
|
||||
modified.seek(modified.fileSize-1);
|
||||
if(modified.readU8()===0x00)
|
||||
patch.addRecord(modified.fileSize-1, [0x00]);
|
||||
}
|
||||
|
||||
return patch
|
||||
}
|
||||
396
public/rom-patcher-js/modules/RomPatcher.format.rup.js
Normal file
396
public/rom-patcher-js/modules/RomPatcher.format.rup.js
Normal file
@@ -0,0 +1,396 @@
|
||||
/* RUP module for Rom Patcher JS v20250430 - Marc Robledo 2018-2025 - http://www.marcrobledo.com/license */
|
||||
/* File format specification: http://www.romhacking.net/documents/288/ */
|
||||
|
||||
const RUP_MAGIC='NINJA2';
|
||||
const RUP_COMMAND_END=0x00;
|
||||
const RUP_COMMAND_OPEN_NEW_FILE=0x01;
|
||||
const RUP_COMMAND_XOR_RECORD=0x02;
|
||||
const RUP_ROM_TYPES=['raw','nes','fds','snes','n64','gb','sms','mega','pce','lynx'];
|
||||
if(typeof module !== "undefined" && module.exports){
|
||||
module.exports = RUP;
|
||||
}
|
||||
|
||||
function RUP(){
|
||||
this.author='';
|
||||
this.version='';
|
||||
this.title='';
|
||||
this.genre='';
|
||||
this.language='';
|
||||
this.date='';
|
||||
this.web='';
|
||||
this.description='';
|
||||
this.files=[];
|
||||
}
|
||||
RUP.prototype.toString=function(){
|
||||
var s='Author: '+this.author;
|
||||
s+='\nVersion: '+this.version;
|
||||
s+='\nTitle: '+this.title;
|
||||
s+='\nGenre: '+this.genre;
|
||||
s+='\nLanguage: '+this.language;
|
||||
s+='\nDate: '+this.date;
|
||||
s+='\nWeb: '+this.web;
|
||||
s+='\nDescription: '+this.description;
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
var file=this.files[i];
|
||||
s+='\n---------------';
|
||||
s+='\nFile '+i+':';
|
||||
s+='\nFile name: '+file.fileName;
|
||||
s+='\nRom type: '+RUP_ROM_TYPES[file.romType];
|
||||
s+='\nSource file size: '+file.sourceFileSize;
|
||||
s+='\nTarget file size: '+file.targetFileSize;
|
||||
s+='\nSource MD5: '+file.sourceMD5;
|
||||
s+='\nTarget MD5: '+file.targetMD5;
|
||||
if(file.overflowMode==='A'){
|
||||
s+='\nOverflow mode: Append ' + file.overflowData.length + ' bytes';
|
||||
}else if(file.overflowMode==='M'){
|
||||
s+='\nOverflow mode: Minify ' + file.overflowData.length + ' bytes';
|
||||
}
|
||||
s+='\n#records: '+file.records.length;
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
RUP.prototype.validateSource=function(romFile,headerSize){
|
||||
var md5string=romFile.hashMD5(headerSize);
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
if(this.files[i].sourceMD5===md5string || this.files[i].targetMD5===md5string){
|
||||
return {
|
||||
file:this.files[i],
|
||||
undo:this.files[i].targetMD5===md5string
|
||||
};
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
RUP.prototype.getValidationInfo=function(){
|
||||
var values=[];
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
values.push(this.files[i].sourceMD5);
|
||||
}
|
||||
return {
|
||||
'type':'MD5',
|
||||
'value':values
|
||||
};
|
||||
}
|
||||
RUP.prototype.getDescription=function(){
|
||||
return this.description? this.description : null;
|
||||
}
|
||||
RUP.prototype.apply=function(romFile, validate){
|
||||
var validFile;
|
||||
if(validate){
|
||||
validFile=this.validateSource(romFile);
|
||||
|
||||
if(!validFile)
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}else{
|
||||
validFile={
|
||||
file:this.files[0],
|
||||
undo:this.files[0].targetMD5===romFile.hashMD5()
|
||||
};
|
||||
}
|
||||
|
||||
var undo=validFile.undo;
|
||||
var patch=validFile.file;
|
||||
|
||||
tempFile=new BinFile(!undo? patch.targetFileSize : patch.sourceFileSize);
|
||||
/* copy original file */
|
||||
romFile.copyTo(tempFile, 0);
|
||||
|
||||
|
||||
for(var i=0; i<patch.records.length; i++){
|
||||
var offset=patch.records[i].offset;
|
||||
romFile.seek(offset);
|
||||
tempFile.seek(offset);
|
||||
for(var j=0; j<patch.records[i].xor.length; j++){
|
||||
tempFile.writeU8(
|
||||
(romFile.isEOF()?0x00:romFile.readU8()) ^ patch.records[i].xor[j]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* add overflow data if needed */
|
||||
if(patch.overflowMode==='A' && !undo){ /* append */
|
||||
tempFile.seek(patch.sourceFileSize);
|
||||
tempFile.writeBytes(patch.overflowData.map((byte) => 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.length<nBytes*2)
|
||||
hexString='0'+hexString;
|
||||
return hexString
|
||||
};
|
||||
RUP.fromFile=function(file){
|
||||
var patch=new RUP();
|
||||
file.readVLV=RUP_readVLV;
|
||||
|
||||
file.seek(RUP_MAGIC.length);
|
||||
|
||||
patch.textEncoding=file.readU8();
|
||||
patch.author=file.readString(84);
|
||||
patch.version=file.readString(11);
|
||||
patch.title=file.readString(256);
|
||||
patch.genre=file.readString(48);
|
||||
patch.language=file.readString(48);
|
||||
patch.date=file.readString(8);
|
||||
patch.web=file.readString(512);
|
||||
patch.description=file.readString(1074).replace(/\\n/g,'\n');
|
||||
|
||||
|
||||
file.seek(0x800);
|
||||
var nextFile;
|
||||
while(!file.isEOF()){
|
||||
var command=file.readU8();
|
||||
|
||||
if(command===RUP_COMMAND_OPEN_NEW_FILE){
|
||||
if(nextFile)
|
||||
patch.files.push(nextFile)
|
||||
|
||||
nextFile={
|
||||
records:[]
|
||||
};
|
||||
|
||||
|
||||
nextFile.fileName=file.readString(file.readVLV());
|
||||
nextFile.romType=file.readU8();
|
||||
nextFile.sourceFileSize=file.readVLV();
|
||||
nextFile.targetFileSize=file.readVLV();
|
||||
|
||||
nextFile.sourceMD5='';
|
||||
for(var i=0; i<16; i++)
|
||||
nextFile.sourceMD5+=RUP.padZeroes(file.readU8(),1);
|
||||
|
||||
nextFile.targetMD5='';
|
||||
for(var i=0; i<16; i++)
|
||||
nextFile.targetMD5+=RUP.padZeroes(file.readU8(),1);
|
||||
|
||||
|
||||
|
||||
if(nextFile.sourceFileSize!==nextFile.targetFileSize){
|
||||
nextFile.overflowMode=file.readString(1); // 'M' (source>target) or 'A' (source<target)
|
||||
if(nextFile.overflowMode!=='M' && nextFile.overflowMode!=='A')
|
||||
throw new Error('RUP: invalid overflow mode');
|
||||
nextFile.overflowData=file.readBytes(file.readVLV());
|
||||
}
|
||||
|
||||
}else if(command===RUP_COMMAND_XOR_RECORD){
|
||||
nextFile.records.push({
|
||||
offset:file.readVLV(),
|
||||
xor:file.readBytes(file.readVLV())
|
||||
});
|
||||
}else if(command===RUP_COMMAND_END){
|
||||
if(nextFile)
|
||||
patch.files.push(nextFile);
|
||||
break;
|
||||
}else{
|
||||
throw new Error('invalid RUP command');
|
||||
}
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
|
||||
|
||||
function RUP_readVLV(){
|
||||
var nBytes=this.readU8();
|
||||
var data=0;
|
||||
for(var i=0; i<nBytes; i++){
|
||||
data+=this.readU8() << i*8;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
function RUP_writeVLV(data){
|
||||
var len=RUP_getVLVLen(data)-1;
|
||||
|
||||
this.writeU8(len);
|
||||
|
||||
while(data){
|
||||
this.writeU8(data & 0xff);
|
||||
data>>=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; i<this.files.length; i++){
|
||||
var file=this.files[i];
|
||||
patchFileSize++; //command 0x01
|
||||
|
||||
patchFileSize+=RUP_getVLVLen(file.fileName.length);
|
||||
patchFileSize+=file.fileName.length;
|
||||
patchFileSize++; //rom type
|
||||
patchFileSize+=RUP_getVLVLen(file.sourceFileSize);
|
||||
patchFileSize+=RUP_getVLVLen(file.targetFileSize);
|
||||
patchFileSize+=32; //MD5s
|
||||
|
||||
if(file.sourceFileSize!==file.targetFileSize){
|
||||
patchFileSize++; // M or A
|
||||
patchFileSize+=RUP_getVLVLen(file.overflowData.length);
|
||||
patchFileSize+=file.overflowData.length;
|
||||
}
|
||||
for(var j=0; j<file.records.length; j++){
|
||||
patchFileSize++; //command 0x01
|
||||
patchFileSize+=RUP_getVLVLen(file.records[j].offset);
|
||||
patchFileSize+=RUP_getVLVLen(file.records[j].xor.length);
|
||||
patchFileSize+=file.records[j].xor.length;
|
||||
}
|
||||
}
|
||||
patchFileSize++; //command 0x00
|
||||
|
||||
|
||||
var patchFile=new BinFile(patchFileSize);
|
||||
patchFile.fileName=fileName+'.rup';
|
||||
patchFile.writeVLV=RUP_writeVLV;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
patchFile.writeString(RUP_MAGIC);
|
||||
patchFile.writeU8(this.textEncoding);
|
||||
patchFile.writeString(this.author, 84);
|
||||
patchFile.writeString(this.version, 11);
|
||||
patchFile.writeString(this.title, 256);
|
||||
patchFile.writeString(this.genre, 48);
|
||||
patchFile.writeString(this.language, 48);
|
||||
patchFile.writeString(this.date, 8);
|
||||
patchFile.writeString(this.web, 512);
|
||||
patchFile.writeString(this.description.replace(/\n/g,'\\n'), 1074);
|
||||
|
||||
for(var i=0; i<this.files.length; i++){
|
||||
var file=this.files[i];
|
||||
patchFile.writeU8(RUP_COMMAND_OPEN_NEW_FILE);
|
||||
|
||||
patchFile.writeVLV(file.fileName);
|
||||
patchFile.writeU8(file.romType);
|
||||
patchFile.writeVLV(file.sourceFileSize);
|
||||
patchFile.writeVLV(file.targetFileSize);
|
||||
|
||||
for(var j=0; j<16; j++)
|
||||
patchFile.writeU8(parseInt(file.sourceMD5.substr(j*2,2), 16));
|
||||
for(var j=0; j<16; j++)
|
||||
patchFile.writeU8(parseInt(file.targetMD5.substr(j*2,2), 16));
|
||||
|
||||
|
||||
|
||||
if(file.sourceFileSize!==file.targetFileSize){
|
||||
patchFile.writeString(file.sourceFileSize>file.targetFileSize?'M':'A');
|
||||
patchFile.writeVLV(file.overflowData.length);
|
||||
patchFile.writeBytes(file.overflowData);
|
||||
}
|
||||
|
||||
for(var j=0; j<file.records.length; j++){
|
||||
patchFile.writeU8(RUP_COMMAND_XOR_RECORD);
|
||||
|
||||
patchFile.writeVLV(file.records[j].offset);
|
||||
patchFile.writeVLV(file.records[j].xor.length);
|
||||
patchFile.writeBytes(file.records[j].xor);
|
||||
}
|
||||
}
|
||||
|
||||
patchFile.writeU8(RUP_COMMAND_END);
|
||||
|
||||
|
||||
|
||||
return patchFile;
|
||||
}
|
||||
|
||||
|
||||
|
||||
RUP.buildFromRoms=function(original, modified, description){
|
||||
var patch=new RUP();
|
||||
|
||||
var today=new Date();
|
||||
patch.date=(today.getYear()+1900)+RUP.padZeroes(today.getMonth()+1, 1)+RUP.padZeroes(today.getDate(), 1);
|
||||
if(description)
|
||||
patch.description=description;
|
||||
|
||||
var file={
|
||||
fileName:'',
|
||||
romType:0,
|
||||
sourceFileSize:original.fileSize,
|
||||
targetFileSize:modified.fileSize,
|
||||
sourceMD5:original.hashMD5(),
|
||||
targetMD5:modified.hashMD5(),
|
||||
overflowMode:null,
|
||||
overflowData:[],
|
||||
records:[]
|
||||
};
|
||||
|
||||
if(file.sourceFileSize<file.targetFileSize){
|
||||
modified.seek(file.sourceFileSize);
|
||||
file.overflowMode='A';
|
||||
file.overflowData=modified.readBytes(file.targetFileSize-file.sourceFileSize).map((byte) => 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
|
||||
}
|
||||
224
public/rom-patcher-js/modules/RomPatcher.format.ups.js
Normal file
224
public/rom-patcher-js/modules/RomPatcher.format.ups.js
Normal file
@@ -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<this.records.length; i++){
|
||||
patchFileSize+=UPS_getVLVLength(this.records[i].offset);
|
||||
patchFileSize+=this.records[i].XORdata.length+1;
|
||||
}
|
||||
patchFileSize+=12; //input/output/patch checksums
|
||||
|
||||
tempFile=new BinFile(patchFileSize);
|
||||
tempFile.writeVLV=UPS_writeVLV;
|
||||
tempFile.fileName=fileName+'.ups';
|
||||
tempFile.writeString(UPS_MAGIC);
|
||||
|
||||
tempFile.writeVLV(this.sizeInput);
|
||||
tempFile.writeVLV(this.sizeOutput);
|
||||
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
tempFile.writeVLV(this.records[i].offset);
|
||||
tempFile.writeBytes(this.records[i].XORdata);
|
||||
tempFile.writeU8(0x00);
|
||||
}
|
||||
tempFile.littleEndian=true;
|
||||
tempFile.writeU32(this.checksumInput);
|
||||
tempFile.writeU32(this.checksumOutput);
|
||||
tempFile.writeU32(tempFile.hashCRC32(0, tempFile.fileSize - 4));
|
||||
|
||||
return tempFile
|
||||
}
|
||||
UPS.prototype.validateSource=function(romFile,headerSize){return romFile.hashCRC32(headerSize)===this.checksumInput}
|
||||
UPS.prototype.getValidationInfo=function(){
|
||||
return {
|
||||
'type':'CRC32',
|
||||
'value':this.checksumInput
|
||||
};
|
||||
}
|
||||
UPS.prototype.apply=function(romFile, validate){
|
||||
if(validate && !this.validateSource(romFile)){
|
||||
throw new Error('Source ROM checksum mismatch');
|
||||
}
|
||||
|
||||
/* fix the glitch that cut the end of the file if it's larger than the changed file patch was originally created with */
|
||||
/* more info: https://github.com/marcrobledo/RomPatcher.js/pull/40#issuecomment-1069087423 */
|
||||
sizeOutput = this.sizeOutput;
|
||||
sizeInput = this.sizeInput;
|
||||
if(!validate && sizeInput < romFile.fileSize){
|
||||
sizeInput = romFile.fileSize;
|
||||
if(sizeOutput < sizeInput){
|
||||
sizeOutput = sizeInput;
|
||||
}
|
||||
}
|
||||
|
||||
/* copy original file */
|
||||
tempFile=new BinFile(sizeOutput);
|
||||
romFile.copyTo(tempFile, 0, sizeInput);
|
||||
|
||||
romFile.seek(0);
|
||||
|
||||
|
||||
var nextOffset=0;
|
||||
for(var i=0; i<this.records.length; i++){
|
||||
var record=this.records[i];
|
||||
tempFile.skip(record.offset);
|
||||
romFile.skip(record.offset);
|
||||
|
||||
for(var j=0; j<record.XORdata.length; j++){
|
||||
tempFile.writeU8((romFile.isEOF()?0x00:romFile.readU8()) ^ record.XORdata[j]);
|
||||
}
|
||||
tempFile.skip(1);
|
||||
romFile.skip(1);
|
||||
}
|
||||
|
||||
if(validate && tempFile.hashCRC32()!==this.checksumOutput){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
UPS.MAGIC=UPS_MAGIC;
|
||||
|
||||
|
||||
/* encode/decode variable length values, used by UPS file structure */
|
||||
function UPS_writeVLV(data){
|
||||
while(1){
|
||||
var x=data & 0x7f;
|
||||
data=data>>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
|
||||
}
|
||||
382
public/rom-patcher-js/modules/RomPatcher.format.vcdiff.js
Normal file
382
public/rom-patcher-js/modules/RomPatcher.format.vcdiff.js
Normal file
@@ -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.offset<addressesStreamEndOffset){
|
||||
/*
|
||||
var instructionIndex=instructionsStream.readS8();
|
||||
if(instructionIndex===-1){
|
||||
break;
|
||||
}
|
||||
*/
|
||||
var instructionIndex = instructionsStream.readU8();
|
||||
|
||||
|
||||
for(var i=0; i<2; i++){
|
||||
var instruction=codeTable[instructionIndex][i];
|
||||
var size=instruction.size;
|
||||
|
||||
if(size===0 && instruction.type!==VCD_NOOP){
|
||||
size=instructionsStream.read7BitEncodedInt()
|
||||
}
|
||||
|
||||
if(instruction.type===VCD_NOOP){
|
||||
continue;
|
||||
|
||||
}else if(instruction.type===VCD_ADD){
|
||||
addRunDataStream.copyToFile2(tempFile, addRunDataIndex+targetWindowPosition, size);
|
||||
addRunDataIndex += size;
|
||||
|
||||
}else if(instruction.type===VCD_COPY){
|
||||
var addr = cache.decodeAddress(addRunDataIndex+winHeader.sourceLength, instruction.mode);
|
||||
var absAddr = 0;
|
||||
|
||||
// source segment and target segment are treated as if they're concatenated
|
||||
var sourceData = null;
|
||||
if(addr < winHeader.sourceLength){
|
||||
absAddr = winHeader.sourcePosition + addr;
|
||||
if(winHeader.indicator & VCD_SOURCE){
|
||||
sourceData = romFile;
|
||||
}else if(winHeader.indicator & VCD_TARGET){
|
||||
sourceData = tempFile;
|
||||
}
|
||||
}else{
|
||||
absAddr = targetWindowPosition + (addr - winHeader.sourceLength);
|
||||
sourceData = tempFile;
|
||||
}
|
||||
|
||||
while(size--){
|
||||
tempFile._u8array[targetWindowPosition + addRunDataIndex++]=sourceData._u8array[absAddr++];
|
||||
//targetU8[targetWindowPosition + targetWindowOffs++] = copySourceU8[absAddr++];
|
||||
}
|
||||
//to-do: test
|
||||
//sourceData.copyToFile2(tempFile, absAddr, size, targetWindowPosition + addRunDataIndex);
|
||||
//addRunDataIndex += size;
|
||||
}else if(instruction.type===VCD_RUN){
|
||||
var runByte = addRunDataStream.readU8();
|
||||
var offset = targetWindowPosition + addRunDataIndex;
|
||||
for(var j=0; j<size; j++){
|
||||
tempFile._u8array[offset+j]=runByte;
|
||||
}
|
||||
|
||||
addRunDataIndex += size;
|
||||
}else{
|
||||
throw new Error('invalid instruction type found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(validate && winHeader.adler32 && (winHeader.adler32 !== adler32(tempFile, targetWindowPosition, winHeader.targetWindowLength))){
|
||||
throw new Error('Target ROM checksum mismatch');
|
||||
}
|
||||
|
||||
parser.skip(winHeader.addRunDataLength + winHeader.addressesLength + winHeader.instructionsLength);
|
||||
targetWindowPosition += winHeader.targetWindowLength;
|
||||
}
|
||||
|
||||
//console.log((performance.now()-t0)/1000);
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
VCDIFF.MAGIC=VCDIFF_MAGIC;
|
||||
|
||||
VCDIFF.fromFile=function(file){
|
||||
return new VCDIFF(file);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function VCDIFF_Parser(binFile, offset)
|
||||
{
|
||||
this.fileSize=binFile.fileSize;
|
||||
this._u8array=binFile._u8array;
|
||||
this.offset=offset || 0;
|
||||
|
||||
/* reimplement readU8, readU32 and skip from BinFile */
|
||||
/* in web implementation, there are no guarantees BinFile will be dynamically loaded before this one */
|
||||
/* so we cannot rely on cloning BinFile.prototype */
|
||||
this.readU8 = binFile.readU8;
|
||||
this.readU32 = binFile.readU32;
|
||||
this.skip = binFile.skip;
|
||||
this.isEOF = binFile.isEOF;
|
||||
this.seek = binFile.seek;
|
||||
}
|
||||
VCDIFF_Parser.prototype.read7BitEncodedInt=function(){
|
||||
var num=0, bits = 0;
|
||||
|
||||
do {
|
||||
bits = this.readU8();
|
||||
num = (num << 7) + (bits & 0x7f);
|
||||
} while(bits & 0x80);
|
||||
|
||||
return num;
|
||||
}
|
||||
VCDIFF_Parser.prototype.decodeWindowHeader=function(){
|
||||
var windowHeader={
|
||||
indicator:this.readU8(),
|
||||
sourceLength:0,
|
||||
sourcePosition:0,
|
||||
adler32:false
|
||||
};
|
||||
|
||||
|
||||
if(windowHeader.indicator & (VCD_SOURCE | VCD_TARGET)){
|
||||
windowHeader.sourceLength = this.read7BitEncodedInt();
|
||||
windowHeader.sourcePosition = this.read7BitEncodedInt();
|
||||
}
|
||||
|
||||
windowHeader.deltaLength = this.read7BitEncodedInt();
|
||||
windowHeader.targetWindowLength = this.read7BitEncodedInt();
|
||||
windowHeader.deltaIndicator = this.readU8(); // secondary compression: 1=VCD_DATACOMP,2=VCD_INSTCOMP,4=VCD_ADDRCOMP
|
||||
if(windowHeader.deltaIndicator!==0){
|
||||
throw new Error('unimplemented windowHeader.deltaIndicator:'+windowHeader.deltaIndicator);
|
||||
}
|
||||
|
||||
windowHeader.addRunDataLength = this.read7BitEncodedInt();
|
||||
windowHeader.instructionsLength = this.read7BitEncodedInt();
|
||||
windowHeader.addressesLength = this.read7BitEncodedInt();
|
||||
|
||||
if(windowHeader.indicator & VCD_ADLER32){
|
||||
windowHeader.adler32 = this.readU32();
|
||||
}
|
||||
|
||||
|
||||
return windowHeader;
|
||||
}
|
||||
|
||||
|
||||
VCDIFF_Parser.prototype.copyToFile2=function(target, targetOffset, len){
|
||||
for(var i=0; i<len; i++){
|
||||
target._u8array[targetOffset+i]=this._u8array[this.offset+i];
|
||||
}
|
||||
//this.file.copyToFile(target, this.offset, len, targetOffset);
|
||||
this.skip(len);
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
// hdrIndicator
|
||||
const VCD_DECOMPRESS = 0x01;
|
||||
const VCD_CODETABLE = 0x02;
|
||||
const VCD_APPHEADER = 0x04; // nonstandard?
|
||||
|
||||
// winIndicator
|
||||
const VCD_SOURCE = 0x01;
|
||||
const VCD_TARGET = 0x02;
|
||||
const VCD_ADLER32 = 0x04;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function VCD_Instruction(instruction, size, mode){
|
||||
this.type=instruction;
|
||||
this.size=size;
|
||||
this.mode=mode;
|
||||
}
|
||||
|
||||
/*
|
||||
build the default code table (used to encode/decode instructions) specified in RFC 3284
|
||||
heavily based on
|
||||
https://github.com/vic-alexiev/TelerikAcademy/blob/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff/CodeTable.cs
|
||||
*/
|
||||
const VCD_NOOP=0;
|
||||
const VCD_ADD=1;
|
||||
const VCD_RUN=2;
|
||||
const VCD_COPY=3;
|
||||
const VCD_DEFAULT_CODE_TABLE=(function(){
|
||||
var entries=[];
|
||||
|
||||
var empty = {type: VCD_NOOP, size: 0, mode: 0};
|
||||
|
||||
// 0
|
||||
entries.push([{type: VCD_RUN, size: 0, mode: 0}, empty]);
|
||||
|
||||
// 1,18
|
||||
for(var size=0; size<18; size++){
|
||||
entries.push([{type: VCD_ADD, size: size, mode: 0}, empty]);
|
||||
}
|
||||
|
||||
// 19,162
|
||||
for(var mode=0; mode<9; mode++){
|
||||
entries.push([{type: VCD_COPY, size: 0, mode: mode}, empty]);
|
||||
|
||||
for(var size=4; size<19; size++){
|
||||
entries.push([{type: VCD_COPY, size: size, mode: mode}, empty]);
|
||||
}
|
||||
}
|
||||
|
||||
// 163,234
|
||||
for(var mode=0; mode<6; mode++){
|
||||
for(var addSize=1; addSize<5; addSize++){
|
||||
for(var copySize=4; copySize<7; copySize++){
|
||||
entries.push([{type: VCD_ADD, size: addSize, mode: 0},
|
||||
{type: VCD_COPY, size: copySize, mode: mode}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 235,246
|
||||
for(var mode=6; mode<9; mode++){
|
||||
for(var addSize=1; addSize<5; addSize++){
|
||||
entries.push([{type: VCD_ADD, size: addSize, mode: 0},
|
||||
{type: VCD_COPY, size: 4, mode: mode}]);
|
||||
}
|
||||
}
|
||||
|
||||
// 247,255
|
||||
for(var mode=0; mode<9; mode++){
|
||||
entries.push([{type: VCD_COPY, size: 4, mode: mode},
|
||||
{type: VCD_ADD, size: 1, mode: 0}]);
|
||||
}
|
||||
|
||||
return entries;
|
||||
})();
|
||||
|
||||
|
||||
|
||||
/*
|
||||
ported from https://github.com/vic-alexiev/TelerikAcademy/tree/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff
|
||||
by Victor Alexiev (https://github.com/vic-alexiev)
|
||||
*/
|
||||
const VCD_MODE_SELF=0;
|
||||
const VCD_MODE_HERE=1;
|
||||
function VCD_AdressCache(nearSize, sameSize){
|
||||
this.nearSize=nearSize;
|
||||
this.sameSize=sameSize;
|
||||
|
||||
this.near=new Array(nearSize);
|
||||
this.same=new Array(sameSize*256);
|
||||
}
|
||||
VCD_AdressCache.prototype.reset=function(addressStream){
|
||||
this.nextNearSlot=0;
|
||||
this.near.fill(0);
|
||||
this.same.fill(0);
|
||||
|
||||
this.addressStream=addressStream;
|
||||
}
|
||||
VCD_AdressCache.prototype.decodeAddress=function(here, mode){
|
||||
var address=0;
|
||||
|
||||
if(mode===VCD_MODE_SELF){
|
||||
address=this.addressStream.read7BitEncodedInt();
|
||||
}else if(mode===VCD_MODE_HERE){
|
||||
address=here-this.addressStream.read7BitEncodedInt();
|
||||
}else if(mode-2<this.nearSize){ //near cache
|
||||
address=this.near[mode-2]+this.addressStream.read7BitEncodedInt();
|
||||
}else{ //same cache
|
||||
var m=mode-(2+this.nearSize);
|
||||
address=this.same[m*256+this.addressStream.readU8()];
|
||||
}
|
||||
|
||||
this.update(address);
|
||||
return address;
|
||||
}
|
||||
VCD_AdressCache.prototype.update=function(address){
|
||||
if(this.nearSize>0){
|
||||
this.near[this.nextNearSlot]=address;
|
||||
this.nextNearSlot=(this.nextNearSlot+1)%this.nearSize;
|
||||
}
|
||||
|
||||
if(this.sameSize>0){
|
||||
this.same[address%(this.sameSize*256)]=address;
|
||||
}
|
||||
}
|
||||
19
public/rom-patcher-js/modules/bz2/LICENSE
Normal file
19
public/rom-patcher-js/modules/bz2/LICENSE
Normal file
@@ -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.
|
||||
1
public/rom-patcher-js/modules/bz2/bz2.js
Normal file
1
public/rom-patcher-js/modules/bz2/bz2.js
Normal file
@@ -0,0 +1 @@
|
||||
/* code has been moved to RomPatcher.format.bdf.js because of incompatibility between webapp, web worker and CLI */
|
||||
28
public/rom-patcher-js/modules/zip.js/LICENSE
Normal file
28
public/rom-patcher-js/modules/zip.js/LICENSE
Normal file
@@ -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.
|
||||
36
public/rom-patcher-js/modules/zip.js/inflate.js
Normal file
36
public/rom-patcher-js/modules/zip.js/inflate.js
Normal file
File diff suppressed because one or more lines are too long
2
public/rom-patcher-js/modules/zip.js/z-worker.js
Normal file
2
public/rom-patcher-js/modules/zip.js/z-worker.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* jshint worker:true */
|
||||
!function(c){"use strict";if(c.zWorkerInitialized)throw new Error("z-worker.js should be run only once");c.zWorkerInitialized=!0,addEventListener("message",function(t){var e,r,c=t.data,n=c.type,s=c.sn,p=o[n];if(p)try{p(c)}catch(t){e={type:n,sn:s,error:(r=t,{message:r.message,stack:r.stack})},postMessage(e)}});var o={importScripts:function(t){t.scripts&&0<t.scripts.length&&importScripts.apply(void 0,t.scripts);postMessage({type:"importScripts"})},newTask:h,append:t,flush:t},f={};function h(t){var e=c[t.codecClass],r=t.sn;if(f[r])throw Error("duplicated sn");f[r]={codec:new e(t.options),crcInput:"input"===t.crcType,crcOutput:"output"===t.crcType,crc:new n},postMessage({type:"newTask",sn:r})}var l=c.performance?c.performance.now.bind(c.performance):Date.now;function t(t){var e=t.sn,r=t.type,c=t.data,n=f[e];!n&&t.codecClass&&(h(t),n=f[e]);var s,p="append"===r,o=l();if(p)try{s=n.codec.append(c,function(t){postMessage({type:"progress",sn:e,loaded:t})})}catch(t){throw delete f[e],t}else delete f[e],s=n.codec.flush();var a=l()-o;o=l(),c&&n.crcInput&&n.crc.append(c),s&&n.crcOutput&&n.crc.append(s);var i=l()-o,u={type:r,sn:e,codecTime:a,crcTime:i},d=[];s&&(u.data=s,d.push(s.buffer)),p||!n.crcInput&&!n.crcOutput||(u.crc=n.crc.get());try{postMessage(u,d)}catch(t){postMessage(u)}}function n(){this.crc=-1}function e(){}n.prototype.append=function(t){for(var e=0|this.crc,r=this.table,c=0,n=0|t.length;c<n;c++)e=e>>>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);
|
||||
28
public/rom-patcher-js/modules/zip.js/zip.min.js
vendored
Normal file
28
public/rom-patcher-js/modules/zip.js/zip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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';
|
||||
|
||||
|
||||
@@ -182,3 +182,9 @@
|
||||
.spin {
|
||||
animation: spin 1s infinite linear;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
160
resources/css/components/drafts.css
Normal file
160
resources/css/components/drafts.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
320
resources/css/components/modcp.css
Normal file
320
resources/css/components/modcp.css
Normal file
@@ -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; }
|
||||
83
resources/css/components/tools.css
Normal file
83
resources/css/components/tools.css
Normal file
@@ -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;
|
||||
}
|
||||
@@ -23,6 +23,18 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vertical-separator {
|
||||
align-items: center;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -41,18 +53,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vertical-separator {
|
||||
align-items: center;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#content {
|
||||
flex-grow: 1;
|
||||
padding: 30px;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
114
resources/js/RomPatcher.js
Normal file
114
resources/js/RomPatcher.js
Normal file
@@ -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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -63,10 +63,10 @@
|
||||
</div>
|
||||
|
||||
<div class="hovercard-actions">
|
||||
<a href="#" class="btn" title="View profile">
|
||||
<a :href="`{{ xfRoute('members') }}/${$store.hovercard.data.user_id}/`" class="btn" title="View profile">
|
||||
<i data-lucide="user" size="14"></i>
|
||||
</a>
|
||||
<a href="#" class="btn" title="Send message">
|
||||
<a :href="`{{ xfRoute('direct-messages/add') }}?to=${$store.hovercard.data.username.replace(' ', '+')}`" class="btn" title="Send message">
|
||||
<i data-lucide="mail" size="14"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
<div class="menu-group">
|
||||
<div class="menu-group-title">{{ $menu['name'] }}</div>
|
||||
@foreach( $menu['items'] as $item )
|
||||
@if( !isset( $item['condition'] ) || $item['condition']() )
|
||||
<a href="{{ isset($item['xf_route']) ? xfRoute($item['xf_route']) : route($item['route']) }}"
|
||||
@class(['menu-item', 'active' => request()->routeIs( $item['route'] ?? '' )]) >
|
||||
<i data-lucide="{{ $item['icon'] }}"></i><span>{{ $item['name'] }}</span>
|
||||
</a>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
6
resources/views/components/modcp-search.blade.php
Normal file
6
resources/views/components/modcp-search.blade.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<form class="search-bar" style="margin-bottom: 15px;">
|
||||
<input type="text" name="{{ $param }}" placeholder="{{ $placeholder }}" value="{{ request($param) }}" autocomplete="off" />
|
||||
<button type="submit" class="search-button">
|
||||
<i data-lucide="search" size="18" color="var(--text2)"></i>
|
||||
</button>
|
||||
</form>
|
||||
@@ -1,6 +1,7 @@
|
||||
<div class="submit-level" x-data="{
|
||||
nsfw: null,
|
||||
state: '{{ old('submit-state', $defaultState) }}',
|
||||
deleteOpen: false,
|
||||
init(){
|
||||
this.$watch('nsfw', (val) => {
|
||||
if( val && this.state === 'published' ) {
|
||||
@@ -9,6 +10,13 @@
|
||||
});
|
||||
}
|
||||
}" x-init="init()">
|
||||
@if($isEdit)
|
||||
<div>
|
||||
<button type="button" class="btn danger" @click="deleteOpen = true; $dispatch('modal:opened')">
|
||||
<i data-lucide="trash-2" size="13"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
@if( section_must_be( [ 'romhacks', 'homebrew' ], $section ) && !$isEdit )
|
||||
<label class="nsfw-label"><input id="nsfw-checkbox" type="checkbox" name="nsfw-entry" x-model="nsfw" style="transform: scale(1.5)"> NSFW</label>
|
||||
@@ -25,4 +33,49 @@
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
@if($isEdit)
|
||||
<template x-teleport="body">
|
||||
<div
|
||||
class="modal-overlay"
|
||||
x-cloak
|
||||
x-show="deleteOpen"
|
||||
x-transition.opacity
|
||||
@click.self="deleteOpen = false"
|
||||
@keydown.escape.window="deleteOpen = false"
|
||||
@modal:opened.window="refreshIcons($el)"
|
||||
>
|
||||
<div class="modal-window" x-show="deleteOpen" x-transition>
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">Delete entry</span>
|
||||
<button type="button" class="modal-close" @click="deleteOpen = false">
|
||||
<i data-lucide="x" size="20"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p style="margin-bottom: 1.5rem; color: var(--text, #333);">
|
||||
Are you sure you want to delete this entry? This action cannot be undone.
|
||||
</p>
|
||||
|
||||
<form action="{{ route('submit.destroy', ['section' => $section, 'entry' => $entry ]) }}" method="POST">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
|
||||
<div class="queue-mod-actions">
|
||||
<button type="button" class="btn" @click="deleteOpen = false">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn danger">
|
||||
<i data-lucide="trash-2" size="14"></i>
|
||||
Confirm deletion
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
<i data-lucide="menu"></i>
|
||||
</button>
|
||||
|
||||
<div class="search-bar">
|
||||
<form class="search-bar" action="{{ route('entries.index') }}">
|
||||
<input type="text" name="s" placeholder="Search" />
|
||||
<button type="submit" class="search-button">
|
||||
<i data-lucide="search" size="18" color="var(--text2)"></i>
|
||||
<input type="text">Search</input>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="topbar-actions">
|
||||
|
||||
@if( !\Auth::guest() && \Auth::user()->is_admin === 1 )
|
||||
@can('is-admin')
|
||||
@php $topbarAdminSeparator = true; @endphp
|
||||
<a href="{{ config('app.forum_url') . '/admin.php' }}" class="btn">
|
||||
<i data-lucide="landmark" size="18"></i>
|
||||
@@ -19,15 +21,15 @@
|
||||
<a href="{{ config('app.url') . '/manage' }}" class="btn">
|
||||
<i data-lucide="shield-cog" size="18"></i>
|
||||
</a>
|
||||
@endif
|
||||
@endcan
|
||||
|
||||
@if( $topbarAdminSeparator )
|
||||
<div class="vertical-separator"></div>
|
||||
@endif
|
||||
|
||||
@if( !\Auth::guest() && \Auth::user()->is_moderator === 1 )
|
||||
@can('is-mod')
|
||||
@php $topbarModSeparator = true; @endphp
|
||||
<a href="#" class="btn">
|
||||
<a href="{{ route('modcp.index') }}" class="btn">
|
||||
<i data-lucide="siren" size="18"></i>
|
||||
</a>
|
||||
<a href="{{ xfRoute('approval-queue') }}" class="btn">
|
||||
@@ -36,7 +38,7 @@
|
||||
<a href="{{ xfRoute('reports') }}" class="btn">
|
||||
<i data-lucide="triangle-alert" size="18"></i>
|
||||
</a>
|
||||
@endif
|
||||
@endcan
|
||||
|
||||
@if( $topbarModSeparator )
|
||||
<div class="vertical-separator"></div>
|
||||
|
||||
54
resources/views/entries/draft_item.blade.php
Normal file
54
resources/views/entries/draft_item.blade.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<div class="drafts-item">
|
||||
<div class="drafts-cover">
|
||||
@if($entry->main_image)
|
||||
<img src="{{ Storage::url($entry->main_image) }}">
|
||||
@else
|
||||
<div class="drafts-cover-placeholder">
|
||||
<i data-lucide="image" size="24"></i>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="drafts-info">
|
||||
<div class="drafts-top">
|
||||
<div>
|
||||
<h3 class="drafts-title">
|
||||
{{ $entry->complete_title }}
|
||||
</h3>
|
||||
<div class="drafts-meta">
|
||||
<span class="badge {{ $entry->type }}">
|
||||
{{ \App\Livewire\Database::ENTRY_TYPES[$entry->type] }}
|
||||
</span>
|
||||
@if( $entry->getRealPlatform() )
|
||||
<span class="badge">{{ $entry->getRealPlatform()->name }}</span>
|
||||
@endif
|
||||
@if( $entry->version )
|
||||
<span class="badge">{{ $entry->version }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="drafts-dates">
|
||||
<span>
|
||||
<i data-lucide="pencil" size="12"></i>
|
||||
Last edited {{ $draft->updated_at->diffForHumans() }}
|
||||
</span>
|
||||
<span>
|
||||
<i data-lucide="calendar" size="12"></i>
|
||||
Created {{ $draft->created_at->format('d M Y') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drafts-actions">
|
||||
<a href="{{ route('submit.edit', ['section' =>$entry->type, 'entry' => $entry]) }}" class="btn primary">
|
||||
<i data-lucide="pen" size="13"></i>
|
||||
Continue editing
|
||||
</a>
|
||||
<a href="{{ route('entries.show', ['section' => $entry->type, 'entry' => $entry ] ) }}" class="btn" target="_blank">
|
||||
<i data-lucide="eye" size="13"></i>
|
||||
Preview
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
25
resources/views/entries/drafts.blade.php
Normal file
25
resources/views/entries/drafts.blade.php
Normal file
@@ -0,0 +1,25 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('page-title', "My Drafts - " . config('app.name'))
|
||||
|
||||
@section('content')
|
||||
<div class="page-title">
|
||||
My Drafts
|
||||
</div>
|
||||
@if($drafts->isEmpty())
|
||||
<div class="drafts-empty">
|
||||
<i data-lucide="pen" size="48"></i>
|
||||
<p>No drafts here</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="drafts-count">
|
||||
<span>{{ $drafts->total() }} draft{{ $drafts->total() > 1 ? 's' : '' }}</span>
|
||||
</div>
|
||||
<div class="drafts-list">
|
||||
@foreach($drafts as $draft)
|
||||
@include('entries.draft_item', ['entry' => $draft])
|
||||
@endforeach
|
||||
</div>
|
||||
{{ $drafts->links() }}
|
||||
@endif
|
||||
@endsection
|
||||
@@ -83,28 +83,28 @@
|
||||
</div>
|
||||
<div class="entry-meta-grid">
|
||||
@if( $entry->game )
|
||||
<x-entry-meta-item label="Game Name" value="{{ $entry->game->name }}" />
|
||||
<x-entry-meta-item label="Game Name" value="{{ $entry->game->name }}" route="{!! databaseRoute( [ 'games' => [ $entry->game->id ], 'platforms' => [ $entry->getRealPlatform()?->id ] ] ) !!}" />
|
||||
@endif
|
||||
@if( $entry->getRealPlatform() )
|
||||
<x-entry-meta-item label="Platform" value="{{ ($entry->getRealPlatform())->name }}" />
|
||||
<x-entry-meta-item label="Platform" value="{{ ($entry->getRealPlatform())->name }}" route="{!! databaseRoute( ['platforms' => [ $entry->getRealPlatform()->id ] ] ) !!}" />
|
||||
@endif
|
||||
@if( $entry->game && $entry->game->genre )
|
||||
<x-entry-meta-item label="Genre" value="{{ $entry->game->genre->name }}" />
|
||||
<x-entry-meta-item label="Genre" value="{{ $entry->game->genre->name }}" route="{!! databaseRoute( ['genres' => [ $entry->game->genre->id ] ]) !!}" />
|
||||
@endif
|
||||
@if( $entry->languages->isNotEmpty() )
|
||||
<x-entry-meta-item label="Language" value="{{ $entry->languages->pluck('name')->implode(', ') }}" route="none" />
|
||||
<x-entry-meta-item label="Language" value="{{ $entry->languages->pluck('name')->implode(', ') }}" route="{!! databaseRoute( [ 'languages' => $entry->languages->pluck('id')->toArray() ]) !!}" />
|
||||
@endif
|
||||
@if( $entry->status_id )
|
||||
<x-entry-meta-item label="Status" value="{{ $entry->status->name }}" />
|
||||
<x-entry-meta-item label="Status" value="{{ $entry->status->name }}" route="{!! databaseRoute( ['statuses' => [ $entry->status->id ] ]) !!}" />
|
||||
@endif
|
||||
@if( $entry->modifications->isNotEmpty() )
|
||||
<x-entry-meta-item label="Type of hack" value="{{ $entry->modifications->pluck('name')->implode(', ') }}" route="{!! databaseRoute( [ 'modifications' => $entry->modifications->pluck('id')->toArray() ] ) !!}" />
|
||||
@endif
|
||||
@if( $entry->version )
|
||||
<x-entry-meta-item label="Version" value="{{ $entry->version }}" route="none" />
|
||||
@endif
|
||||
@if( $entry->release_date )
|
||||
<x-entry-meta-item label="Release Date" value="{{ $entry->release_date }}" />
|
||||
@endif
|
||||
@if( $entry->modifications->isNotEmpty() )
|
||||
<x-entry-meta-item label="Type of hack" value="{{ $entry->modifications->pluck('name')->implode(', ') }}" route="none" />
|
||||
<x-entry-meta-item label="Release Date" value="{{ $entry->release_date->format('Y-m-d') }}" route="none" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="hack-actions" style="display:flex;gap:10px;">
|
||||
@@ -206,7 +206,7 @@
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@if( $entry->staff_credits )
|
||||
@if( $entry->parseStaffCredits() )
|
||||
<x-entry-section-title label="Staff Credits" icon="users-round" />
|
||||
<div class="entry-description">
|
||||
<ul>
|
||||
|
||||
95
resources/views/layouts/modcp.blade.php
Normal file
95
resources/views/layouts/modcp.blade.php
Normal file
@@ -0,0 +1,95 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="modcp-wrapper">
|
||||
<aside class="modcp-sidebar">
|
||||
<div class="modcp-sidebar-header">
|
||||
<i data-lucide="shield" size="16"></i>
|
||||
Mod CP
|
||||
</div>
|
||||
|
||||
<nav class="modcp-nav">
|
||||
|
||||
<div class="modcp-nav-group">
|
||||
<span class="modcp-nav-label">Overview</span>
|
||||
<a href="{{ route('modcp.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.index') ? 'active' : '' }}>
|
||||
<i data-lucide="layout-dashboard" size="15"></i>
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="{{ route('queue.index') }}" class="modcp-nav-item" {{ request()->routeIs('queue.index') ? 'active' : '' }}>
|
||||
<i data-lucide="gavel" size="15"></i>
|
||||
Submissions Queue
|
||||
@if(( $pending = \App\Models\Entry::where('state','pending')->count() ) > 0)
|
||||
<span class="modcp-nav-badge">{{ $pending }}</span>
|
||||
@endif
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="modcp-nav-group">
|
||||
<span class="modcp-nav-label">Content</span>
|
||||
<a href="{{ route('modcp.locked') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.locked') ? 'active' : '' }}>
|
||||
<i data-lucide="lock" size="15"></i>
|
||||
Locked entries
|
||||
</a>
|
||||
@can('is-admin')
|
||||
<a href="{{ route('modcp.draft') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.draft') ? 'active' : '' }}>
|
||||
<i data-lucide="scissors" size="15"></i>
|
||||
Draft entries
|
||||
</a>
|
||||
<a href="{{ route('modcp.hidden') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.hidden') ? 'active' : '' }}>
|
||||
<i data-lucide="eye-off" size="15"></i>
|
||||
Hidden entries
|
||||
</a>
|
||||
<a href="{{ route('modcp.deleted') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.deleted') ? 'active' : '' }}>
|
||||
<i data-lucide="trash-2" size="15"></i>
|
||||
Deleted entries
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="modcp-nav-group">
|
||||
<span class="modcp-nav-label">Resources</span>
|
||||
<a href="{{ route('modcp.games.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.games.*') ? 'active' : '' }}">
|
||||
<i data-lucide="gamepad-2" size="15"></i>
|
||||
Games
|
||||
</a>
|
||||
<a href="{{ route('modcp.languages.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.languages.*') ? 'active' : '' }}">
|
||||
<i data-lucide="languages" size="15"></i>
|
||||
Languages
|
||||
</a>
|
||||
<a href="{{ route('modcp.authors.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.authors.*') ? 'active' : '' }}">
|
||||
<i data-lucide="users" size="15"></i>
|
||||
Authors
|
||||
</a>
|
||||
@can('is-admin')
|
||||
<a href="{{ route('modcp.platforms.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.platforms.*') ? 'active' : '' }}">
|
||||
<i data-lucide="gamepad-directional" size="15"></i>
|
||||
Platforms
|
||||
</a>
|
||||
<a href="{{ route('modcp.genres.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.genres.*') ? 'active' : '' }}">
|
||||
<i data-lucide="box" size="15"></i>
|
||||
Genres
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="modcp-nav-group">
|
||||
<span class="modcp-nav-label">Community</span>
|
||||
<a href="{{ xfRoute('reports') }}" class="modcp-nav-item">
|
||||
<i data-lucide="triangle-alert" size="15"></i>
|
||||
Reports
|
||||
</a>
|
||||
<a href="{{ xfRoute('approval-queue') }}" class="modcp-nav-item">
|
||||
<i data-lucide="message-circle-check" size="15"></i>
|
||||
Approval Queue
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="modcp-content">
|
||||
@yield('modcp-content')
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -37,6 +37,9 @@
|
||||
{{-- Platforms --}}
|
||||
<x-database-filter-without-mode title="Platform" :items="$allPlatforms" model="platforms"/>
|
||||
|
||||
{{-- Genres --}}
|
||||
<x-database-filter-without-mode title="Genre" :items="$allGenres" model="genres"/>
|
||||
|
||||
{{-- Statuses --}}
|
||||
<x-database-filter-without-mode title="Status" :items="$allStatuses" model="statuses"/>
|
||||
|
||||
|
||||
89
resources/views/modcp/authors.blade.php
Normal file
89
resources/views/modcp/authors.blade.php
Normal file
@@ -0,0 +1,89 @@
|
||||
@extends('layouts.modcp')
|
||||
|
||||
@section('modcp-content')
|
||||
|
||||
<div class="modcp-page-title">
|
||||
Authors
|
||||
<span class="modcp-count">{{ $items->total() }}</span>
|
||||
</div>
|
||||
|
||||
<x-mod-c-p-search placeholder="Search an author..."/>
|
||||
|
||||
<div class="modcp-add-form">
|
||||
<form action="{{ route('modcp.authors.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="modcp-add-form-inner">
|
||||
<input type="text" name="name" class="form-input"
|
||||
placeholder="Author name..." required>
|
||||
<div style="width:20%">
|
||||
<livewire:xf-user-selector />
|
||||
</div>
|
||||
<input type="text" name="website" class="form-input"
|
||||
placeholder="Website">
|
||||
<button type="submit" class="btn primary">
|
||||
<i data-lucide="plus" size="14"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modcp-list">
|
||||
@forelse($items as $author)
|
||||
<div class="modcp-list-item" x-data="{ editing: false }">
|
||||
|
||||
<div class="modcp-list-item-info" x-show="!editing">
|
||||
<span class="modcp-list-item-title">{{ $author->name }}</span>
|
||||
<span class="modcp-list-item-meta">
|
||||
<span class="badge">{{ $author->website ?? '—' }}</span>
|
||||
@if(($xfUser = $author->user()) !== null )
|
||||
<span class="badge">
|
||||
<x-xf-username-link :user="$xfUser" />
|
||||
</span>
|
||||
@endif
|
||||
· {{ $author->entries_count }} {{ Str::plural('entry', $author->entries_count) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('modcp.authors.update', $author) }}" method="POST"
|
||||
class="modcp-list-item-edit modcp-list-item-edit--game"
|
||||
x-show="editing" x-cloak>
|
||||
@csrf @method('PATCH')
|
||||
<input type="text" name="name" class="form-input"
|
||||
placeholder="Author name..." value="{{ $author->name }}" required>
|
||||
<div style="width:20%">
|
||||
<livewire:xf-user-selector :initial-user-id="$author->user_id" />
|
||||
</div>
|
||||
<input type="text" name="website" class="form-input"
|
||||
placeholder="Website" value="{{ $author->website }}">
|
||||
<button type="submit" class="btn primary">
|
||||
<i data-lucide="check" size="13"></i>
|
||||
</button>
|
||||
<button type="button" class="btn" @click="editing = false">
|
||||
<i data-lucide="x" size="13"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="modcp-list-item-actions" x-show="!editing">
|
||||
<button type="button" class="btn" @click="editing = true">
|
||||
<i data-lucide="pen" size="13"></i>
|
||||
</button>
|
||||
<form action="{{ route('modcp.authors.destroy', $author) }}" method="POST"
|
||||
style="display:inline"
|
||||
onsubmit="return confirm('Delete {{ addslashes($author->name) }}?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn danger"
|
||||
{{ $author->entries_count > 0 ? 'disabled title=Has entries' : '' }}>
|
||||
<i data-lucide="trash-2" size="13"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@empty
|
||||
<div class="modcp-empty"><p>No authors yet.</p></div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{ $items->links() }}
|
||||
|
||||
@endsection
|
||||
61
resources/views/modcp/deleted.blade.php
Normal file
61
resources/views/modcp/deleted.blade.php
Normal file
@@ -0,0 +1,61 @@
|
||||
@extends('layouts.modcp')
|
||||
|
||||
@section('page-title', 'Deleted entries - ' . config('app.name') )
|
||||
|
||||
@section('modcp-content')
|
||||
|
||||
<div class="modcp-page-title">
|
||||
<i data-lucide="trash-2" size="20"></i>
|
||||
Deleted entries
|
||||
<span class="modcp-count">{{ $entries->total() }}</span>
|
||||
</div>
|
||||
|
||||
@if($entries->isEmpty())
|
||||
<div class="modcp-empty">
|
||||
<i data-lucide="check-circle" size="36"></i>
|
||||
<p>No deleted entries.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="modcp-list">
|
||||
@foreach($entries as $entry)
|
||||
<div class="modcp-list-item modcp-list-item--deleted">
|
||||
<div class="modcp-list-item-cover">
|
||||
@if($entry->main_image)
|
||||
<img src="{{ Storage::url($entry->main_image) }}" alt="">
|
||||
@else
|
||||
<i data-lucide="image" size="20"></i>
|
||||
@endif
|
||||
</div>
|
||||
<div class="modcp-list-item-info">
|
||||
<span class="modcp-list-item-title">{{ $entry->complete_title ?? $entry->title }}</span>
|
||||
<span class="modcp-list-item-meta">
|
||||
<span class="badge {{ $entry->type }}">{{ $entry->type }}</span>
|
||||
@php $daysLeft = max(0, 7 - (int) now()->diffInDays($entry->deleted_at)) @endphp
|
||||
<span style="color: var(--error)">
|
||||
Deleted {{ $entry->deleted_at->diffForHumans() }}
|
||||
@if($daysLeft > 0) · purged in {{ $daysLeft }}d @endif
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="modcp-list-item-actions">
|
||||
<form action="{{ route('modcp.restore', $entry->id) }}" method="POST" style="display:inline">
|
||||
@csrf @method('PATCH')
|
||||
<button type="submit" class="btn success">
|
||||
<i data-lucide="rotate-ccw" size="13"></i> Restore
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ route('modcp.destroy', $entry->id) }}" method="POST" style="display:inline"
|
||||
@submit="if (!confirm('Permanently delete this entry?')) $event.preventDefault()">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn danger">
|
||||
<i data-lucide="trash-2" size="13"></i> Purge
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
{{ $entries->links() }}
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
52
resources/views/modcp/entries.blade.php
Normal file
52
resources/views/modcp/entries.blade.php
Normal file
@@ -0,0 +1,52 @@
|
||||
@extends('layouts.modcp')
|
||||
|
||||
@section('page-title', $pageTitle . ' - ' . config('app.name') )
|
||||
|
||||
@section('modcp-content')
|
||||
<div class="modcp-page-title">
|
||||
{{ $pageTitle }}
|
||||
<span class="modcp-count">{{ $entries->count() }}</span>
|
||||
</div>
|
||||
|
||||
@if($entries->isEmpty())
|
||||
<div class="modcp-empty">
|
||||
<i data-lucide="check-circle" size="36"></i>
|
||||
<p>No {{ $state }} entries.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="modcp-list">
|
||||
@foreach($entries as $entry)
|
||||
<div class="modcp-list-item">
|
||||
<div class="modcp-list-item-cover">
|
||||
@if($entry->main_image)
|
||||
<img src="{{ Storage::url($entry->main_image) }}" alt="">
|
||||
@else
|
||||
<i data-lucide="image" size="20"></i>
|
||||
@endif
|
||||
</div>
|
||||
<div class="modcp-list-item-info">
|
||||
<span class="modcp-list-item-title">{{ $entry->complete_title }}</span>
|
||||
<span class="modcp-list-item-meta">
|
||||
<span class="badge {{ $entry->type }}">{{ \App\Livewire\Database::ENTRY_TYPES[$entry->type] }}</span>
|
||||
@if($entry->getRealPlatform())
|
||||
<span class="badge">{{ $entry->getRealPlatform()->name }}</span>
|
||||
@endif
|
||||
Added {{ $entry->created_at->format('d M Y') }} by<x-xf-username-link :user-id="$entry->user_id" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="modcp-list-item-actions">
|
||||
<a href="{{ route('entries.show', ['section' => $entry->type, 'entry' => $entry]) }}"
|
||||
class="btn" target="_blank">
|
||||
<i data-lucide="eye" size="13"></i> View
|
||||
</a>
|
||||
<a href="{{ route('submit.edit', [$entry->type, $entry->id]) }}"
|
||||
class="btn">
|
||||
<i data-lucide="pen" size="13"></i> Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
{{ $entries->links() }}
|
||||
@endif
|
||||
@endsection
|
||||
103
resources/views/modcp/games.blade.php
Normal file
103
resources/views/modcp/games.blade.php
Normal file
@@ -0,0 +1,103 @@
|
||||
@extends('layouts.modcp')
|
||||
|
||||
@section('modcp-content')
|
||||
|
||||
<div class="modcp-page-title">
|
||||
Games
|
||||
<span class="modcp-count">{{ $items->total() }}</span>
|
||||
</div>
|
||||
|
||||
<x-mod-c-p-search placeholder="Search a game..." />
|
||||
|
||||
<div class="modcp-add-form">
|
||||
<form action="{{ route('modcp.games.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="modcp-add-form-inner">
|
||||
<input type="text" name="name" class="form-input"
|
||||
placeholder="Game name..." required>
|
||||
<select name="platform_id" class="form-select" required style="width: 20%">
|
||||
<option value="" disabled selected>Platform...</option>
|
||||
@foreach($platforms as $platform)
|
||||
<option value="{{ $platform->id }}">{{ $platform->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="genre_id" class="form-select" required style="width: 20%">
|
||||
<option value="" disabled selected>Genre...</option>
|
||||
@foreach($genres as $genre)
|
||||
<option value="{{ $genre->id }}">{{ $genre->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn primary">
|
||||
<i data-lucide="plus" size="14"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modcp-list">
|
||||
@forelse($items as $game)
|
||||
<div class="modcp-list-item" x-data="{ editing: false }">
|
||||
|
||||
<div class="modcp-list-item-info" x-show="!editing">
|
||||
<span class="modcp-list-item-title">{{ $game->name }}</span>
|
||||
<span class="modcp-list-item-meta">
|
||||
<span class="badge">{{ $game->platform->name ?? '—' }}</span>
|
||||
<span class="badge">{{ $game->genre->name ?? '—' }}</span>
|
||||
· {{ $game->entries_count }} {{ Str::plural('entry', $game->entries_count) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('modcp.games.update', $game) }}" method="POST"
|
||||
class="modcp-list-item-edit modcp-list-item-edit--game"
|
||||
x-show="editing" x-cloak>
|
||||
@csrf @method('PATCH')
|
||||
<input type="text" name="name" class="form-input"
|
||||
value="{{ $game->name }}" required>
|
||||
<select name="platform_id" class="form-select form-select--small" required>
|
||||
@foreach($platforms as $platform)
|
||||
<option value="{{ $platform->id }}"
|
||||
{{ $game->platform_id == $platform->id ? 'selected' : '' }}>
|
||||
{{ $platform->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="genre_id" class="form-select form-select--small" required>
|
||||
@foreach($genres as $genre)
|
||||
<option value="{{ $genre->id }}"
|
||||
{{ $game->genre_id == $genre->id ? 'selected' : '' }}>
|
||||
{{ $genre->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn primary">
|
||||
<i data-lucide="check" size="13"></i>
|
||||
</button>
|
||||
<button type="button" class="btn" @click="editing = false">
|
||||
<i data-lucide="x" size="13"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="modcp-list-item-actions" x-show="!editing">
|
||||
<button type="button" class="btn" @click="editing = true">
|
||||
<i data-lucide="pen" size="13"></i>
|
||||
</button>
|
||||
<form action="{{ route('modcp.games.destroy', $game) }}" method="POST"
|
||||
style="display:inline"
|
||||
onsubmit="return confirm('Delete {{ addslashes($game->name) }}?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn danger"
|
||||
{{ $game->entries_count > 0 ? 'disabled title=Has entries' : '' }}>
|
||||
<i data-lucide="trash-2" size="13"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@empty
|
||||
<div class="modcp-empty"><p>No games yet.</p></div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{ $items->links() }}
|
||||
|
||||
@endsection
|
||||
91
resources/views/modcp/index.blade.php
Normal file
91
resources/views/modcp/index.blade.php
Normal file
@@ -0,0 +1,91 @@
|
||||
@extends('layouts.modcp')
|
||||
|
||||
@section('page-title', "Dashboard - " . config('app.name') )
|
||||
|
||||
@section('modcp-content')
|
||||
<div class="modcp-page-title">
|
||||
Dashboard
|
||||
</div>
|
||||
<div class="modcp-stats">
|
||||
<a href="{{ route('queue.index') }}" class="modcp-stat-card modcp-stat-card--orange">
|
||||
<div class="modcp-stat-icon"><i data-lucide="clipboard-list" size="22"></i></div>
|
||||
<div class="modcp-stat-info">
|
||||
<span class="modcp-stat-value">{{ $stats['pending'] }}</span>
|
||||
<span class="modcp-stat-label">In queue</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="{{ route('modcp.locked') }}" class="modcp-stat-card">
|
||||
<div class="modcp-stat-icon"><i data-lucide="lock" size="22"></i></div>
|
||||
<div class="modcp-stat-info">
|
||||
<span class="modcp-stat-value">{{ $stats['locked'] }}</span>
|
||||
<span class="modcp-stat-label">Locked</span>
|
||||
</div>
|
||||
</a>
|
||||
@can('is-admin')
|
||||
<a href="{{ route('modcp.draft') }}" class="modcp-stat-card">
|
||||
<div class="modcp-stat-icon"><i data-lucide="scissors" size="22"></i></div>
|
||||
<div class="modcp-stat-info">
|
||||
<span class="modcp-stat-value">{{ $stats['draft'] }}</span>
|
||||
<span class="modcp-stat-label">Draft</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="{{ route('modcp.hidden') }}" class="modcp-stat-card">
|
||||
<div class="modcp-stat-icon"><i data-lucide="eye-off" size="22"></i></div>
|
||||
<div class="modcp-stat-info">
|
||||
<span class="modcp-stat-value">{{ $stats['hidden'] }}</span>
|
||||
<span class="modcp-stat-label">Hidden</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="{{ route('modcp.deleted') }}" class="modcp-stat-card modcp-stat-card--danger">
|
||||
<div class="modcp-stat-icon"><i data-lucide="trash-2" size="22"></i></div>
|
||||
<div class="modcp-stat-info">
|
||||
<span class="modcp-stat-value">{{ $stats['deleted'] }}</span>
|
||||
<span class="modcp-stat-label">Deleted</span>
|
||||
</div>
|
||||
</a>
|
||||
@endcan
|
||||
<div class="modcp-stat-card modcp-stat-card--muted">
|
||||
<div class="modcp-stat-icon"><i data-lucide="database" size="22"></i></div>
|
||||
<div class="modcp-stat-info">
|
||||
<span class="modcp-stat-value">{{ $stats['total'] }}</span>
|
||||
<span class="modcp-stat-label">Total entries</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($recentDeleted->isNotEmpty())
|
||||
<div class="modcp-section-title" style="margin-top: 25px;">Recently deleted</div>
|
||||
<div class="modcp-list">
|
||||
@foreach($recentDeleted as $entry)
|
||||
<div class="modcp-list-item">
|
||||
<div class="modcp-list-item-info">
|
||||
<span class="modcp-list-item-title">{{ $entry->complete_title ?? $entry->title }}</span>
|
||||
<span class="modcp-list-item-meta">
|
||||
<span class="badge {{ $entry->type }}">{{ $entry->type }}</span>
|
||||
Deleted {{ $entry->deleted_at->diffForHumans() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="modcp-list-item-actions">
|
||||
<form action="{{ route('modcp.restore', $entry->id) }}" method="POST" style="display:inline">
|
||||
@csrf @method('PATCH')
|
||||
<button type="submit" class="btn success">
|
||||
<i data-lucide="rotate-ccw" size="13"></i> Restore
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ route('modcp.destroy', $entry->id) }}" method="POST" style="display:inline"
|
||||
onsubmit="return confirm('Permanently delete?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn danger">
|
||||
<i data-lucide="trash-2" size="13"></i> Purge
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<a href="{{ route('modcp.deleted') }}" class="modcp-list-see-all">
|
||||
See all deleted entries →
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
76
resources/views/modcp/resources.blade.php
Normal file
76
resources/views/modcp/resources.blade.php
Normal file
@@ -0,0 +1,76 @@
|
||||
@extends('layouts.modcp')
|
||||
|
||||
@section('page-title', $title . ' - ' . config('app.name'))
|
||||
|
||||
@section('modcp-content')
|
||||
|
||||
<div class="modcp-page-title">
|
||||
{{ $title }}
|
||||
<span class="modcp-count">{{ $items->total() }}</span>
|
||||
</div>
|
||||
|
||||
<x-mod-c-p-search placeholder="Search a {{ $singular }}..." />
|
||||
|
||||
<div class="modcp-add-form">
|
||||
<form action="{{ route($storeRoute) }}" method="POST" class="modcp-add-form-inner">
|
||||
@csrf
|
||||
<input type="text" name="name" class="form-input" placeholder="Add new {{ strtolower($singular) }}..." required>
|
||||
@if(isset($extraFields))
|
||||
@foreach($extraFields as $field)
|
||||
<input type="text" name="{{ $field['name'] }}" class="form-input" placeholder="{{ $field['placeholder'] }}">
|
||||
@endforeach
|
||||
@endif
|
||||
<button type="submit" class="btn primary">
|
||||
<i data-lucide="plus" size="14"></i> Add
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modcp-list">
|
||||
@forelse($items as $item)
|
||||
<div class="modcp-list-item" x-data="{ editing: false }">
|
||||
<div class="modcp-list-item-info" x-show="!editing">
|
||||
<span class="modcp-list-item-title">{{ $item->name }}</span>
|
||||
<span class="modcp-list-item-meta">
|
||||
slug: {{ $item->slug }}
|
||||
@isset($item->entries_count)
|
||||
· {{ $item->entries_count }} {{ Str::plural('entry', $item->entries_count) }}
|
||||
@endisset
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form action="{{ route($updateRoute, $item) }}" method="POST"
|
||||
class="modcp-list-item-edit" x-show="editing" x-cloak>
|
||||
@csrf @method('PATCH')
|
||||
<input type="text" name="name" class="form-input" value="{{ $item->name }}">
|
||||
<button type="submit" class="btn primary">
|
||||
<i data-lucide="check" size="13"></i>
|
||||
</button>
|
||||
<button type="button" class="btn" @click="editing = false">
|
||||
<i data-lucide="x" size="13"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="modcp-list-item-actions" x-show="!editing">
|
||||
<button type="button" class="btn" @click="editing = true">
|
||||
<i data-lucide="pen" size="13"></i>
|
||||
</button>
|
||||
<form action="{{ route($destroyRoute, $item) }}" method="POST" style="display:inline"
|
||||
onsubmit="return confirm('Delete {{ $item->name }}?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn danger">
|
||||
<i data-lucide="trash-2" size="13"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="modcp-empty">
|
||||
<p>No {{ strtolower($title) }} yet.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{ $items->links() }}
|
||||
|
||||
@endsection
|
||||
@@ -137,7 +137,7 @@
|
||||
@endif
|
||||
|
||||
<x-form-group-title label="{{ $words['attachments'] }}" icon="paperclip" />
|
||||
<x-main-image-field :old-path="old('main-image', $entry->main_image ?? '')" />
|
||||
<x-main-image-field :old-path="old('main-image', $entry->main_image ?? '') ?? ''" />
|
||||
<x-gallery-field :old-paths="old('gallery', $entry->gallery->pluck('image')->toArray() ?? [] )"/>
|
||||
@error('gallery')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@@ -181,6 +181,18 @@
|
||||
<livewire:xf-user-selector :initial-user-id="$entry->user_id" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group grid-c2">
|
||||
<div>
|
||||
<x-form-field-title name="XenForo Comments Thread ID" />
|
||||
<input type="text" name="comments_thread_id" class="form-input" value="{{ old('comments_thread_id', $entry->comments_thread_id) }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<x-form-field-title name="Metadata" required="true" />
|
||||
<div class="form-type-of-checkboxes form-group level" id="entry-metadata">
|
||||
<label><input class="form-checkbox" type="checkbox" name="featured" value="1" {{ old('featured', $entry->featured ) ? 'checked' : '' }}>Featured entry</label>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@cannot('moderate', $entry)
|
||||
|
||||
|
||||
@@ -36,21 +36,18 @@
|
||||
'upload-item-done': file.done,
|
||||
'upload-item-error': file.error
|
||||
}">
|
||||
<template x-if="!file.done && !file.error">
|
||||
<i data-lucide="loader-2" class="spin"></i>
|
||||
</template>
|
||||
<template x-if="file.error">
|
||||
<i data-lucide="alert-circle"></i>
|
||||
</template>
|
||||
<template x-if="file.done && file.state === 'public'">
|
||||
<i data-lucide="eye" class="file-state-icon file-state-icon--public"></i>
|
||||
</template>
|
||||
<template x-if="file.done && file.state === 'private'">
|
||||
<i data-lucide="eye-off" class="file-state-icon file-state-icon--private"></i>
|
||||
</template>
|
||||
<template x-if="file.done && file.state === 'archived'">
|
||||
<i data-lucide="archive" class="file-state-icon file-state-icon--archived"></i>
|
||||
</template>
|
||||
|
||||
<div class="file-status-icons">
|
||||
<i x-show="!file.done && !file.error" data-lucide="loader-2" class="spin"></i>
|
||||
|
||||
<i x-show="file.error" data-lucide="alert-circle"></i>
|
||||
|
||||
<i x-show="file.done && file.state === 'public'" data-lucide="eye" class="file-state-icon file-state-icon--public"></i>
|
||||
|
||||
<i x-show="file.done && file.state === 'private'" data-lucide="eye-off" class="file-state-icon file-state-icon--private"></i>
|
||||
|
||||
<i x-show="file.done && file.state === 'archived'" data-lucide="archive" class="file-state-icon file-state-icon--archived"></i>
|
||||
</div>
|
||||
|
||||
<div class="upload-item-info">
|
||||
<span class="upload-item-name" x-text="file.name"></span>
|
||||
@@ -103,13 +100,46 @@
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="upload-item-actions">
|
||||
<div class="upload-item-actions" x-data="{ showMetadata: false }">
|
||||
<button type="button" class="btn" x-show="file.error" @click="handleRetryFile(i)">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
</button>
|
||||
@if($isEdit)
|
||||
<button type="button" class="btn" x-show="file.done" @click="showMetadata = true">
|
||||
<i data-lucide="settings"></i>
|
||||
</button>
|
||||
@endif
|
||||
<button type="button" class="btn" x-show="file.done || file.error" @click="handleRemoveFile(i)">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
<template x-teleport="body">
|
||||
<div class="modal-overlay"
|
||||
x-cloak
|
||||
x-show="showMetadata"
|
||||
x-transition.opacity.duration.300ms
|
||||
@click.self="showMetadata = false"
|
||||
@keydown.escape.window="showMetadata = false">
|
||||
<div class="modal-window" x-show="showMetadata" x-transition>
|
||||
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" style="display: flex; align-items: center; gap: 8px;">
|
||||
File Settings: <span x-text="file.name" style="color: var(--rhpz-orange);"></span>
|
||||
</span>
|
||||
<button type="button" class="modal-close" @click="showMetadata = false">
|
||||
<i data-lucide="x" size="20"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-content">
|
||||
<div class="form-group">
|
||||
<x-form-group-title label="Online patcher" />
|
||||
<label><input type="checkbox" class="form-checkbox" :name="'files_metadata[' + file.uuid + '][online_patcher]'" x-model="file.meta_online_patcher"> Enable it</label>
|
||||
<label x-show="file.meta_online_patcher"><input type="checkbox" class="form-checkbox" :name="'files_metadata[' + file.meta_secondary_online_patcher + '][secondary_online_patcher]'" x-model="file.meta_secondary_online_patcher"> Mark as a secondary patch</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<input type="hidden" name="files_uuid[]" :value="file.uuid" x-show="file.done">
|
||||
@if($isEdit)
|
||||
|
||||
92
resources/views/tools/patcher.blade.php
Normal file
92
resources/views/tools/patcher.blade.php
Normal file
@@ -0,0 +1,92 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('page-title', "ROM Patcher - " . config('app.name'))
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{ asset('rom-patcher-js/RomPatcher.webapp.js') }}"></script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="page-title">
|
||||
<span>ROM Patcher</span>
|
||||
</div>
|
||||
|
||||
<div id="rom-patcher-container" class="patcher-container" x-data="RomPatcher({{ Js::from($patches ?? []) }})">
|
||||
|
||||
<div class="patcher-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">1. Original ROM</label>
|
||||
<div class="patcher-dropzone" id="rom-dropzone"
|
||||
:class="{ 'dragover': isRomDragOver, 'has-file': romFileName !== '' }"
|
||||
@click="triggerFileInput('rom-patcher-input-file-rom')"
|
||||
@dragover.prevent="isRomDragOver = true"
|
||||
@dragleave.prevent="isRomDragOver = false"
|
||||
@drop.prevent="isRomDragOver = false; handleDrop($event, 'rom')">
|
||||
|
||||
<input type="file" id="rom-patcher-input-file-rom" style="display: none;" @change="handleInputChange($event, 'rom')" disabled />
|
||||
<i data-lucide="gamepad-2" size="40" :style="romFileName ? 'color: var(--rhpz-orange)' : 'color: var(--text2)'"></i>
|
||||
<span style="color: var(--text); font-weight: 500;" x-text="romFileName ? romFileName : 'Drag n\'drop your file here or click'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">2. Patch file</label>
|
||||
|
||||
<div class="patcher-dropzone" id="patch-dropzone"
|
||||
x-show="!hasEmbedded"
|
||||
:class="{ 'dragover': isPatchDragOver, 'has-file': patchFileName !== '' }"
|
||||
@click="triggerFileInput('rom-patcher-input-file-patch')"
|
||||
@dragover.prevent="isPatchDragOver = true"
|
||||
@dragleave.prevent="isPatchDragOver = false"
|
||||
@drop.prevent="isPatchDragOver = false; handleDrop($event, 'patch')">
|
||||
|
||||
<input type="file" id="rom-patcher-input-file-patch" style="display: none;" @change="handleInputChange($event, 'patch')" disabled />
|
||||
<i data-lucide="file-archive" size="40" :style="patchFileName ? 'color: var(--rhpz-orange)' : 'color: var(--text2)'"></i>
|
||||
<span style="color: var(--text); font-weight: 500;" x-text="patchFileName ? patchFileName : 'Drag n\'drop your file here or click'"></span>
|
||||
</div>
|
||||
|
||||
<div x-show="hasEmbedded" class="embed-patch-box">
|
||||
<div class="embed-patch-box-icon">
|
||||
<div class="embed-patch-box-icon-block">
|
||||
<i data-lucide="package" size="24" color="var(--rhpz-orange)"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight: 600; color: var(--text); font-size: 1.1rem;">Patch file</div>
|
||||
<div style="font-size: 0.85rem; color: var(--text2);">Select if there is multiple patch files</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rom-patcher-container-input" style="margin-top: 10px;">
|
||||
<select id="rom-patcher-select-patch" class="form-select" style="width: 100%; cursor: pointer;"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="patcher-status-box" id="patcher-status" x-show="showStatusBox" x-transition x-cloak style="margin-top: 20px;">
|
||||
<div class="rom-patcher-row" style="color: var(--text2);">
|
||||
<div style="color: var(--rhpz-orange); font-weight: bold;">Checksums:</div>
|
||||
<ul style="margin-bottom: 0">
|
||||
<li>CRC32: <span id="rom-patcher-span-crc32"></span></li>
|
||||
<li>MD5: <span id="rom-patcher-span-md5"></span></li>
|
||||
<li>SHA-1: <span id="rom-patcher-span-sha1"></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<span id="rom-patcher-span-rom-info"></span>
|
||||
<div class="rom-patcher-row margin-bottom" id="rom-patcher-row-patch-description">
|
||||
<div style="color: var(--rhpz-orange); font-weight: bold;">Description:</div>
|
||||
<div id="rom-patcher-patch-description"></div>
|
||||
</div>
|
||||
<div class="rom-patcher-row margin-bottom" id="rom-patcher-row-patch-requirements">
|
||||
<div id="rom-patcher-patch-requirements-type" style="color: var(--rhpz-orange); font-weight: bold;">ROM requirements:</div>
|
||||
<div id="rom-patcher-patch-requirements-value"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 25px; border-top: 1px solid var(--border); padding-top: 20px; text-align: right;">
|
||||
<button type="button" class="btn primary" id="rom-patcher-button-apply" disabled>
|
||||
<i data-lucide="wrench" size="16"></i> Apply patch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\EntryController;
|
||||
use App\Http\Controllers\ModCP\LanguageController;
|
||||
use App\Http\Controllers\WebhookController;
|
||||
use Illuminate\Routing\RedirectController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -12,6 +13,9 @@ Route::get('/', [ \App\Http\Controllers\HomeController::class, 'index' ] )->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');
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user