Add additional mod functionality

This commit is contained in:
RainbowShaggy 2024-11-04 23:32:56 +01:00 committed by Miraak
parent 0ec61feaeb
commit a6e463d929
6 changed files with 322 additions and 193 deletions

View File

@ -16,7 +16,7 @@ public function getFileWithPatchline(string $patchlineName, string $hash) : Bina
{
$patchline = Patchline::tryFromName(str($patchlineName)->upper());
$gameFile = GameFile::select(['name'])->wherePatchline($patchline)->whereHash($hash)->latest()->first();
$gameFile = GameFile::select(['filename'])->wherePatchline($patchline)->whereHash($hash)->latest()->first();
$disk = Storage::disk('patches');
if($gameFile === null)
@ -49,7 +49,33 @@ public function getGameFileList(string $patchlineName = null) : JsonResponse
return response()->json(['error' => 'Unauthorized'], 401);
}
$gameFiles = GameFile::select(['name', 'hash', 'game_path', 'action'])->where('patchline', $patchline->value)->latest()->get();
$gameFiles = GameFile::select(['filename', 'hash', 'game_path', 'action'])
->where('patchline', $patchline->value)
->where('is_additional', 0)->latest()->get();
if (count($gameFiles) <= 0)
return response()->json(['error' => 'No files found'], 404);
return response()->json($gameFiles, 200);
}
public function getGameModFileList(string $patchlineName = null) : JsonResponse
{
$patchline = Patchline::tryFromName(str($patchlineName)->upper()) ?? Patchline::LIVE;
if (!$patchline) {
return response()->json(['error' => 'Invalid patchline'], 404);
}
$neededRole = $patchline->getNeededRole();
if ($neededRole !== null && ( !Auth::check() || !Auth::user()->hasRole($neededRole->value))) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$gameFiles = GameFile::select(['name', 'description', 'filename', 'hash', 'game_path', 'action'])
->where('patchline', $patchline->value)
->where('is_additional', 1)->latest()->get();
if (count($gameFiles) <= 0)
return response()->json(['error' => 'No files found'], 404);

View File

@ -4,14 +4,11 @@
use App\Enums\Auth\Permissions;
use App\Enums\Launcher\Patchline;
use App\Http\Controllers\Controller;
use App\Models\GameFile;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
class FileManagerController extends AdminToolController
{
@ -29,6 +26,7 @@ public function store(Request $request)
'files.*' => 'required|file',
'patchline' => 'required|integer',
'gamepath.*' => 'required|string',
'is_additional' => 'required|integer',
]);
$duplicateFiles = [];
@ -37,35 +35,42 @@ public function store(Request $request)
// Handle file upload logic
if ($request->hasFile('files')) {
for($i = 0; $i < count($request->file('files')); $i++) {
$file = $request->file('files')[$i];
if (!$request->hasFile('files')) {
Session::flash('alert-error', 'Failed to upload files. No files were provided.');
return redirect()->back();
}
$filename = $file->getClientOriginalName();
$filehash = str(hash_file('sha256', $file->getRealPath()))->upper();
for ($i = 0; $i < count($request->file('files')); $i++) {
$file = $request->file('files')[$i];
$gameFile = GameFile::where('name', $filename)->where('patchline', $request->input('patchline'))->first() ?? new GameFile;
$filename = $file->getClientOriginalName();
$filehash = str(hash_file('sha256', $file->getRealPath()))->upper();
if ($gameFile->hash == $filehash) {
$duplicateFiles[] = $gameFile->name;
continue;
}
$gameFile = GameFile::where('filename', $filename)->where('patchline', $request->input('patchline'))->first() ?? new GameFile;
if (isset($gameFile->id)) {
$overwrittenFiles[] = $gameFile->name;
}
$gameFile->name = $filename;
$gameFile->hash = $filehash;
$gameFile->patchline = $request->input('patchline');
GameFile::getDisk()->putFileAs(strtolower($gameFile->patchline->name), $file, $gameFile->name);
$uploadedFiles[] = $gameFile->name;
$gameFile->game_path = $request->game_path[$i];
$gameFile->action = $request->file_action[$i];
$gameFile->save();
if ($gameFile->hash == $filehash) {
$duplicateFiles[] = $gameFile->filename;
continue;
}
if (isset($gameFile->id)) {
$overwrittenFiles[] = $gameFile->filename;
}
$gameFile->filename = $filename;
$gameFile->hash = $filehash;
$gameFile->patchline = $request->input('patchline');
$gameFile->name = $request->input('game_mod_name')[$i];
$gameFile->description = $request->input('game_mod_description')[$i];
$gameFile->is_additional = $request->input('is_additional');
GameFile::getDisk()->putFileAs(strtolower($gameFile->patchline->name), $file, $gameFile->filename);
$uploadedFiles[] = $gameFile->filename;
$gameFile->game_path = $request->game_path[$i];
$gameFile->action = $request->file_action[$i];
$gameFile->save();
}
if (count($uploadedFiles) === 0) {
@ -95,10 +100,11 @@ public function store(Request $request)
public function index(Request $request) : View {
$patchline = Patchline::tryFrom($request->input('patchline')) ?? Patchline::LIVE;
$files = GameFile::latest()->where('patchline', $patchline)->get();
$files = GameFile::latest()->where('patchline', $patchline)->where('is_additional', (bool)$request->input('additional_files'))->get();
return view('admin.tools.file-manager', [
'patchline' => $patchline,
'showAdditionalFiles' => (bool)$request->input('additional_files'),
'files' => $files,
]);
}
@ -107,9 +113,9 @@ public function update(GameFile $file_manager) : RedirectResponse {
$file_manager->action = (int)!$file_manager->action->value;
if($file_manager->save()) {
Session::flash('alert-success', 'File ' . $file_manager->name . ' was successfully marked for ' . ($file_manager->action->value ? 'add' : 'delete'));
Session::flash('alert-success', 'File ' . $file_manager->filename . ' was successfully marked for ' . ($file_manager->action->value ? 'add' : 'delete'));
} else {
Session::flash('alert-error', 'Failed to mark file ' . $file_manager->name . ' for ' . ($file_manager->action->value ? 'add' : 'delete'));
Session::flash('alert-error', 'Failed to mark file ' . $file_manager->filename . ' for ' . ($file_manager->action->value ? 'add' : 'delete'));
}
return redirect()->back();
@ -118,7 +124,7 @@ public function update(GameFile $file_manager) : RedirectResponse {
public function destroy(GameFile $file_manager) : RedirectResponse {
$file_manager->delete();
Session::flash('alert-success', 'File ' . $file_manager->name . ' was successfully deleted');
Session::flash('alert-success', 'File ' . $file_manager->filename . ' was successfully deleted');
return redirect()->back();
}

View File

@ -40,7 +40,7 @@ public static function getDisk():FileSystemAdapter {
}
public function getDiskPath(): string {
return strtolower($this->patchline->name).'/'.$this->name;
return strtolower($this->patchline->name).'/'.$this->filename;
}
public function fileExists(): bool

View File

@ -0,0 +1,51 @@
<?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 doesn't support tinyint
DB::statement("ALTER TABLE `game_files` ADD COLUMN `is_additional` TINYINT UNSIGNED NOT NULL DEFAULT '0' AFTER `action`;");
if (Schema::hasIndex('game_files', ['name', 'hash', 'version'])) {
Schema::table('game_files', function (Blueprint $table) {
$table->dropUnique(['name', 'hash', 'version']);
});
}
Schema::table('game_files', function (Blueprint $table) {
$table->renameColumn('name', 'filename');
$table->unique(['filename', 'hash', 'patchline']);
});
Schema::table('game_files', function (Blueprint $table) {
$table->string('name', 128)->nullable()->after('filename');
$table->string('description', 255)->nullable()->after('name');
$table->unique(['name']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('game_files', function (Blueprint $table) {
$table->dropUnique(['filename', 'hash', 'patchline']);
$table->dropUnique(['name']);
$table->dropColumn('is_additional');
$table->dropColumn('name');
$table->dropColumn('description');
$table->renameColumn('filename', 'name');
$table->unique(['name', 'hash', 'patchline']);
});
}
};

View File

@ -6,181 +6,224 @@
/** @var Patchline $patchline */
@endphp
<x-layouts.admin>
<div class="w-full p-2 md:px-16 bg-inherit container mx-auto">
<h1 class="text-4xl font-semilight py-10">Deathgarden file manager</h1>
<x-layouts.admin>
<div class="w-full p-2 md:px-16 bg-inherit container mx-auto">
<h1 class="text-4xl font-semilight py-10">Deathgarden file manager</h1>
<form action="{{ url()->current() }}" method="GET">
<div class="flex flex-row items-center">
<label for="patchlines" class="block my-4 mr-4 font-medium text-gray-900 dark:text-white">Select a
patchline:</label>
<x-inputs.dropdown
id="patchlines"
required
name="patchline"
:cases="Patchline::cases()"
:selected="$patchline"
onchange="this.form.submit()"
/>
</div>
</form>
<div class="flex flex-col items-center justify-center">
<table class="">
<thead>
<th>Name</th>
<th>Hash</th>
<th>Size</th>
<th>Last update</th>
<th>Actions</th>
</thead>
<tbody>
@foreach($files as $file)
<tr @class([
'text-center',
'!bg-green-600 hover:!bg-green-500' => $file->action === FileAction::ADD,
'!bg-red-600 hover:!bg-red-500' => $file->action === FileAction::DELETE
<form action="{{ url()->current() }}" method="GET">
<div class="flex items-center my-4 w-1/2">
<div class="flex-auto">
<label for="patchlines" class="mr-4 font-medium text-gray-900 dark:text-white">Select a
patchline:</label>
<x-inputs.dropdown
id="patchlines"
required
name="patchline"
:cases="Patchline::cases()"
:selected="$patchline"
onchange="this.form.submit()" />
</div>
<div class="flex flex-auto">
<label for="additional_files">Show additional mods:</label>
<x-inputs.checkbox
class="w-6 h-6 ml-4"
id="additional_files"
name="additional_files"
:checked="$showAdditionalFiles"
value="1"
onchange="this.form.submit()" />
</div>
</div>
</form>
<div class="flex flex-col items-center justify-center">
<table>
<thead>
@if($showAdditionalFiles)
<th>Name</th>
<th>Description</th>
<th>Filename / Hash</th>
@else
<th>Filename</th>
<th>Hash</th>
@endif
<th class="w-24">Size</th>
<th class="w-32">Last update</th>
<th>Actions</th>
</thead>
<tbody>
@foreach($files as $file)
<tr @class([ 'text-center' , '!bg-green-600 hover:!bg-green-500'=> $file->action === FileAction::ADD,
'!bg-red-600 hover:!bg-red-500' => $file->action === FileAction::DELETE
])
>
<td title="{{$file->game_path}}">
{{ $file->name }}
</td>
<td>
{{ $file->hash }}
</td>
<td>
@if($file->fileExists())
{{ round($file->getFileSize() / 1000, 1) }} kB
>
@if($showAdditionalFiles)
<td>
{{ $file->name }}
</td>
<td>
{{ $file->description }}
</td>
<td title="{{$file->game_path}}">
{{ $file->filename }} <br>
{{ $file->hash }}
</td>
@else
Error while fetching file
<td title="{{$file->game_path}}">
{{ $file->filename }}
</td>
<td>
{{ $file->hash }}
</td>
@endif
</td>
<td>
{{ $file->updated_at }}
</td>
<td>
<form action="{{ route('file-manager.update', ['file_manager' => $file->id]) }}"
method="POST">
@method('PUT')
@csrf
<button class="">Mark for {{ $file->action->value ? 'delete' : 'add' }}</button>
</form>
<form action="{{ route('file-manager.destroy', ['file_manager' => $file->id]) }}"
method="POST">
@method('DELETE')
@csrf
<button class=""
<td>
@if($file->fileExists())
{{ round($file->getFileSize() / 1000, 1) }} kB
@else
Error while fetching file
@endif
</td>
<td>
{{ $file->updated_at }}
</td>
<td>
<form action="{{ route('file-manager.update', ['file_manager' => $file->id]) }}"
method="POST">
@method('PUT')
@csrf
<button class="">Mark for {{ $file->action->value ? 'delete' : 'add' }}</button>
</form>
<form action="{{ route('file-manager.destroy', ['file_manager' => $file->id]) }}"
method="POST">
@method('DELETE')
@csrf
<button class=""
onclick="return confirm('This will delete the file on the server, but won\'t do any actions on the clients.\nAre you sure?')">
Delete
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
Delete
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="mx-auto w-full max-w-screen-2xl mt-8">
<form action="{{ route('file-manager.store') }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="patchline" value="{{ request()->input('patchline') ?? '0' }}">
@csrf
<div id="fileInputsContainer">
<div class="flex flex-wrap gap-4">
<div class="w-2/5 flex items-center mb-2">
<x-inputs.text-input name="game_path[]"/>
<div class="mx-auto w-full max-w-screen-2xl mt-8">
<form action="{{ route('file-manager.store') }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="patchline" value="{{ request()->input('patchline') ?? '0' }}">
<input type="hidden" name="is_additional" value="{{ request()->input('additional_files') ?? '0' }}">
@csrf
<div id="fileInputsContainer">
@if($showAdditionalFiles)
<div class="flex flex-wrap gap-4">
<div class="w-2/12 flex items-center mb-2">
<x-inputs.text-input name="game_mod_name[]" placeholder="Mod name" />
</div>
<div class="w-5/12 flex items-center mb-2">
<x-inputs.text-input name="game_mod_description[]" placeholder="Mod description" />
</div>
</div>
<select name="file_action[]"
class="w-32 rounded-md h-[41.43px] bg-gray-800/75 border border-gray-600 text-white text-sm focus:border-[#6A64F1] focus:shadow-md block px-4 py-2">
<option value="1" selected>Add</option>
<option value="0">Delete</option>
</select>
<div class="flex items-center mb-2">
<x-inputs.file-input name="files[]"/>
@endif
<div class="flex flex-wrap gap-4">
<div class="w-6/12 flex items-center mb-2">
<x-inputs.text-input name="game_path[]" />
</div>
<select name="file_action[]"
class="w-1/12 rounded-md h-[41.43px] bg-gray-800/75 border border-gray-600 text-white text-sm focus:border-[#6A64F1] focus:shadow-md block px-4 py-2">
<option value="1" selected>Add</option>
<option value="0">Delete</option>
</select>
<div class="flex items-center mb-2">
<x-inputs.file-input name="files[]" />
</div>
</div>
</div>
</div>
<div class="flex justify-end mt-4">
<div class="w-auto mx-2">
<button type="button" id="addFileInput"
class="inline-flex rounded-md bg-gray-800/75 px-6 py-2 font-semibold text-gray-300 hover:text-white transition focus:outline-none focus-visible:ring-2 focus-visible:ring-[#BD92F5] focus-visible:ring-offset-2 focus-visible:ring-offset-black">
Add More Files
</button>
<div class="flex justify-end mt-4">
@if (!$showAdditionalFiles)
<div class="w-auto mx-2">
<x-inputs.button type="button" id="addFileInput">
Add More Files
</x-inputs.button>
</div>
@endif
<div class="w-auto mx-2">
<x-inputs.button>
Submit
</x-inputs.button>
</div>
</div>
<div class="w-auto mx-2">
<button class="inline-flex rounded-md bg-gray-800/75 px-6 py-2 font-semibold text-gray-300 hover:text-white transition focus:outline-none focus-visible:ring-2 focus-visible:ring-[#BD92F5] focus-visible:ring-offset-2 focus-visible:ring-offset-black">
Submit
</button>
</div>
</div>
</form>
</form>
</div>
</div>
</div>
</div>
<script>
function addFileInputEventListener(fileInput) {
fileInput.addEventListener('change', function (event) {
let selectedFile = event.target.files[0];
let textInput = event.target.closest('.flex-wrap').querySelector('input[name="game_path[]"]');
if (textInput) {
if (selectedFile) {
var fileName = selectedFile.name;
fileName = examineFilePaths(fileName);
textInput.value = fileName;
} else {
textInput.value = '';
<script>
function addFileInputEventListener(fileInput) {
fileInput.addEventListener('change', function(event) {
let selectedFile = event.target.files[0];
let textInput = event.target.closest('.flex-wrap').querySelector('input[name="game_path[]"]');
if (textInput) {
if (selectedFile) {
var fileName = selectedFile.name;
fileName = examineFilePaths(fileName);
textInput.value = fileName;
} else {
textInput.value = '';
}
}
});
}
function examineFilePaths(fileName) {
switch (fileName) {
case 'TheExit_BE.exe':
return './TheExit/Binaries/Win64/' + fileName;
case 'BEClient_x64.dll':
return './TheExit/Binaries/Win64/BattlEye/' + fileName;
default:
break;
}
var extension = fileName.split('.').pop();
switch (extension) {
case 'pak':
return './TheExit/Content/Paks/' + fileName;
case 'sig':
return './TheExit/Content/Paks/' + fileName;
default:
return './' + fileName;
}
}
@if(!$showAdditionalFiles)
document.getElementById('addFileInput').addEventListener('click', function() {
let container = document.getElementById('fileInputsContainer');
let newInput = document.createElement('div');
newInput.classList.add('flex', 'flex-wrap', 'gap-4');
newInput.innerHTML = `
<div class="w-6/12 flex items-center mb-2">
<x-inputs.text-input name="game_path[]" />
</div>
<select name="file_action[]" class="w-1/12 rounded-md h-[41.43px] bg-gray-800/75 border border-gray-600 text-white text-sm focus:border-[#6A64F1] focus:shadow-md block px-4 py-2">
<option value="1" selected>Add</option>
<option value="0">Delete</option>
</select>
<div class="flex items-center mb-2">
<x-inputs.file-input name="files[]" />
</div>
`;
container.appendChild(newInput);
// Add event listener to file input of the newly created input group
var fileInput = newInput.querySelector('input[name="files[]"]');
addFileInputEventListener(fileInput);
});
}
@endif
function examineFilePaths(fileName) {
switch (fileName) {
case 'TheExit_BE.exe':
return './TheExit/Binaries/Win64/' + fileName;
case 'BEClient_x64.dll':
return './TheExit/Binaries/Win64/BattlEye/' + fileName;
default:
break;
}
var extension = fileName.split('.').pop();
switch (extension) {
case 'pak':
return './TheExit/Content/Paks/' + fileName;
case 'sig':
return './TheExit/Content/Paks/' + fileName;
default:
return './' + fileName;
}
}
document.getElementById('addFileInput').addEventListener('click', function () {
let container = document.getElementById('fileInputsContainer');
let newInput = document.createElement('div');
newInput.classList.add('flex', 'flex-wrap', 'gap-4');
newInput.innerHTML = `
<div class="w-2/5 flex items-center mb-2">
<x-inputs.text-input name="game_path[]" />
</div>
<select name="file_action[]" class="w-32 rounded-md h-[41.43px] bg-gray-800/75 border border-gray-600 text-white text-sm focus:border-[#6A64F1] focus:shadow-md block px-4 py-2">
<option value="1" selected>Add</option>
<option value="0">Delete</option>
</select>
<div class="flex items-center mb-2">
<x-inputs.file-input name="files[]" />
</div>
`;
container.appendChild(newInput);
// Add event listener to file input of the newly created input group
var fileInput = newInput.querySelector('input[name="files[]"]');
addFileInputEventListener(fileInput);
});
// Add event listener to file input of the initial input group
var initialFileInput = document.querySelector('input[name="files[]"]');
addFileInputEventListener(initialFileInput);
</script>
</x-layouts.admin>
// Add event listener to file input of the initial input group
var initialFileInput = document.querySelector('input[name="files[]"]');
addFileInputEventListener(initialFileInput);
</script>
</x-layouts.admin>

3
dist/routes/api.php vendored
View File

@ -18,6 +18,9 @@
Route::get('patch/files', [PatchController::class, 'getGameFileList']);
Route::get('patch/{patchlineName}/files', [PatchController::class, 'getGameFileList']);
Route::get('patch/mods', [PatchController::class, 'getGameModFileList']);
Route::get('patch/{patchlineName}/mods', [PatchController::class, 'getGameModFileList']);
Route::get('launcher-version', [StatisticsController::class, 'getLauncherVersion']);
Route::get('launcher-message', [StatisticsController::class, 'getLauncherMessage']);
});