diff --git a/.ai/mcp/mcp.json b/.ai/mcp/mcp.json new file mode 100644 index 000000000..1ed7581f5 --- /dev/null +++ b/.ai/mcp/mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "laravel-boost": { + "command": "/opt/homebrew/Cellar/php/8.5.1/bin/php", + "args": [ + "/Users/hannes/PhpstormProjects/API/artisan", + "boost:mcp" + ] + } + } +} \ No newline at end of file diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 000000000..4a165c12d --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,7 @@ +[mcp_servers.laravel-boost] +command = "php" +args = ["artisan", "boost:mcp"] + +[mcp_servers.php] +command = "artisan" +args = ["boost:mcp"] diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..a186cd207 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[compose.yaml] +indent_size = 4 diff --git a/.env.ci b/.env.ci deleted file mode 100644 index 2358c2a34..000000000 --- a/.env.ci +++ /dev/null @@ -1,15 +0,0 @@ -APP_NAME=API_CI -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_URL=http://127.0.0.1:8000 - -DB_CONNECTION=sqlite -DB_DATABASE=db.sqlite -QUEUE_DRIVER=sync - -ADMIN_AUTH_USE_STUB=true - - -SC_DATA_VERSION=9.99.9 -SC_DATA_PATH=framework/testing/scunpacked \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..4817788e1 --- /dev/null +++ b/.env.example @@ -0,0 +1,69 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +FORTIFY_ALLOW_REGISTRATION=true + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=api +DB_USERNAME=root +DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +DEEPL_AUTH_KEY= + +VITE_APP_NAME="${APP_NAME}" diff --git a/.gitattributes b/.gitattributes index a8763f8ef..fcb21d396 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,11 @@ -* text=auto -*.css linguist-vendored -*.scss linguist-vendored +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/.gitignore b/.gitignore index 9c1406b61..01bf21c45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,31 @@ +*.log +.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.fleet +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json /node_modules +/public/build +/public/hot /public/storage /storage/*.key +/storage/pail +/storage/app/api/scunpacked-data/labels.json +/storage/app/api/ScToolBoxLocales/chinese_(simplified)/global.ini /vendor -/.idea Homestead.json Homestead.yaml -.env -.env.example -composer.phar -/Vagrantfile -/_ide_helper.php -/.phpstorm.meta.php -/.phpunit.result.cache -/packages -/.php_cs.cache -./.php-cs-fixer.cache -./*.cache \ No newline at end of file +Thumbs.db +/v2 +CLAUDE.md +.junie +.claude +AGENTS.md diff --git a/.gitmodules b/.gitmodules index 50f17df67..ed0184147 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "storage/app/api/scunpacked-data"] - path = storage/app/api/scunpacked-data - url = https://github.com/StarCitizenWiki/scunpacked-data.git -[submodule "storage/app/api/ScToolBoxLocales"] - path = storage/app/api/ScToolBoxLocales - url = https://github.com/StarCitizenToolBox/LocalizationData +[submodule "storage/app/api/StarCitizenDeutsch"] + path = storage/app/api/StarCitizenDeutsch + url = https://github.com/rjcncpt/StarCitizen-Deutsch-INI diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..8c6715a15 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "laravel-boost": { + "command": "php", + "args": [ + "artisan", + "boost:mcp" + ] + } + } +} \ No newline at end of file diff --git a/.phpcs.xml b/.phpcs.xml deleted file mode 100644 index 79393d441..000000000 --- a/.phpcs.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - PSR12 - minor customizations - - - - - - - - - - - - - - - - - - ./app - - - \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index bf530a02e..9aeaa717e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,28 @@ -### Stage 1: build PHP extensions on PHP 8.4 -FROM php:8.4-apache AS extensions +# Stage 0: base with PHP extensions (runtime-safe) +FROM php:8.5-apache AS base +WORKDIR /var/www/html -LABEL stage=intermediate +# helper to install extensions + dependencies correctly +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/install-php-extensions RUN set -eux; \ apt-get update; \ - apt-get install -y --no-install-recommends \ - libicu-dev \ - zlib1g-dev \ - libzip-dev \ - libpng-dev \ - libjpeg62-turbo-dev \ - libwebp-dev \ - libfreetype6-dev \ - libgmp-dev \ - libpq-dev \ - libxml2-dev \ - libonig-dev; \ - rm -rf /var/lib/apt/lists/* - -RUN set -eux; \ - docker-php-ext-install -j"$(nproc)" bcmath gmp intl opcache pdo_mysql zip - -RUN set -eux; \ - docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \ - docker-php-ext-install -j"$(nproc)" gd + apt-get install -y --no-install-recommends ffmpeg; \ + rm -rf /var/lib/apt/lists/*; \ + install-php-extensions \ + bcmath \ + gmp \ + intl \ + pdo_pgsql \ + zip \ + gd \ + mbstring \ + curl \ + dom \ + xml; \ + a2enmod rewrite +# OPcache is built-in on PHP 8.5; just configure it. RUN set -eux; \ { \ echo 'opcache.enable=1'; \ @@ -33,72 +30,41 @@ RUN set -eux; \ echo 'opcache.interned_strings_buffer=16'; \ echo 'opcache.max_accelerated_files=16000'; \ echo 'opcache.validate_timestamps=0'; \ - echo 'opcache.load_comments=Off'; \ echo 'opcache.save_comments=1'; \ - echo 'opcache.fast_shutdown=0'; \ - } > /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini + } > /usr/local/etc/php/conf.d/docker-opcache.ini; \ + echo 'memory_limit = 1G' > /usr/local/etc/php/conf.d/docker-php-memlimit.ini; \ + echo 'max_execution_time = 60' > /usr/local/etc/php/conf.d/docker-php-executiontime.ini -### Stage 2: composer install on PHP 8.4 -FROM php:8.4-apache AS api +COPY ./docker/vhost.conf /etc/apache2/sites-available/000-default.conf -LABEL stage=intermediate -WORKDIR /api +# Stage 1: composer deps +FROM base AS vendor RUN set -eux; \ apt-get update; \ - apt-get install -y --no-install-recommends zip unzip git; \ + apt-get install -y --no-install-recommends git unzip zip; \ rm -rf /var/lib/apt/lists/* COPY --from=composer:2 /usr/bin/composer /usr/bin/composer -COPY --chown=www-data:www-data composer.json composer.lock /api/ +COPY --chown=www-data:www-data composer.json composer.lock /var/www/html/ -RUN chown -R www-data:www-data /api && mkdir -p /api/vendor && chown -R www-data:www-data /api/vendor USER www-data - RUN set -eux; \ - composer install --no-dev --ignore-platform-reqs --no-ansi --no-autoloader --no-interaction --no-scripts - -COPY --chown=www-data:www-data / /api - -RUN rm -rf storage/app/api/scunpacked-data storage/app/api/ScToolBoxLocales - -RUN composer dump-autoload --optimize --classmap-authoritative + composer install --no-dev --no-ansi --no-interaction --no-progress --prefer-dist -### Stage 3: final runtime image on PHP 8.4 -FROM php:8.4-apache +COPY --chown=www-data:www-data . /var/www/html +RUN set -eux; \ + composer dump-autoload --optimize --classmap-authoritative -USER root +# Stage 2: final runtime +FROM base AS app WORKDIR /var/www/html -RUN set -eux; \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - ffmpeg \ - libfreetype6 \ - libjpeg62-turbo \ - libwebp7 \ - libpng16-16 \ - libzip5; \ - rm -rf /var/lib/apt/lists/* - -COPY --chown=www-data:www-data --from=api /api /var/www/html -COPY --from=extensions /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/ -COPY --from=extensions /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/ +COPY --from=vendor --chown=www-data:www-data /var/www/html /var/www/html -COPY ./docker/vhost.conf /etc/apache2/sites-available/000-default.conf COPY --chown=www-data:www-data --chmod=770 ./docker/start.sh /usr/local/bin/start -COPY --chown=www-data:www-data --chmod=770 ./docker/schedule.sh /usr/local/bin/schedule - -RUN set -eux; \ - echo 'memory_limit = 1G' > /usr/local/etc/php/conf.d/docker-php-memlimit.ini; \ - echo 'max_execution_time = 60' > /usr/local/etc/php/conf.d/docker-php-executiontime.ini; \ - a2enmod rewrite USER www-data -RUN set -eux; \ - php artisan storage:link; \ - php artisan optimize - CMD ["/usr/local/bin/start"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 52b6789ea..000000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2016 Star Citizen Wiki - -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. \ No newline at end of file diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php new file mode 100644 index 000000000..7bf18d0a4 --- /dev/null +++ b/app/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,40 @@ + $input + */ + public function create(array $input): User + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique(User::class), + ], + 'password' => $this->passwordRules(), + ])->validate(); + + return User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => Hash::make($input['password']), + ]); + } +} diff --git a/app/Actions/Fortify/PasswordValidationRules.php b/app/Actions/Fortify/PasswordValidationRules.php new file mode 100644 index 000000000..76b19d330 --- /dev/null +++ b/app/Actions/Fortify/PasswordValidationRules.php @@ -0,0 +1,18 @@ +|string> + */ + protected function passwordRules(): array + { + return ['required', 'string', Password::default(), 'confirmed']; + } +} diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php new file mode 100644 index 000000000..7a57c5037 --- /dev/null +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -0,0 +1,29 @@ + $input + */ + public function reset(User $user, array $input): void + { + Validator::make($input, [ + 'password' => $this->passwordRules(), + ])->validate(); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/app/Actions/Fortify/UpdateUserPassword.php b/app/Actions/Fortify/UpdateUserPassword.php new file mode 100644 index 000000000..700563905 --- /dev/null +++ b/app/Actions/Fortify/UpdateUserPassword.php @@ -0,0 +1,32 @@ + $input + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'current_password' => ['required', 'string', 'current_password:web'], + 'password' => $this->passwordRules(), + ], [ + 'current_password.current_password' => __('The provided password does not match your current password.'), + ])->validateWithBag('updatePassword'); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/app/Actions/Fortify/UpdateUserProfileInformation.php b/app/Actions/Fortify/UpdateUserProfileInformation.php new file mode 100644 index 000000000..0930ddf38 --- /dev/null +++ b/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -0,0 +1,58 @@ + $input + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users')->ignore($user->id), + ], + ])->validateWithBag('updateProfileInformation'); + + if ($input['email'] !== $user->email && + $user instanceof MustVerifyEmail) { + $this->updateVerifiedUser($user, $input); + } else { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + ])->save(); + } + } + + /** + * Update the given verified user's profile information. + * + * @param array $input + */ + protected function updateVerifiedUser(User $user, array $input): void + { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + 'email_verified_at' => null, + ])->save(); + + $user->sendEmailVerificationNotification(); + } +} diff --git a/app/Console/Commands/AbstractQueueCommand.php b/app/Console/Commands/AbstractQueueCommand.php deleted file mode 100644 index f9e0215a1..000000000 --- a/app/Console/Commands/AbstractQueueCommand.php +++ /dev/null @@ -1,81 +0,0 @@ -output === null) { - return; - } - - parent::info($string, $verbosity); - } - - /** - * {@inheritDoc} - */ - public function error($string, $verbosity = null): void - { - if ($this->output === null) { - return; - } - - parent::error($string, $verbosity); - } - - /** - * Creates a progressbar if output is not null - * - * @param int $size Progressbar size - */ - public function createProgressBar(int $size): void - { - if ($this->output === null) { - return; - } - - $this->bar = $this->output->createProgressBar($size); - } - - /** - * Advances the bar - */ - public function advanceBar(): void - { - if ($this->output === null || $this->bar === null) { - return; - } - - $this->bar->advance(); - } - - /** - * Finishes the bar - */ - public function finishBar(): void - { - if ($this->output === null || $this->bar === null) { - return; - } - - $this->bar->finish(); - } -} diff --git a/app/Console/Commands/AddUser.php b/app/Console/Commands/AddUser.php new file mode 100644 index 000000000..f23951970 --- /dev/null +++ b/app/Console/Commands/AddUser.php @@ -0,0 +1,132 @@ +argument('name'); + $email = (string) $this->argument('email'); + $passwordValue = $this->resolvePassword(); + + $validator = Validator::make([ + 'name' => $name, + 'email' => $email, + 'password' => $passwordValue, + ], [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique(User::class), + ], + 'password' => ['required', 'string', Password::default()], + ]); + + if ($validator->fails()) { + foreach ($validator->errors()->all() as $message) { + $this->error($message); + } + + return self::FAILURE; + } + + $user = new User; + $user->name = $name; + $user->email = $email; + $user->password = Hash::make($passwordValue); + $user->is_admin = (bool) $this->option('admin'); + $user->language_id = $this->resolveLanguageId(); + $user->save(); + + $this->info(sprintf('User "%s" created.', $user->email)); + + return self::SUCCESS; + } + + /** + * Prompt for missing input arguments using the returned questions. + * + * @return array + */ + protected function promptForMissingArgumentsUsing(): array + { + return [ + 'name' => fn (): string => text(label: 'Name', required: true), + 'email' => fn (): string => text(label: 'Email address', required: true), + ]; + } + + private function resolvePassword(): string + { + $passwordValue = $this->option('password'); + + if (is_string($passwordValue) && $passwordValue !== '') { + return $passwordValue; + } + + $passwordValue = password(label: 'Password', required: true); + $confirmation = password(label: 'Confirm password', required: true); + + while ($confirmation !== $passwordValue) { + $this->error('Passwords do not match. Please try again.'); + + $passwordValue = password(label: 'Password', required: true); + $confirmation = password(label: 'Confirm password', required: true); + } + + return $passwordValue; + } + + private function resolveLanguageId(): int + { + $languageCode = (string) config('language.english', Language::ENGLISH); + $language = Language::query()->where('code', $languageCode)->first(); + + if ($language === null) { + $language = new Language; + $language->code = $languageCode; + $language->save(); + } + + return (int) $language->getKey(); + } +} diff --git a/app/Console/Commands/CommLink/BackfillImageHashes.php b/app/Console/Commands/CommLink/BackfillImageHashes.php new file mode 100644 index 000000000..b88c86d1c --- /dev/null +++ b/app/Console/Commands/CommLink/BackfillImageHashes.php @@ -0,0 +1,65 @@ +option('chunk')); + $queue = (string) $this->option('queue'); + $includeAll = (bool) $this->option('all'); + + $query = Image::query() + ->where(function (Builder $query) { + $query->whereRelation('metadata', 'mime', 'LIKE', 'video%') + ->orWhereRelation('metadata', 'mime', 'LIKE', 'image%'); + }) + ->where('src', 'NOT LIKE', '%.svg') + ->where('src', 'NOT LIKE', '%.tiff') + ->select('id'); + + if (! $includeAll) { + $query->whereDoesntHave('hash'); + } + + $dispatched = 0; + + $query->where('id', '>', 43482)->orderByDesc('id')->chunkById($chunkSize, function ($images) use (&$dispatched, $queue): void { + foreach ($images as $image) { + ComputeImageHash::dispatch($image->id) + ->onConnection('database') + ->onQueue($queue); + $dispatched++; + } + }); + + $this->info(sprintf('Dispatched %d hashing jobs to queue "%s".', $dispatched, $queue)); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/CommLink/ComputeSimilarImageIds.php b/app/Console/Commands/CommLink/ComputeSimilarImageIds.php new file mode 100644 index 000000000..06df28dcc --- /dev/null +++ b/app/Console/Commands/CommLink/ComputeSimilarImageIds.php @@ -0,0 +1,65 @@ +option('recent'); + + $query = Image::query() + ->select('id', 'base_image_id', 'created_at') + ->whereNull('base_image_id') + ->whereHas('hash', function ($q): void { + $q->whereNotNull('pdq_hash'); + }); + + if ($recentOnly) { + $query->where('created_at', '>=', now()->subWeek()); + } + + $dispatched = 0; + + $query->orderBy('created_at')->chunk(25, function ($images) use (&$dispatched): void { + foreach ($images as $image) { + // Refresh to check if another job already set base_image_id + $image->refresh(); + + if ($image->base_image_id !== null) { + continue; + } + + ComputeSimilarImageIdsJob::dispatch($image->id) + ->onConnection('database') + ->onQueue('comm-link-images'); + $dispatched++; + } + }); + + $this->info(sprintf('Dispatched %d similarity computation jobs to queue "comm-link-images".', $dispatched)); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/CommLink/DownloadCommLinks.php b/app/Console/Commands/CommLink/DownloadCommLinks.php new file mode 100644 index 000000000..c25b43f8a --- /dev/null +++ b/app/Console/Commands/CommLink/DownloadCommLinks.php @@ -0,0 +1,63 @@ +argument('ids')) + ->filter(static fn ($id) => is_numeric($id)) + ->map(static fn ($id) => (int) $id) + ->filter(static fn (int $id) => $id >= self::FIRST_COMM_LINK_ID) + ->values(); + + if ($ids->isEmpty()) { + $this->error('No valid Comm-Link IDs provided.'); + + return self::FAILURE; + } + + $skipExisting = ! $this->option('overwrite'); + + $ids->each(function (int $id) use ($skipExisting): void { + $this->info(sprintf('Dispatching download for Comm-Link %d', $id)); + dispatch(new DownloadCommLink($id, $skipExisting)); + }); + + if ($this->option('import')) { + $this->info('Dispatching import for recently downloaded Comm-Links.'); + dispatch(new ImportCommLinks(30)); + } + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/CommLink/ImportCommLinks.php b/app/Console/Commands/CommLink/ImportCommLinks.php new file mode 100644 index 000000000..ffa7286d2 --- /dev/null +++ b/app/Console/Commands/CommLink/ImportCommLinks.php @@ -0,0 +1,89 @@ +option('all')) { + dispatch(new ImportCommLinksJob(-1)); + FilterCache::bust(FilterCache::NAMESPACE_COMM_LINKS); + + return self::SUCCESS; + } + + $id = $this->argument('id'); + + if ($id === null || ! is_numeric($id)) { + $this->error('A Comm-Link ID is required unless --all is specified.'); + + return self::FAILURE; + } + + $commLinkId = (int) $id; + + if ($commLinkId < self::FIRST_COMM_LINK_ID) { + $this->error('Comm-Link ID is below the first known ID.'); + + return self::FAILURE; + } + + $files = Storage::disk('comm_links')->files((string) $commLinkId); + + if ($files === []) { + $this->warn('Comm-Link file not found locally, dispatching download.'); + dispatch(new DownloadCommLink($commLinkId, true)); + dispatch(new ImportCommLinksJob(30)); + + return self::SUCCESS; + } + + sort($files); + $file = end($files); + + if ($file === false) { + $this->error('Unable to determine Comm-Link file.'); + + return self::FAILURE; + } + + $basename = Str::afterLast($file, '/'); + dispatch(new ImportCommLink($commLinkId, $basename, (bool) $this->option('force'))); + FilterCache::bust(FilterCache::NAMESPACE_COMM_LINKS); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/CommLink/RedownloadCommLinks.php b/app/Console/Commands/CommLink/RedownloadCommLinks.php new file mode 100644 index 000000000..fe163a41a --- /dev/null +++ b/app/Console/Commands/CommLink/RedownloadCommLinks.php @@ -0,0 +1,39 @@ +option('skip'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + $skipExisting = $skip ?? true; + + ReDownloadDbCommLinks::withChain([ + new ImportCommLinks(-1), + ]) + ->dispatch($skipExisting); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/CommLink/ScheduleCommLinks.php b/app/Console/Commands/CommLink/ScheduleCommLinks.php new file mode 100644 index 000000000..6260593a5 --- /dev/null +++ b/app/Console/Commands/CommLink/ScheduleCommLinks.php @@ -0,0 +1,35 @@ +dispatch(); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/CommLink/TranslateCommLinks.php b/app/Console/Commands/CommLink/TranslateCommLinks.php new file mode 100644 index 000000000..f9b206776 --- /dev/null +++ b/app/Console/Commands/CommLink/TranslateCommLinks.php @@ -0,0 +1,24 @@ +info('Dispatching Comm-Link Translation'); + + TranslateCommLinksJob::dispatch(); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/Game/AddGameVersion.php b/app/Console/Commands/Game/AddGameVersion.php new file mode 100644 index 000000000..c3930d448 --- /dev/null +++ b/app/Console/Commands/Game/AddGameVersion.php @@ -0,0 +1,162 @@ +argument('code'); + + $parsed = $this->parseVersion($rawCode); + + if ($parsed === null) { + $this->error('Invalid game version format. Expected Major.Minor.Patch.SCOPE.Buildnumber with scope LIVE, PTU, or EPTU (e.g. 4.4.0-LIVE.10753606).'); + + return self::FAILURE; + } + + if ($this->versionExists($parsed['code'])) { + $this->error(sprintf('Game version "%s" already exists.', $parsed['code'])); + + return self::FAILURE; + } + + $releasedAt = $this->parseReleaseDate((string) $this->option('released-at')); + + if ($releasedAt === false) { + $this->error('Invalid released-at value. Use a parseable date/time like "2025-12-06" or "2025-12-06 15:30".'); + + return self::FAILURE; + } + + $setDefault = (bool) $this->option('default'); + + DB::transaction(function () use ($parsed, $releasedAt, $setDefault): void { + if ($setDefault) { + GameVersion::query() + ->where('is_default', true) + ->update(['is_default' => false]); + } + + GameVersion::query()->create([ + 'code' => $parsed['code'], + 'channel' => $parsed['scope'], + 'released_at' => $releasedAt, + 'is_default' => $setDefault, + ]); + }); + + $this->info(sprintf('Game version "%s" created.', $parsed['code'])); + + return self::SUCCESS; + } + + /** + * Prompt for missing input arguments using the returned questions. + * + * @return array + */ + protected function promptForMissingArgumentsUsing(): array + { + return [ + 'code' => function (): string { + return text( + label: 'Enter game version (Major.Minor.Patch.SCOPE.Buildnumber)', + placeholder: '4.4.0-LIVE.10753606', + validate: function (string $value): ?string { + return $this->parseVersion($value) !== null + ? null + : 'Format must be Major.Minor.Patch-SCOPE.Buildnumber with scope LIVE, PTU, or EPTU (e.g. 4.4.0-LIVE.10753606).'; + } + ); + }, + ]; + } + + /** + * @return array{code: string, scope: string}|null + */ + private function parseVersion(string $code): ?array + { + $pattern = '/^(?\d+)\.(?\d+)\.(?\d+)-(?[a-z]+)\.(?\d+)$/i'; + + if (! preg_match($pattern, trim($code), $matches)) { + return null; + } + + if (! $this->isAllowedScope($matches['scope'])) { + return null; + } + + return [ + 'code' => sprintf( + '%s.%s.%s-%s.%s', + $matches['major'], + $matches['minor'], + $matches['patch'], + Str::upper($matches['scope']), + $matches['build'] + ), + 'scope' => Str::lower($matches['scope']), + ]; + } + + private function versionExists(string $code): bool + { + return GameVersion::query() + ->whereRaw('LOWER(code) = ?', [Str::lower($code)]) + ->exists(); + } + + private function isAllowedScope(string $scope): bool + { + $allowed = ['LIVE', 'PTU', 'EPTU']; + + return in_array(Str::upper($scope), $allowed, true); + } + + private function parseReleaseDate(string $releasedAt): Carbon|false|null + { + if ($releasedAt === '' || $releasedAt === null) { + return null; + } + + try { + return Carbon::parse($releasedAt); + } catch (\Throwable) { + return false; + } + } +} diff --git a/app/Console/Commands/Game/BackfillShipmatrixIds.php b/app/Console/Commands/Game/BackfillShipmatrixIds.php new file mode 100644 index 000000000..544017a05 --- /dev/null +++ b/app/Console/Commands/Game/BackfillShipmatrixIds.php @@ -0,0 +1,100 @@ +whereNull('shipmatrix_id') + ->with(['manufacturer', 'gameVersion', 'vehicle']); + + // Filter by game version if specified + if ($version = $this->option('game-version')) { + $query->whereHas('gameVersion', fn ($q) => $q->where('code', $version)); + } + + // Apply limit if specified + if ($limit = $this->option('limit')) { + $query->limit((int) $limit); + } + + $vehicles = $query->get(); + + if ($vehicles->isEmpty()) { + $this->info('No unmatched vehicles found!'); + + return self::SUCCESS; + } + + $this->info("Found {$vehicles->count()} unmatched vehicles"); + + $matched = 0; + $failed = 0; + $updates = []; // Collect for batch update + + foreach ($vehicles as $vehicle) { + // Build payload for matching + $payload = [ + 'UUID' => $vehicle->vehicle->uuid ?? null, + 'Name' => $vehicle->name, + 'ClassName' => $vehicle->class_name, + 'Manufacturer' => [ + 'Code' => $vehicle->manufacturer?->name_short, + 'Name' => $vehicle->manufacturer?->name, + ], + ]; + + $shipmatrixId = $this->matcher->findMatch($payload); + + if ($shipmatrixId !== null) { + $updates[$vehicle->id] = $shipmatrixId; + $matched++; + $this->line("✓ Matched: {$vehicle->name}"); + } else { + $failed++; + $this->line("✗ Failed: {$vehicle->name}"); + } + } + + // Batch update all matched records + if (! $this->option('dry-run') && ! empty($updates)) { + foreach ($updates as $id => $shipmatrixId) { + VehicleData::where('id', $id)->update(['shipmatrix_id' => $shipmatrixId]); + } + } + + $this->newLine(); + $this->info("Summary: {$matched} matched, {$failed} failed"); + + if ($this->option('dry-run')) { + $this->warn('DRY RUN - No changes made'); + } + + if ($failed > 0) { + $this->warn("Run 'php artisan game:review-vehicle-matches' to manually match failed vehicles"); + } + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/Game/ComputeItemBaseIds.php b/app/Console/Commands/Game/ComputeItemBaseIds.php new file mode 100644 index 000000000..facb103fd --- /dev/null +++ b/app/Console/Commands/Game/ComputeItemBaseIds.php @@ -0,0 +1,62 @@ +option('game-version'); + + $gameVersion = GameVersion::query() + ->requestedOrDefault(is_string($versionCode) && $versionCode !== '' ? $versionCode : null) + ->first(); + + if ($gameVersion === null) { + if (is_string($versionCode) && $versionCode !== '') { + $this->error(sprintf('Game version "%s" does not exist.', $versionCode)); + } else { + $this->error('No default game version exists.'); + } + + return self::FAILURE; + } + + $dryRun = (bool) $this->option('dry-run'); + + ComputeItemBaseIdsJob::dispatch($gameVersion->id, $dryRun); + + $message = $dryRun + ? 'Dispatched dry-run item base id compute job for version %s.' + : 'Dispatched item base id compute job for version %s.'; + + $this->info(sprintf($message, $gameVersion->code)); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/Game/ImportEntityTags.php b/app/Console/Commands/Game/ImportEntityTags.php new file mode 100644 index 000000000..e9b323e93 --- /dev/null +++ b/app/Console/Commands/Game/ImportEntityTags.php @@ -0,0 +1,126 @@ +option('path'); + + if (Storage::disk('scunpacked')->missing($path)) { + $this->error(sprintf('%s not found in scunpacked storage.', $path)); + + return self::FAILURE; + } + + try { + $contents = Storage::disk('scunpacked')->get($path); + $payload = json_decode($contents, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + $this->error(sprintf('Failed to decode %s: %s', $path, $exception->getMessage())); + + return self::FAILURE; + } + + if (! is_array($payload)) { + $this->error(sprintf('%s must contain an object of tags.', $path)); + + return self::FAILURE; + } + + if ($payload === []) { + $this->warn('No tags found in file.'); + + return self::SUCCESS; + } + + $now = now(); + $skipped = 0; + + $tags = collect($payload) + ->filter(static function ($name, $uuid) use (&$skipped): bool { + if (! is_string($uuid) || trim($uuid) === '') { + $skipped++; + + return false; + } + + if (! is_string($name) || trim($name) === '') { + $skipped++; + + return false; + } + + return true; + }) + ->map(function (string $name, string $uuid) use ($now): array { + return [ + 'uuid' => trim($uuid), + 'name' => trim($name), + 'created_at' => $now, + 'updated_at' => $now, + ]; + }); + + if ($tags->isEmpty()) { + $this->warn('No valid tag records found to import.'); + + return self::SUCCESS; + } + + $totalCreated = 0; + $totalUpdated = 0; + + foreach ($tags->chunk(10000) as $batch) { + $batchUuids = $batch->pluck('uuid')->all(); + + $existing = EntityTag::query() + ->whereIn('uuid', $batchUuids) + ->pluck('uuid') + ->all(); + + EntityTag::query()->upsert( + $batch->values()->all(), + ['uuid'], + ['name', 'updated_at'] + ); + + $batchCreated = count(array_diff($batchUuids, $existing)); + $totalCreated += $batchCreated; + $totalUpdated += $batch->count() - $batchCreated; + } + + $this->info(sprintf( + 'Imported %d entity tags (%d new, %d updated). Skipped %d invalid.', + $tags->count(), + $totalCreated, + $totalUpdated, + $skipped + )); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/Game/ImportItems.php b/app/Console/Commands/Game/ImportItems.php new file mode 100644 index 000000000..ea1233c44 --- /dev/null +++ b/app/Console/Commands/Game/ImportItems.php @@ -0,0 +1,100 @@ +argument('version'); + + $gameVersion = GameVersion::query() + ->where('code', $versionCode) + ->first(); + + if ($gameVersion === null) { + $this->error(sprintf('Game version "%s" does not exist. Please create it first.', $versionCode)); + + return self::FAILURE; + } + + $itemFiles = collect(Storage::disk('scunpacked')->files('items')) + ->filter(static fn (string $path): bool => str_ends_with($path, '.json')) + ->values(); + + if ($itemFiles->isEmpty()) { + $this->warn('No item files found in storage/app/api/scunpacked-data/items.'); + + return self::SUCCESS; + } + + $this->dispatchJobs($itemFiles, $gameVersion->id); + FilterCache::bust(FilterCache::NAMESPACE_ITEMS); + + $this->info(sprintf( + 'Dispatched %d item import jobs for version %s.', + $itemFiles->count(), + $gameVersion->code + )); + + return self::SUCCESS; + } + + protected function promptForMissingArgumentsUsing(): array + { + return [ + 'version' => function (): string { + $options = GameVersion::query() + ->orderByDesc('released_at') + ->orderBy('code') + ->pluck('code', 'code') + ->toArray(); + + if ($options === []) { + $this->error('No game versions exist. Please create one before importing.'); + + return ''; + } + + return select( + label: 'Select game version to import', + options: $options + ); + }, + ]; + } + + private function dispatchJobs(Collection $itemFiles, int $gameVersionId): void + { + $itemFiles->each(static function (string $path) use ($gameVersionId): void { + ImportItemData::dispatch($gameVersionId, $path); + }); + } +} diff --git a/app/Console/Commands/Game/ImportManufacturers.php b/app/Console/Commands/Game/ImportManufacturers.php new file mode 100644 index 000000000..3b9454079 --- /dev/null +++ b/app/Console/Commands/Game/ImportManufacturers.php @@ -0,0 +1,120 @@ +option('path'); + + if (Storage::disk('scunpacked')->missing($path)) { + $this->error(sprintf('%s not found in scunpacked storage.', $path)); + + return self::FAILURE; + } + + try { + $contents = Storage::disk('scunpacked')->get($path); + $payload = json_decode($contents, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + $this->error(sprintf('Failed to decode %s: %s', $path, $exception->getMessage())); + + return self::FAILURE; + } + + if (! is_array($payload)) { + $this->error(sprintf('%s must contain an array of manufacturers.', $path)); + + return self::FAILURE; + } + + $now = now(); + $skipped = 0; + + $manufacturers = collect($payload) + ->filter(static function ($manufacturer) use (&$skipped): bool { + if (! is_array($manufacturer)) { + $skipped++; + + return false; + } + + $hasRequiredKeys = isset($manufacturer['reference'], $manufacturer['name'], $manufacturer['code']); + + if (! $hasRequiredKeys) { + $skipped++; + } + + return $hasRequiredKeys; + }) + ->keyBy(fn (array $manufacturer): string => (string) $manufacturer['reference']) + ->map(function (array $manufacturer) use ($now): array { + return [ + 'uuid' => (string) $manufacturer['reference'], + 'name' => (string) $manufacturer['name'], + 'code' => (string) $manufacturer['code'], + 'created_at' => $now, + 'updated_at' => $now, + ]; + }); + + if ($manufacturers->isEmpty()) { + $this->warn('No valid manufacturer records found to import.'); + + return self::SUCCESS; + } + + $uuids = $manufacturers->keys()->all(); + + $existing = Manufacturer::query() + ->whereIn('uuid', $uuids) + ->pluck('uuid') + ->all(); + + Manufacturer::query()->upsert( + $manufacturers->values()->all(), + ['uuid'], + ['name', 'code', 'updated_at'] + ); + + FilterCache::bust(FilterCache::NAMESPACE_ITEMS); + FilterCache::bust(FilterCache::NAMESPACE_VEHICLES); + + $created = count(array_diff($uuids, $existing)); + $updated = $manufacturers->count() - $created; + + $this->info(sprintf( + 'Imported %d manufacturers (%d new, %d updated). Skipped %d invalid.', + $manufacturers->count(), + $created, + $updated, + $skipped + )); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/Game/ImportVehicles.php b/app/Console/Commands/Game/ImportVehicles.php new file mode 100644 index 000000000..0c30a22d1 --- /dev/null +++ b/app/Console/Commands/Game/ImportVehicles.php @@ -0,0 +1,101 @@ +argument('version'); + + $gameVersion = GameVersion::query() + ->where('code', $versionCode) + ->first(); + + if ($gameVersion === null) { + $this->error(sprintf('Game version "%s" does not exist. Please create it first.', $versionCode)); + + return self::FAILURE; + } + + $shipFiles = collect(Storage::disk('scunpacked')->files('ships')) + ->filter(static fn (string $path): bool => str_ends_with($path, '.json')) + ->reject(static fn (string $path): bool => str_ends_with($path, '-raw.json')) + ->values(); + + if ($shipFiles->isEmpty()) { + $this->warn('No ship files found in storage/app/api/scunpacked-data/ships.'); + + return self::SUCCESS; + } + + $this->dispatchJobs($shipFiles, $gameVersion->id); + FilterCache::bust(FilterCache::NAMESPACE_VEHICLES); + + $this->info(sprintf( + 'Dispatched %d vehicle import jobs for version %s.', + $shipFiles->count(), + $gameVersion->code + )); + + return self::SUCCESS; + } + + protected function promptForMissingArgumentsUsing(): array + { + return [ + 'version' => function (): string { + $options = GameVersion::query() + ->orderByDesc('released_at') + ->orderBy('code') + ->pluck('code', 'code') + ->toArray(); + + if ($options === []) { + $this->error('No game versions exist. Please create one before importing.'); + + return ''; + } + + return select( + label: 'Select game version to import', + options: $options + ); + }, + ]; + } + + private function dispatchJobs(Collection $shipFiles, int $gameVersionId): void + { + $shipFiles->each(static function (string $path) use ($gameVersionId): void { + ImportVehicleData::dispatch($gameVersionId, $path); + }); + } +} diff --git a/app/Console/Commands/Game/ReviewVehicleMatches.php b/app/Console/Commands/Game/ReviewVehicleMatches.php new file mode 100644 index 000000000..0f1a6b681 --- /dev/null +++ b/app/Console/Commands/Game/ReviewVehicleMatches.php @@ -0,0 +1,103 @@ +whereNull('shipmatrix_id') + ->with('manufacturer') + ->limit($this->option('limit')) + ->get(); + + if ($unmatched->isEmpty()) { + $this->info('No unmatched vehicles found!'); + + return self::SUCCESS; + } + + $this->info("Found {$unmatched->count()} unmatched vehicles"); + $newOverrides = []; + + foreach ($unmatched as $vehicle) { + $this->newLine(); + $this->line("Game Vehicle: {$vehicle->name}"); + $this->line("Class Name: {$vehicle->class_name}"); + $this->line("Manufacturer: {$vehicle->manufacturer?->name}"); + + $suggestions = $this->findPotentialMatches($vehicle->name); + + if ($suggestions->isEmpty()) { + $this->warn('No potential matches found'); + + continue; + } + + $selectedId = search( + label: 'Select matching ShipMatrix vehicle (or press Ctrl+C to skip):', + options: fn (string $value) => strlen($value) > 0 + ? ShipMatrixVehicle::query() + ->where('name', 'LIKE', "%{$value}%") + ->limit(10) + ->pluck('name', 'id') + ->all() + : $suggestions->pluck('name', 'id')->all(), + placeholder: 'Type to search...', + ); + + if ($selectedId === null) { + continue; + } + + $selected = ShipMatrixVehicle::find($selectedId); + + if (confirm("Confirm: '{$vehicle->name}' → '{$selected->name}'?")) { + $vehicle->update(['shipmatrix_id' => $selected->id]); + + $this->info('✓ Matched!'); + + if (confirm('Add this to config overrides?', default: false)) { + $newOverrides[$vehicle->name] = $selected->name; + } + } + } + + // suggested overrides + if (! empty($newOverrides)) { + $this->newLine(); + $this->line('Add these to config/game.php:'); + $this->line("'vehicle_name_overrides' => ["); + foreach ($newOverrides as $from => $to) { + $this->line(" '{$from}' => '{$to}',"); + } + $this->line('],'); + } + + return self::SUCCESS; + } + + private function findPotentialMatches(string $name): Collection + { + return ShipMatrixVehicle::query() + ->whereRaw('LOWER(name) LIKE ?', ['%'.mb_strtolower($name).'%']) + ->orWhereRaw('LOWER(slug) LIKE ?', ['%'.mb_strtolower(Str::slug($name)).'%']) + ->limit(10) + ->get(); + } +} diff --git a/app/Console/Commands/Game/SyncGameData.php b/app/Console/Commands/Game/SyncGameData.php new file mode 100644 index 000000000..2c8e43fcf --- /dev/null +++ b/app/Console/Commands/Game/SyncGameData.php @@ -0,0 +1,193 @@ + + */ + protected $aliases = ['game:sync']; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Sync game manufacturers, entity tags, and optional data imports.'; + + /** + * Execute the console command. + */ + public function handle(): int + { + $skipItems = (bool) $this->option('skip-items'); + $skipVehicles = (bool) $this->option('skip-vehicles'); + $skipComputeBaseIds = (bool) $this->option('skip-compute-item-base-ids'); + $skipBackfillShipmatrixIds = (bool) $this->option('skip-backfill-shipmatrix-ids'); + + $gameVersion = $this->resolveGameVersion($skipItems, $skipVehicles); + + if ($gameVersion === null && (! $skipItems || ! $skipVehicles)) { + return self::FAILURE; + } + + if (Artisan::call('game:import-manufacturers') !== self::SUCCESS) { + return self::FAILURE; + } + + if (Artisan::call('game:import-tags') !== self::SUCCESS) { + return self::FAILURE; + } + + if (! $skipItems && $gameVersion !== null) { + $this->dispatchItemImports($gameVersion, $skipComputeBaseIds); + } + + if (! $skipVehicles && $gameVersion !== null) { + $this->dispatchVehicleImports($gameVersion, $skipBackfillShipmatrixIds); + } + + return self::SUCCESS; + } + + private function resolveGameVersion(bool $skipItems, bool $skipVehicles): ?GameVersion + { + if ($skipItems && $skipVehicles) { + return null; + } + + $versionCode = $this->option('game-version'); + + if (! is_string($versionCode) || $versionCode === '') { + $options = GameVersion::query() + ->orderByDesc('released_at') + ->orderBy('code') + ->pluck('code', 'code') + ->toArray(); + + if ($options === []) { + $this->error('No game versions exist. Please create one before syncing.'); + + return null; + } + + $versionCode = select( + label: 'Select game version to import', + options: $options + ); + } + + $gameVersion = GameVersion::query() + ->where('code', $versionCode) + ->first(); + + if ($gameVersion === null) { + $this->error(sprintf('Game version "%s" does not exist. Please create it first.', $versionCode)); + } + + return $gameVersion; + } + + private function dispatchItemImports(GameVersion $gameVersion, bool $skipComputeBaseIds): void + { + $itemFiles = collect(Storage::disk('scunpacked')->files('items')) + ->filter(static fn (string $path): bool => Str::endsWith($path, '.json')) + ->values(); + + if ($itemFiles->isEmpty()) { + $this->warn('No item files found in storage/app/api/scunpacked-data/items.'); + + return; + } + + $jobs = $itemFiles->map(static function (string $path) use ($gameVersion): ImportItemData { + return new ImportItemData($gameVersion->id, $path); + }); + + $this->dispatchChunkedBatch($jobs, $skipComputeBaseIds ? null : function () use ($gameVersion): void { + ComputeItemBaseIdsJob::dispatch($gameVersion->id, false); + }); + } + + private function dispatchVehicleImports(GameVersion $gameVersion, bool $skipBackfillShipmatrixIds): void + { + $shipFiles = collect(Storage::disk('scunpacked')->files('ships')) + ->filter(static fn (string $path): bool => Str::endsWith($path, '.json')) + ->reject(static fn (string $path): bool => Str::endsWith($path, '-raw.json')) + ->values(); + + if ($shipFiles->isEmpty()) { + $this->warn('No ship files found in storage/app/api/scunpacked-data/ships.'); + + return; + } + + $jobs = $shipFiles->map(static function (string $path) use ($gameVersion): ImportVehicleData { + return new ImportVehicleData($gameVersion->id, $path); + }); + + $this->dispatchChunkedBatch($jobs, $skipBackfillShipmatrixIds ? null : function () use ($gameVersion): void { + Artisan::call('game:backfill-shipmatrix-ids', [ + '--game-version' => $gameVersion->code, + ]); + }); + } + + /** + * @param \Illuminate\Support\Collection $jobs + */ + private function dispatchChunkedBatch(Collection $jobs, ?Closure $then): void + { + $jobChunks = $jobs->chunk(self::BATCH_SIZE)->values(); + $firstChunk = $jobChunks->shift(); + + if ($firstChunk === null) { + return; + } + + $pendingBatch = Bus::batch($firstChunk->values()); + + if ($then !== null) { + $pendingBatch->then($then); + } + + $batch = $pendingBatch->dispatch(); + + $jobChunks->each(static function (Collection $chunk) use ($batch): void { + $batch->add($chunk->values()); + }); + } +} diff --git a/app/Console/Commands/MigrateData.php b/app/Console/Commands/MigrateData.php new file mode 100644 index 000000000..2244255f0 --- /dev/null +++ b/app/Console/Commands/MigrateData.php @@ -0,0 +1,474 @@ +error('No groups configured in config/data_migration.php'); + + return self::FAILURE; + } + + if ($this->option('list')) { + $this->listGroups($groupsCfg); + + return self::SUCCESS; + } + + $fromName = (string) ($this->option('from') ?: ($cfg['from_connection'] ?? 'legacy_mysql')); + $toName = (string) ($this->option('to') ?: ($cfg['to_connection'] ?? 'pgsql')); + + $from = DB::connection($fromName); + $to = DB::connection($toName); + + $selectedGroups = $this->resolveGroups(array_keys($groupsCfg)); + if (empty($selectedGroups)) { + $this->error('No groups selected. Use --group=Name or --all. Use --list to see options.'); + + return self::FAILURE; + } + + $chunk = max(1, (int) $this->option('chunk')); + $truncate = (bool) $this->option('truncate'); + $force = (bool) $this->option('force'); + $dryRun = (bool) $this->option('dry-run'); + $syncSeq = (bool) $this->option('sync-sequences'); + $noProgress = (bool) $this->option('no-progress'); + + foreach ($selectedGroups as $group) { + $this->info("Group: {$group}"); + + $tables = $groupsCfg[$group] ?? []; + foreach ($tables as $def) { + $tableDef = $this->normalizeTableDef($def); + + try { + $this->migrateTable( + from: $from, + to: $to, + tableDef: $tableDef, + chunk: $chunk, + truncate: $truncate, + force: $force, + dryRun: $dryRun, + syncSequences: $syncSeq, + noProgress: $noProgress, + ); + } catch (Throwable $e) { + $this->error("Failed table {$tableDef['table']}: {$e->getMessage()}"); + + return self::FAILURE; + } + } + } + + return self::SUCCESS; + } + + private function listGroups(array $groupsCfg): void + { + $this->line('Available groups:'); + foreach ($groupsCfg as $group => $tables) { + $this->line("- {$group}"); + foreach ($tables as $t) { + $def = $this->normalizeTableDef($t); + $this->line(" - {$def['table']}".($def['target'] !== $def['table'] ? " -> {$def['target']}" : '')); + } + } + } + + private function resolveGroups(array $allGroupNames): array + { + if ($this->option('all')) { + return $allGroupNames; + } + + $requested = (array) $this->option('group'); + $requested = array_values(array_filter(array_map('trim', $requested))); + + if (empty($requested)) { + return []; + } + + $map = []; + foreach ($allGroupNames as $g) { + $map[Str::lower($g)] = $g; + } + + $resolved = []; + foreach ($requested as $r) { + $key = Str::lower($r); + if (! isset($map[$key])) { + $this->warn("Unknown group '{$r}' (ignored). Use --list to see valid groups."); + + continue; + } + $resolved[] = $map[$key]; + } + + return array_values(array_unique($resolved)); + } + + private function normalizeTableDef(string|array $def): array + { + if (is_string($def)) { + return [ + 'table' => $def, + 'target' => $def, + 'primary_key' => 'id', + 'rename' => [], + 'drop' => [], + 'defaults' => [], + 'post_updates' => [], + ]; + } + + return array_merge( + [ + 'target' => $def['table'] ?? null, + 'primary_key' => 'id', + 'rename' => [], + 'drop' => [], + 'defaults' => [], + 'post_updates' => [], + ], + $def, + ); + } + + private function migrateTable( + ConnectionInterface $from, + ConnectionInterface $to, + array $tableDef, + int $chunk, + bool $truncate, + bool $force, + bool $dryRun, + bool $syncSequences, + bool $noProgress, + ): void { + $sourceTable = $tableDef['table']; + $targetTable = $tableDef['target'] ?? $sourceTable; + $pk = $tableDef['primary_key'] ?: null; + + $this->line(" Table: {$sourceTable} -> {$targetTable}"); + + if (! $truncate && ! $force) { + $hasAnyRows = $to->table($targetTable)->limit(1)->exists(); + if ($hasAnyRows) { + $this->line(' Skipped (destination table already contains data). Use --force or --truncate.'); + + return; + } + } + + $sourceCols = Schema::connection($from->getName())->getColumnListing($sourceTable); + $targetCols = Schema::connection($to->getName())->getColumnListing($targetTable); + + if (empty($sourceCols)) { + $this->warn(" Source table has no columns or does not exist: {$sourceTable}"); + + return; + } + if (empty($targetCols)) { + $this->warn(" Target table has no columns or does not exist: {$targetTable}"); + + return; + } + + $rename = (array) ($tableDef['rename'] ?? []); + $drop = array_fill_keys((array) ($tableDef['drop'] ?? []), true); + $defaults = (array) ($tableDef['defaults'] ?? []); + + $map = []; + foreach ($sourceCols as $src) { + if (isset($drop[$src])) { + continue; + } + + $dst = $rename[$src] ?? $src; + if (in_array($dst, $targetCols, true)) { + $map[$src] = $dst; + } + } + + foreach ($defaults as $dst => $_val) { + if (! in_array($dst, $targetCols, true)) { + $this->warn(" Default column not found on target (ignored): {$dst}"); + unset($defaults[$dst]); + } + } + + if (empty($map) && empty($defaults)) { + $this->warn(' No compatible columns found to migrate (after rename/drop filtering).'); + + return; + } + + if ($truncate && ! $dryRun) { + $this->truncateTargetTable($to, $targetTable); + } + + $query = $from->table($sourceTable); + + $total = null; + if (! $noProgress) { + try { + $total = $query->count(); + } catch (Throwable) { + $total = null; + } + } + + $bar = null; + if (! $noProgress && is_int($total)) { + $bar = $this->output->createProgressBar($total); + $bar->start(); + } + + $insertBatch = function ($rows) use ($to, $targetTable, $map, $defaults, $dryRun, $bar) { + $payload = []; + foreach ($rows as $row) { + $out = []; + + foreach ($map as $src => $dst) { + $val = $row->{$src} ?? null; + $out[$dst] = $this->sanitizeValue($val); + } + + foreach ($defaults as $dst => $val) { + $out[$dst] = $val; + } + + $payload[] = $out; + } + + if (! $dryRun && ! empty($payload)) { + $to->table($targetTable)->insert($payload); + } + + if ($bar) { + $bar->advance(count($rows)); + } + }; + + $useChunkById = $pk && in_array($pk, $sourceCols, true); + + if ($useChunkById) { + $query->orderBy($pk)->chunkById($chunk, $insertBatch, $pk); + } else { + $orderCol = $sourceCols[0]; + $this->warn(" No usable primary key found for chunkById; using offset chunk ordered by '{$orderCol}'."); + $query->orderBy($orderCol)->chunk($chunk, $insertBatch); + } + + if ($bar) { + $bar->finish(); + $this->newLine(); + } + + if (! $dryRun && ! empty($tableDef['post_updates'])) { + $this->applyPostUpdates( + from: $from, + to: $to, + tableDef: $tableDef, + chunk: $chunk, + noProgress: $noProgress, + ); + } + + if ($syncSequences && ! $dryRun) { + $this->syncPostgresSequenceBestEffort($to, $targetTable, $pk ?: 'id'); + } + } + + private function applyPostUpdates( + ConnectionInterface $from, + ConnectionInterface $to, + array $tableDef, + int $chunk, + bool $noProgress, + ): void { + $sourceTable = $tableDef['table']; + $targetTable = $tableDef['target'] ?? $sourceTable; + $pk = $tableDef['primary_key'] ?: 'id'; + + foreach ((array) $tableDef['post_updates'] as $upd) { + $column = (string) ($upd['column'] ?? ''); + $sourceColumn = (string) ($upd['source_column'] ?? $column); + $refTable = (string) ($upd['ref_table'] ?? ''); + $refColumn = (string) ($upd['ref_column'] ?? 'id'); + + if ($column === '' || $sourceColumn === '') { + $this->warn(' post_updates entry missing column/source_column (skipped).'); + + continue; + } + + $this->line(" Post-update: {$targetTable}.{$column} from {$sourceTable}.{$sourceColumn}"); + + $q = $from->table($sourceTable) + ->select([$pk, $sourceColumn]) + ->whereNotNull($sourceColumn) + ->orderBy($pk); + + $total = null; + if (! $noProgress) { + try { + $total = (clone $q)->count(); + } catch (Throwable) { + $total = null; + } + } + + $bar = null; + if (! $noProgress && is_int($total)) { + $bar = $this->output->createProgressBar($total); + $bar->start(); + } + + $q->chunkById($chunk, function ($rows) use ($to, $targetTable, $pk, $column, $sourceColumn, $refTable, $refColumn, $bar) { + $pairs = []; + $refIds = []; + + foreach ($rows as $r) { + $id = $r->{$pk}; + $val = $r->{$sourceColumn}; + + $val = $this->sanitizeValue($val); + + $pairs[] = ['__id' => $id, '__val' => $val]; + if ($val !== null) { + $refIds[] = $val; + } + } + + $refSet = null; + if ($refTable !== '' && ! empty($refIds)) { + $refIds = array_values(array_unique($refIds)); + $existing = $to->table($refTable) + ->whereIn($refColumn, $refIds) + ->pluck($refColumn) + ->all(); + + $refSet = array_fill_keys($existing, true); + } + + $payload = []; + foreach ($pairs as $p) { + $id = $p['__id']; + $val = $p['__val']; + + if ($refSet !== null && $val !== null && ! isset($refSet[$val])) { + $val = null; + } + + $payload[] = [ + $pk => $id, + $column => $val, + ]; + } + + if (! empty($payload)) { + $to->table($targetTable)->upsert($payload, [$pk], [$column]); + } + + if ($bar) { + $bar->advance(count($rows)); + } + }, $pk); + + if ($bar) { + $bar->finish(); + $this->newLine(); + } + } + } + + private function truncateTargetTable(ConnectionInterface $to, string $table): void + { + $wrapped = $to->getQueryGrammar()->wrapTable($table); + $to->statement("TRUNCATE TABLE {$wrapped} RESTART IDENTITY CASCADE"); + } + + private function sanitizeValue(mixed $val): mixed + { + if (is_string($val)) { + if ($val === '0000-00-00' || $val === '0000-00-00 00:00:00') { + return null; + } + } + + return $val; + } + + private function syncPostgresSequenceBestEffort(ConnectionInterface $to, string $table, string $pk): void + { + try { + $wrappedTable = $to->getQueryGrammar()->wrapTable($table); + $wrappedPk = $to->getQueryGrammar()->wrap($pk); + + $sql = " + SELECT setval( + pg_get_serial_sequence(?, ?), + COALESCE((SELECT MAX({$wrappedPk}) FROM {$wrappedTable}), 1), + true + ) + "; + + $to->select($sql, [$table, $pk]); + } catch (Throwable $e) { + $this->warn(" Sequence sync skipped: {$e->getMessage()}"); + } + } +} diff --git a/app/Console/Commands/MigrateTranslations.php b/app/Console/Commands/MigrateTranslations.php new file mode 100644 index 000000000..f6bb69f21 --- /dev/null +++ b/app/Console/Commands/MigrateTranslations.php @@ -0,0 +1,157 @@ +option('from') ?: ($cfg['from_connection'] ?? 'legacy_mysql')); + $toName = (string) ($this->option('to') ?: ($cfg['to_connection'] ?? 'pgsql')); + $chunk = max(1, (int) $this->option('chunk')); + + $from = DB::connection($fromName); + $to = DB::connection($toName); + + $this->info('Migrating translations to JSON columns...'); + + $this->migrateSimple($from, $to, 'shipmatrix_production_statuses', 'production_status_translations', 'production_status_id', $chunk); + $this->migrateSimple($from, $to, 'shipmatrix_production_notes', 'production_note_translations', 'production_note_id', $chunk); + $this->migrateSimple($from, $to, 'shipmatrix_vehicle_sizes', 'vehicle_size_translations', 'size_id', $chunk); + $this->migrateSimple($from, $to, 'shipmatrix_vehicle_types', 'vehicle_type_translations', 'type_id', $chunk); + $this->migrateSimple($from, $to, 'shipmatrix_vehicle_foci', 'vehicle_focus_translations', 'focus_id', $chunk); + $this->migrateSimple($from, $to, 'galactapedia_articles', 'galactapedia_article_translations', 'galactapedia_article_id', $chunk); + $this->migrateSimple($from, $to, 'starmap_starsystems', 'starsystem_translations', 'starsystem_id', $chunk); + $this->migrateSimple($from, $to, 'starmap_celestial_objects', 'celestial_object_translations', 'celestial_object_id', $chunk); + $this->migrateSimple($from, $to, 'shipmatrix_vehicles', 'vehicle_translations', 'vehicle_id', $chunk); + // $this->migrateByKey($from, $to, 'game_items', 'sc_item_translations', 'item_uuid', 'uuid', $chunk); + $this->migrateSimple($from, $to, 'comm_links', 'comm_link_translations', 'comm_link_id', $chunk); + + $this->migrateManufacturer($from, $to, $chunk); + + $this->info('Translation migration complete.'); + + return self::SUCCESS; + } + + private function migrateSimple( + ConnectionInterface $from, + ConnectionInterface $to, + string $parentTable, + string $translationTable, + string $foreignKey, + int $chunk, + ): void { + $this->line("Migrating {$translationTable}..."); + + $to->table($parentTable)->select('id')->orderBy('id')->chunkById( + $chunk, + function ($records) use ($from, $to, $translationTable, $foreignKey, $parentTable) { + foreach ($records as $record) { + $translations = $from->table($translationTable) + ->where($foreignKey, $record->id) + ->get() + ->pluck('translation', 'locale_code') + ->filter(fn ($value) => $value !== null && $value !== '') + ->mapWithKeys(fn ($value, $key) => [substr($key, 0, 2) => $value]) + ->toArray(); + + if ($translations === []) { + continue; + } + + $to->table($parentTable) + ->where('id', $record->id) + ->update(['translation' => json_encode($translations)]); + } + } + ); + } + + private function migrateManufacturer(ConnectionInterface $from, ConnectionInterface $to, int $chunk): void + { + $this->line('Migrating manufacturer_translations...'); + + $to->table('shipmatrix_manufacturers')->select('id')->orderBy('id')->chunkById( + $chunk, + function ($records) use ($from, $to) { + foreach ($records as $record) { + $translations = $from->table('manufacturer_translations') + ->where('manufacturer_id', $record->id) + ->get(); + + $knownFor = []; + $description = []; + + foreach ($translations as $translation) { + if ($translation->known_for !== null && $translation->known_for !== '') { + $knownFor[substr($translation->locale_code, 0, 2)] = $translation->known_for; + } + + if ($translation->description !== null && $translation->description !== '') { + $description[substr($translation->locale_code, 0, 2)] = $translation->description; + } + } + + $to->table('shipmatrix_manufacturers') + ->where('id', $record->id) + ->update([ + 'known_for' => $knownFor !== [] ? json_encode($knownFor) : null, + 'description' => $description !== [] ? json_encode($description) : null, + ]); + } + } + ); + } + + private function migrateByKey( + ConnectionInterface $from, + ConnectionInterface $to, + string $parentTable, + string $translationTable, + string $translationKey, + string $parentKey, + int $chunk, + ): void { + $this->line("Migrating {$translationTable}..."); + + $to->table($parentTable)->select($parentKey)->orderBy($parentKey)->chunk( + $chunk, + function ($records) use ($from, $to, $translationTable, $translationKey, $parentTable, $parentKey) { + foreach ($records as $record) { + $value = $record->{$parentKey}; + + $translations = $from->table($translationTable) + ->where($translationKey, $value) + ->get() + ->pluck('translation', 'locale_code') + ->filter(fn ($translation) => $translation !== null && $translation !== '') + ->mapWithKeys(fn ($translation, $locale) => [substr($locale, 0, 2) => $translation]) + ->toArray(); + + if ($translations === []) { + continue; + } + + $to->table($parentTable) + ->where($parentKey, $value) + ->update(['translation' => json_encode($translations)]); + } + } + ); + } +} diff --git a/app/Console/Commands/PopulateData.php b/app/Console/Commands/PopulateData.php deleted file mode 100644 index a57166136..000000000 --- a/app/Console/Commands/PopulateData.php +++ /dev/null @@ -1,69 +0,0 @@ -createProgressBar(6); - - if ($this->option('seed')) { - Artisan::call('db:seed'); - } - $this->bar->advance(); - - if (! $this->option('skipCommLinks')) { - $this->info('Downloading and importing all missing Comm-Links.'); - Artisan::call('comm-links:schedule'); - } - $this->bar->advance(); - - if (! $this->option('skipGalactapedia')) { - $this->info('Downloading and importing all Galactapedia articles.'); - Artisan::call('galactapedia:import-categories'); - Artisan::call('galactapedia:import-articles'); - Artisan::call('galactapedia:import-properties'); - } - $this->bar->advance(); - - Artisan::call('ship-matrix:download --import'); - $this->bar->advance(); - - if (! $this->option('skipStarmap')) { - $this->info('Downloading and importing starmap.'); - Artisan::call('starmap:download --import'); - } - $this->bar->advance(); - - if (! $this->option('skipScUnpacked')) { - $this->info('Importing all Star Citizen Items.'); - Artisan::call('sc:import-items'); - } - $this->bar->finish(); - - return Command::SUCCESS; - } -} diff --git a/app/Console/Commands/Rsi/CommLink/AbstractCommLinkCommand.php b/app/Console/Commands/Rsi/CommLink/AbstractCommLinkCommand.php deleted file mode 100644 index 84ea58f00..000000000 --- a/app/Console/Commands/Rsi/CommLink/AbstractCommLinkCommand.php +++ /dev/null @@ -1,33 +0,0 @@ -argument('offset'); - - if ($offset <= 0) { - return self::FIRST_COMM_LINK_ID; - } - - if ($offset > 0 && $offset < self::FIRST_COMM_LINK_ID) { - $offset = self::FIRST_COMM_LINK_ID + $offset; - } - - return $offset; - } -} diff --git a/app/Console/Commands/Rsi/CommLink/CommLinkSchedule.php b/app/Console/Commands/Rsi/CommLink/CommLinkSchedule.php deleted file mode 100644 index c65989a2f..000000000 --- a/app/Console/Commands/Rsi/CommLink/CommLinkSchedule.php +++ /dev/null @@ -1,42 +0,0 @@ -chain( - [ - new ImportCommLinks(30), - ] - ); - - return Command::SUCCESS; - } -} diff --git a/app/Console/Commands/Rsi/CommLink/Download/DownloadCommLink.php b/app/Console/Commands/Rsi/CommLink/Download/DownloadCommLink.php deleted file mode 100644 index e69148838..000000000 --- a/app/Console/Commands/Rsi/CommLink/Download/DownloadCommLink.php +++ /dev/null @@ -1,102 +0,0 @@ -dispatcher = $dispatcher; - } - - /** - * Execute the console command. - */ - public function handle(): int - { - collect($this->argument('id'))->filter( - static function ($id) { - return is_numeric($id); - } - ) - ->filter( - static function ($id) { - return (int) $id >= self::FIRST_COMM_LINK_ID; - } - ) - ->tap( - function (Collection $collection) { - $this->createProgressBar($collection->count()); - } - ) - ->each( - function (int $id) { - $skipExisting = true; - - if ($this->option('overwrite') === true) { - $skipExisting = false; - } - - $this->info('Downloading specified Comm-Links'); - $this->dispatcher->dispatch(new DownloadCommLinkJob($id, $skipExisting)); - $this->advanceBar(); - } - ); - - $this->finishBar(); - - if ($this->option('import') === true) { - $this->dispatchImportJob(); - } - - return CommLinkCommand::SUCCESS; - } - - /** - * Import jobs to run after downloading comm link files - */ - private function dispatchImportJob(): void - { - $this->info("\nImporting Comm-Links"); - $this->dispatcher->dispatch(new ImportCommLinks(30)); - $this->dispatcher->dispatch(new CreateImageMetadata); - $this->dispatcher->dispatch(new CreateImageHashes); - } -} diff --git a/app/Console/Commands/Rsi/CommLink/Download/DownloadCommLinkImages.php b/app/Console/Commands/Rsi/CommLink/Download/DownloadCommLinkImages.php deleted file mode 100644 index d8e4c8f28..000000000 --- a/app/Console/Commands/Rsi/CommLink/Download/DownloadCommLinkImages.php +++ /dev/null @@ -1,42 +0,0 @@ -option('skip'); - if ($skip === true || $skip === 'true' || $skip === '1') { - $skip = true; - } else { - $skip = false; - } - - ReDownloadDbCommLinks::withChain( - [ - new ImportCommLinks(-1), - ] - )->onQueue('redownload_comm_links')->dispatch($skip); - - return Command::SUCCESS; - } -} diff --git a/app/Console/Commands/Rsi/CommLink/Image/ComputeSimilarImageIds.php b/app/Console/Commands/Rsi/CommLink/Image/ComputeSimilarImageIds.php deleted file mode 100644 index 0ad19f3a9..000000000 --- a/app/Console/Commands/Rsi/CommLink/Image/ComputeSimilarImageIds.php +++ /dev/null @@ -1,57 +0,0 @@ -whereNull('base_image_id') - //->whereRelation('metadata', 'size', '>=', 250 * 1024) - ->with([ - 'metadata' => fn ($query) => $query->orderBy('size', 'DESC'), - ]); - - if ($this->option('recent') === true) { - $images->where('created_at', '>=', Carbon::now()->subWeek()); - } - - $images->orderBy('created_at') - ->chunk(25, function (Collection $images) { - $images->each(function (Image $image) { - $image->refresh(); - - if ($image->base_image_id !== null) { - return; - } - - \App\Jobs\Rsi\CommLink\Image\ComputeSimilarImageIds::dispatch($image)->onQueue('comm_link_images'); - }); - }); - - return Command::SUCCESS; - } -} diff --git a/app/Console/Commands/Rsi/CommLink/Image/CreateImageHashes.php b/app/Console/Commands/Rsi/CommLink/Image/CreateImageHashes.php deleted file mode 100644 index aaae35a73..000000000 --- a/app/Console/Commands/Rsi/CommLink/Image/CreateImageHashes.php +++ /dev/null @@ -1,73 +0,0 @@ -info('Starting calculation of image hashes'); - - $images = $this->getImages(); - - $this->createProgressBar($images->count()); - - $images->chunk( - 100, - function (Collection $images) { - $images->each( - function (Image $image) { - CreateImageHash::dispatch($image)->onQueue('comm_link_images'); - $this->advanceBar(); - } - ); - } - ); - - $this->finishBar(); - - return QueueCommand::SUCCESS; - } - - /** - * The images to create hashes for - * Image needs to have an attached comm link and metadata - */ - private function getImages(): Builder - { - return Image::query() - ->where(function (Builder $query) { - $query->whereRelation('metadata', 'mime', 'LIKE', 'video%') - ->orWhereRelation('metadata', 'mime', 'LIKE', 'image%'); - }) - ->whereHas('commLinks') - ->doesntHave('hash') - ->where('src', 'NOT LIKE', '%.svg') - ->where('src', 'NOT LIKE', '%.tiff'); - } -} diff --git a/app/Console/Commands/Rsi/CommLink/Image/CreateImageMetadata.php b/app/Console/Commands/Rsi/CommLink/Image/CreateImageMetadata.php deleted file mode 100644 index c5fd5311e..000000000 --- a/app/Console/Commands/Rsi/CommLink/Image/CreateImageMetadata.php +++ /dev/null @@ -1,57 +0,0 @@ -info('Starting creation of image metadata.'); - - $query = Image::query() - ->whereHas('commLinks') - ->whereDoesntHave('metadata'); - - $this->createProgressBar($query->count()); - - $query->chunk( - 100, - function (Collection $images) { - $images->each( - function (Image $image) { - CreateImageMetadatum::dispatch($image); - $this->advanceBar(); - } - ); - } - ); - - $this->finishBar(); - - return QueueCommand::SUCCESS; - } -} diff --git a/app/Console/Commands/Rsi/CommLink/Image/SyncImageIds.php b/app/Console/Commands/Rsi/CommLink/Image/SyncImageIds.php deleted file mode 100644 index c959607f2..000000000 --- a/app/Console/Commands/Rsi/CommLink/Image/SyncImageIds.php +++ /dev/null @@ -1,40 +0,0 @@ -info('Dispatching Comm-Link Image Sync'); - - $offset = $this->parseOffset(); - - $this->info("Starting at Comm-Link ID {$offset}"); - - \App\Jobs\Rsi\CommLink\SyncImageIds::dispatch($offset); - - return CommLinkCommand::SUCCESS; - } -} diff --git a/app/Console/Commands/Rsi/CommLink/ImportCommLink.php b/app/Console/Commands/Rsi/CommLink/ImportCommLink.php deleted file mode 100644 index 6c5c5d091..000000000 --- a/app/Console/Commands/Rsi/CommLink/ImportCommLink.php +++ /dev/null @@ -1,72 +0,0 @@ -option('all') === true) { - ImportCommLinksJob::dispatch(-1); - - return Command::SUCCESS; - } - - if ($this->argument('id') === null) { - $this->error('Missing Comm-Link ID argument.'); - - return Command::FAILURE; - } - - $id = (int) $this->argument('id'); - - try { - $commLink = CommLink::query()->where('cig_id', $id)->firstOrFail(); - } catch (ModelNotFoundException $e) { - return Artisan::call('comm-links:download', [ - 'id' => $id, - '--import' => true, - ]); - } - - if (count(Storage::disk('comm_links')->files($id)) === 0) { - $this->error('Comm-Link does not exist on \'comm_links\' disk.'); - - return Command::FAILURE; - } - - $file = basename(Arr::last(Storage::disk('comm_links')->files($id))); - - dispatch(new ImportCommLinkJob($id, $file, $commLink, true)); - - return Command::SUCCESS; - } -} diff --git a/app/Console/Commands/Rsi/CommLink/TranslateCommLinks.php b/app/Console/Commands/Rsi/CommLink/TranslateCommLinks.php deleted file mode 100644 index b3f41a65a..000000000 --- a/app/Console/Commands/Rsi/CommLink/TranslateCommLinks.php +++ /dev/null @@ -1,48 +0,0 @@ -info('Dispatching Comm-Link Translation'); - - $modifiedTime = (int) $this->argument('modifiedTime'); - - if ($modifiedTime > 0) { - $this->info("Including Comm-Links that were created in the last '{$modifiedTime}' minutes"); - } elseif ($modifiedTime === -1) { - $this->info('Including all Comm-Links'); - } - - TranslateCommLinksJob::dispatch($this->filterDirectories('comm_links', $modifiedTime)->toArray()); - - return CommLinkCommand::SUCCESS; - } -} diff --git a/app/Console/Commands/Rsi/CommLink/Wiki/CreateCommLinkWikiPages.php b/app/Console/Commands/Rsi/CommLink/Wiki/CreateCommLinkWikiPages.php deleted file mode 100644 index a2fa2d235..000000000 --- a/app/Console/Commands/Rsi/CommLink/Wiki/CreateCommLinkWikiPages.php +++ /dev/null @@ -1,36 +0,0 @@ -info('Dispatching Comm-Link Wiki Page Creation'); - - \App\Jobs\Wiki\CommLink\CreateCommLinkWikiPages::dispatch(); - - return Command::SUCCESS; - } -} diff --git a/app/Console/Commands/Rsi/CommLink/Wiki/CreateCommLinkWikiTranslationPages.php b/app/Console/Commands/Rsi/CommLink/Wiki/CreateCommLinkWikiTranslationPages.php deleted file mode 100644 index 1e6a373e4..000000000 --- a/app/Console/Commands/Rsi/CommLink/Wiki/CreateCommLinkWikiTranslationPages.php +++ /dev/null @@ -1,36 +0,0 @@ -info('Dispatching Comm-Link Wiki Translation Page Creation'); - - \App\Jobs\Wiki\CommLink\CreateCommLinkWikiTranslationPages::dispatch(); - - return Command::SUCCESS; - } -} diff --git a/app/Console/Commands/SC/ComputeItemBaseIds.php b/app/Console/Commands/SC/ComputeItemBaseIds.php deleted file mode 100644 index ec3c5ae5e..000000000 --- a/app/Console/Commands/SC/ComputeItemBaseIds.php +++ /dev/null @@ -1,30 +0,0 @@ -call( - 'sc:import-items', - [ - '--skipVehicles', - '--type' => 'Char_Clothing_Torso_1,Char_Clothing_Legs,Char_Clothing_Torso_0,Char_Clothing_Feet,Char_Clothing_Hat,Char_Armor_Backpack,Char_Clothing_Hands,Char_Armor_Helmet,Char_Armor_Arms,Char_Armor_Torso,Char_Armor_Legs,Char_Armor_Undersuit,Char_Clothing_Torso_2,Char_Clothing_Backpack', - ] - ); - } -} diff --git a/app/Console/Commands/SC/ImportFactions.php b/app/Console/Commands/SC/ImportFactions.php deleted file mode 100644 index 5f190e7a3..000000000 --- a/app/Console/Commands/SC/ImportFactions.php +++ /dev/null @@ -1,56 +0,0 @@ -withProgressBar(File::allFiles(scdata('factions')), function (string $file) { - $data = File::json($file); - - if (str_starts_with($data['description'], '@')) { - $data['description'] = null; - } - - /** @var Faction $model */ - $model = Faction::query()->updateOrCreate([ - 'uuid' => $data['__ref'], - ], [ - 'name' => $data['displayName'], - 'class_name' => $data['ClassName'], - 'description' => $data['description'], - 'game_token' => $data['gameToken'], - 'default_reaction' => $data['defaultReaction'], - ]); - - collect($data['factionRelationships'])->each(function ($factionRelationship) use ($model) { - $model->relations()->updateOrCreate([ - 'other_faction_uuid' => $factionRelationship['faction'], - 'relation' => $factionRelationship['reactionType'], - ]); - }); - }); - } -} diff --git a/app/Console/Commands/SC/ImportItems.php b/app/Console/Commands/SC/ImportItems.php deleted file mode 100644 index b03b80ec7..000000000 --- a/app/Console/Commands/SC/ImportItems.php +++ /dev/null @@ -1,126 +0,0 @@ -getData(); - - $files = File::allFiles(scdata('items')) + Storage::allFiles(scdata('ships')); - - // Debug - // $files = collect($files)->filter(fn ($file) => $file->isFile() && str_contains($file->getFilename(), 'klwe_sniper_energy_01')); - - if ($this->option('skipItems') === false) { - collect($files) - ->filter(function (string $file) { - return ! str_contains($file, '-raw.json'); - }) - ->tap(function (Collection $chunks) { - $this->info(sprintf( - 'Importing %d items in chunks of 25 (%d).', - $chunks->count(), - (int) ($chunks->count() / 25) - )); - }) - ->chunk(25) - ->tap(function (Collection $chunks) { - $this->createProgressBar($chunks->count()); - }) - ->each(function (Collection $chunk) use ($manufacturers) { - $this->bar->advance(); - - $chunk->map(function (string $file) use ($manufacturers) { - return [ - 'file' => $file, - 'item' => (new Item($file, $manufacturers))->getData(), - ]; - }) - ->filter(function (array $data) { - return $data['item'] !== null; - }) - ->filter(function (array $data) { - $item = $data['item']; - - return isset($item['name']) && ! in_array($item['name'], $this->ignoredNames, true); - }) - ->filter(function (array $data) { - if (! $this->option('type') !== null) { - return true; - } - - $types = array_map( - 'strtolower', - array_map('trim', explode(',', $this->option('type'))) - ); - - return in_array(strtolower($data['item']['type']), $types, true); - }) - ->map(function (array $data) { - \App\Jobs\SC\Import\Item::dispatch($data['item']); - - return [ - 'item' => $data['item'], - 'file' => $data['file'], - ]; - }) - ->each(function (array $data) { - ['item' => $item, 'file' => $path] = $data; - ItemSpecificationCreator::createSpecification($item, $path); - }); - }); - } - - if ($this->option('skipVehicles') === false) { - $this->info("\n\nImporting Vehicles"); - Artisan::call('sc:import-vehicles'); - } - - Artisan::call('sc:compute-item-base-ids'); - - $this->info('Done.'); - - return Command::SUCCESS; - } -} diff --git a/app/Console/Commands/SC/ImportMissions.php b/app/Console/Commands/SC/ImportMissions.php deleted file mode 100644 index 0dc55b339..000000000 --- a/app/Console/Commands/SC/ImportMissions.php +++ /dev/null @@ -1,299 +0,0 @@ -importRewards(); - $this->importStandings(); - $this->importScopes(); - $this->importTypes(); - $this->importGiver(); - $this->importMissions(); - $this->syncMissions(); - } - - private function importRewards(): void - { - $this->withProgressBar(File::allFiles(scdata('reputation/rewards')), function (string $file) { - $data = File::json($file); - - if (isset($data['missionGiverBonuses'])) { - // TODO: Import Bonuses - return; - } - - Reward::query()->updateOrCreate([ - 'uuid' => $data['__ref'], - ], [ - 'editor_name' => $data['editorName'], - 'reputation_amount' => $data['reputationAmount'], - 'class_name' => $data['ClassName'], - ]); - }); - } - - private function importStandings(): void - { - $this->withProgressBar(File::allFiles(scdata('reputation/standings')), function (string $file) { - $data = File::json($file); - - if (str_starts_with($data['displayName'], '@')) { - $data['displayName'] = null; - } - - if (str_starts_with($data['perkDescription'], '@')) { - $data['perkDescription'] = null; - } - - if ($data['description'] === 'desc' || str_starts_with($data['description'], '@')) { - $data['description'] = null; - } - - Standing::query()->updateOrCreate([ - 'uuid' => $data['__ref'], - ], [ - 'name' => $data['name'], - 'description' => $data['description'], - 'display_name' => $data['displayName'], - 'perk_description' => $data['perkDescription'], - 'min_reputation' => $data['minReputation'], - 'drift_reputation' => $data['driftReputation'], - 'drift_time_hours' => $data['driftTimeHours'], - 'gated' => $data['gated'], - ]); - }); - } - - private function importScopes(): void - { - $this->withProgressBar(File::allFiles(scdata('reputation/scopes')), function (string $file) { - $data = File::json($file); - - if ($data['description'] === 'desc' || str_starts_with($data['description'], '@')) { - $data['description'] = null; - } - - /** @var Scope $scope */ - $scope = Scope::query()->updateOrCreate([ - 'uuid' => $data['__ref'], - ], [ - 'scope_name' => $data['scopeName'], - 'display_name' => $data['displayName'], - 'description' => $data['description'], - 'class_name' => $data['ClassName'], - 'initial_reputation' => $data['standingMap']['reputationCeiling'], - 'reputation_ceiling' => $data['standingMap']['initialReputation'], - ]); - - $ids = collect($data['standingMap']['standings'])->map(function (array $standing) { - return Standing::query()->where('uuid', $standing['value'])->first()?->id; - })->filter(fn ($id) => $id !== null); - - $scope->standings()->sync($ids); - }); - } - - private function importTypes(): void - { - $this->withProgressBar(File::allFiles(scdata('missions/types')), function (string $file) { - $data = File::json($file); - - Type::query()->updateOrCreate([ - 'uuid' => $data['__ref'], - ], [ - 'name' => $data['LocalisedTypeName'], - ]); - }); - } - - private function importGiver(): void - { - $this->withProgressBar(File::allFiles(scdata('missions/missiongiver')), function (string $file) { - $data = File::json($file); - - $texts = [ - 'displayName', - 'description', - 'headquarters', - ]; - - foreach ($texts as $text) { - if (str_starts_with($data[$text], '@')) { - $data[$text] = null; - } - } - - /** @var Giver $giver */ - $giver = Giver::query()->updateOrCreate([ - 'uuid' => $data['__ref'], - ], [ - 'name' => $data['displayName'], - 'headquarters' => $data['headquarters'], - 'invitation_timeout' => $data['invitationTimeout'], - 'visit_timeout' => $data['visitTimeout'], - 'short_cooldown' => $data['shortCooldown'], - 'medium_cooldown' => $data['mediumCooldown'], - 'long_cooldown' => $data['longCooldown'], - ]); - - if (! empty($data['description'])) { - $giver->translations()->updateOrCreate([ - 'locale_code' => Language::ENGLISH, - ], [ - 'translation' => $data['description'], - ]); - } - }); - } - - private function importMissions(): void - { - $this->withProgressBar(File::allFiles(scdata('missions')), function (string $file) { - $data = File::json($file); - - if (empty($data['title'])) { - return; - } - - /** @var Mission $mission */ - $mission = Mission::query()->updateOrCreate([ - 'uuid' => $data['__ref'], - ], [ - 'not_for_release' => $data['notForRelease'], - 'title' => $data['title'], - 'title_hud' => $data['titleHUD'], - 'mission_giver' => $data['missionGiver'], - 'comms_channel_name' => $data['commsChannelName'], - 'locality_available' => $data['localityAvailable'], - 'location_mission_available' => $data['locationMissionAvailable'], - 'initially_active' => $data['initiallyActive'], - 'notify_on_available' => $data['notifyOnAvailable'], - 'show_as_offer' => $data['showAsOffer'], - 'mission_buy_in_amount' => $data['missionBuyInAmount'], - 'refund_buy_in_on_withdraw' => $data['refundBuyInOnWithdraw'], - 'has_complete_button' => $data['hasCompleteButton'], - 'handles_abandon_request' => $data['handlesAbandonRequest'], - 'mission_module_per_player' => $data['missionModulePerPlayer'], - 'max_instances' => $data['maxInstances'], - 'max_players_per_instance' => $data['maxPlayersPerInstance'], - 'max_instances_per_player' => $data['maxInstancesPerPlayer'], - 'can_be_shared' => $data['canBeShared'], - 'once_only' => $data['onceOnly'], - 'tutorial' => $data['tutorial'], - 'display_allied_markers' => $data['displayAlliedMarkers'], - 'available_in_prison' => $data['availableInPrison'], - 'fail_if_sent_to_prison' => $data['failIfSentToPrison'], - 'fail_if_became_criminal' => $data['failIfBecameCriminal'], - 'fail_if_leave_prison' => $data['failIfLeavePrison'], - 'request_only' => $data['requestOnly'], - 'respawn_time' => $data['respawnTime'], - 'respawn_time_variation' => $data['respawnTimeVariation'], - 'instance_has_life_time' => $data['instanceHasLifeTime'], - 'show_life_time_in_mobi_glas' => $data['showLifeTimeInMobiGlas'], - 'instance_life_time' => $data['instanceLifeTime'], - 'instance_life_time_variation' => $data['instanceLifeTimeVariation'], - 'can_reaccept_after_abandoning' => $data['canReacceptAfterAbandoning'], - 'abandoned_cooldown_time' => $data['abandonedCooldownTime'], - 'abandoned_cooldown_time_variation' => $data['abandonedCooldownTimeVariation'], - 'can_reaccept_after_failing' => $data['canReacceptAfterFailing'], - 'has_personal_cooldown' => $data['hasPersonalCooldown'], - 'personal_cooldown_time' => $data['personalCooldownTime'], - 'personal_cooldown_time_variation' => $data['personalCooldownTimeVariation'], - 'module_handles_own_shutdown' => $data['moduleHandlesOwnShutdown'], - 'linked_mission' => $data['linkedMission'], - 'lawful_mission' => $data['lawfulMission'], - 'invitation_mission' => $data['invitationMission'], - 'version' => config('api.sc_data_version'), - 'type_id' => Type::query()->where('uuid', $data['type'])->first()?->id, - 'giver_id' => Giver::query()->where('uuid', $data['missionGiverRecord'])->first()?->id, - ]); - - if (! str_starts_with($data['description'], '@')) { - $mission->translations()->updateOrCreate([ - 'locale_code' => Language::ENGLISH, - ], [ - 'translation' => $data['description'], - ]); - } - - if (! empty($data['missionReward'])) { - $mission->reward()->updateOrCreate([ - 'mission_id' => $mission->id, - ], [ - 'amount' => $data['missionReward']['reward'], - 'max' => $data['missionReward']['max'], - 'plus_bonuses' => $data['missionReward']['plusBonusses'], - 'currency' => $data['missionReward']['currencyType'], - 'reputation_bonus' => $data['missionReward']['reputationBonus'], - ]); - } - - if (! empty($data['missionDeadline'])) { - $mission->deadline()->updateOrCreate([ - 'mission_id' => $mission->id, - ], [ - 'mission_completion_time' => $data['missionDeadline']['missionCompletionTime'], - 'mission_auto_end' => $data['missionDeadline']['missionAutoEnd'], - 'mission_result_after_timer_end' => $data['missionDeadline']['missionResultAfterTimerEnd'], - 'mission_end_reason' => $data['missionDeadline']['missionEndReason'], - ]); - } - }); - } - - private function syncMissions(): void - { - $this->withProgressBar(File::allFiles(scdata('missions')), function (string $file) { - $data = File::json($file); - - /** @var Mission $mission */ - $mission = Mission::query()->where('uuid', $data['__ref'])->first(); - - if ($mission === null) { - return; - } - - $ids = collect($data['requiredMissions'])->map(function (array $standing) { - return Mission::query()->where('uuid', $standing['value'])->first()?->id; - })->filter(fn ($id) => $id !== null); - - $mission->requiredMissions()->sync($ids); - - $ids = collect($data['associatedMissions'])->map(function (array $standing) { - return Mission::query()->where('uuid', $standing['value'])->first()?->id; - })->filter(fn ($id) => $id !== null); - - $mission->associatedMissions()->sync($ids); - }); - } -} diff --git a/app/Console/Commands/SC/ImportPersonalWeapons.php b/app/Console/Commands/SC/ImportPersonalWeapons.php deleted file mode 100644 index c3dd8a704..000000000 --- a/app/Console/Commands/SC/ImportPersonalWeapons.php +++ /dev/null @@ -1,38 +0,0 @@ -call( - 'sc:import-items', - [ - '--skipVehicles', - '--type' => 'WeaponPersonal', - ] - ); - } -} diff --git a/app/Console/Commands/SC/ImportShops.php b/app/Console/Commands/SC/ImportShops.php deleted file mode 100644 index a96044066..000000000 --- a/app/Console/Commands/SC/ImportShops.php +++ /dev/null @@ -1,38 +0,0 @@ -info('Importing Shops'); - ShopItems::dispatch(); - $this->info('Done'); - - return Command::SUCCESS; - } -} diff --git a/app/Console/Commands/SC/ImportVehicleItems.php b/app/Console/Commands/SC/ImportVehicleItems.php deleted file mode 100644 index d2d10747d..000000000 --- a/app/Console/Commands/SC/ImportVehicleItems.php +++ /dev/null @@ -1,38 +0,0 @@ -call( - 'sc:import-items', - [ - '--skipVehicles', - '--type' => 'Armor,Battery,BombLauncher,Cooler,CoolerController,EMP,ExternalFuelTank,FlightController,FuelIntake,FuelTank,MainThruster,ManneuverThruster,Missile,MissileController,MissileLauncher,Paints,PowerPlant,QuantumDrive,QuantumFuelTank,QuantumInterdictionGenerator,Radar,SelfDestruct,Shield,ShieldController,ToolArm,Turret,TurretBase,UtilityTurret,WeaponDefensive,WeaponGun,WeaponMining,WeaponMount,WheeledController', - ] - ); - } -} diff --git a/app/Console/Commands/SC/ImportVehicles.php b/app/Console/Commands/SC/ImportVehicles.php deleted file mode 100644 index daf543d84..000000000 --- a/app/Console/Commands/SC/ImportVehicles.php +++ /dev/null @@ -1,126 +0,0 @@ -error('ships.json not found. Did you clone scunpacked?'); - - return Command::FAILURE; - } - - try { - $vehicles = json_decode($vehicles, true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { - $this->error($e->getMessage()); - - return Command::FAILURE; - } - - collect($vehicles) - ->tap(function (Collection $chunks) { - $this->info(sprintf( - 'Importing %d vehicles in chunks of 5 (%d).', - $chunks->count(), - (int) ($chunks->count() / 5) - )); - }) - ->chunk(5) - ->tap(function (Collection $chunks) { - $this->createProgressBar($chunks->count()); - }) - ->each(function (Collection $chunk) { - $this->bar->advance(); - $chunk - ->filter(function (array $vehicle) { - return isset($vehicle['ClassName']) && $this->isNotIgnoredClass($vehicle['ClassName']); - }) - ->map(function (array $vehicle) { - $vehicle['filePathRaw'] = scdata(sprintf( - 'ships/%s-raw.json', - strtolower($vehicle['ClassName']) - )); - - $vehicle['filePath'] = scdata(sprintf( - 'ships/%s.json', - strtolower($vehicle['ClassName']) - )); - - return $vehicle; - })->each(function (array $vehicle) { - Vehicle::dispatch($vehicle); - }); - }); - - return Command::SUCCESS; - } - - private function isNotIgnoredClass(string $class): bool - { - $tests = [ - '_Hangar', - 'Active1', - 'BIS29', - 'Bombless', - 'CINEMATIC_ONLY', - //'F7A_Mk1', - 'Fleetweek', - 'fw22nfz', - 'Indestructible', - 'Krugeri', - 'modifiers', - 'NO_CUSTOM', - 'NoCrimesAgainst', - 'Prison', - 'Showdown', - 'SM_TE', - 'Test', - 'Tutorial', - 'Unmanned', - ]; - - $isGood = true; - - foreach ($tests as $toTest) { - $isGood = $isGood && stripos($class, $toTest) === false; - } - - $isGood = $isGood && $class !== 'TEST_Boat'; - - return $isGood; - } -} diff --git a/app/Console/Commands/SC/TranslateItems.php b/app/Console/Commands/SC/TranslateItems.php deleted file mode 100644 index 14ae57125..000000000 --- a/app/Console/Commands/SC/TranslateItems.php +++ /dev/null @@ -1,43 +0,0 @@ -info('Translating Items'); - $items = Item::query()->whereHas('translations'); - $this->createProgressBar($items->count()); - - $items->each(function (Item $item) { - TranslateItem::dispatch($item); - $this->advanceBar(); - }); - - return 0; - } -} diff --git a/app/Console/Commands/SC/Wiki/AbstractCreateWikiPage.php b/app/Console/Commands/SC/Wiki/AbstractCreateWikiPage.php deleted file mode 100644 index 692daac37..000000000 --- a/app/Console/Commands/SC/Wiki/AbstractCreateWikiPage.php +++ /dev/null @@ -1,153 +0,0 @@ - -|image = {{Find image}} -|name = -|manufacturer = -}} -ITEM; - - protected string $template; - - final public function uploadWiki($model, string $summary): void - { - try { - $token = $this->getCsrfToken('services.wiki_translations'); - $response = MediaWikiApi::edit($this->getPageName($model)) - ->withAuthentication() - ->text($this->getPageText($model)) - ->csrfToken($token) - ->summary($summary) - ->request([ - 'timeout' => 600, - ]); - } catch (ErrorException|GuzzleException $e) { - $this->error($e->getMessage()); - - return; - } - - // $this->createEnglishSubpage($this->getPageName($model), $token); - - if ($response->hasErrors() && $response->getErrors()['code'] !== 'articleexists') { - $this->error(implode(', ', $response->getErrors())); - } - } - - abstract protected function prepareTemplate($model): string; - - abstract protected function getPageName($model): string; - - abstract protected function getManufacturerCode($model): string; - - protected function getUUID($model): string - { - return $model->uuid; - } - - protected function getPageText($model): string - { - $name = $this->getPageName($model); - - $itemTemplate = $this->itemTemplate; - $itemTemplate = str_replace('', $this->getUUID($model), $itemTemplate); - $itemTemplate = str_replace('', $name, $itemTemplate); - $itemTemplate = str_replace('', $this->getManufacturerCode($model), $itemTemplate); - - $originalTemplate = $this->template; - - if (config('services.wiki_pages.refname') === null || config('services.wiki_pages.version') === null) { - throw new RuntimeException('Missing config'); - } - - $this->template = str_replace( - '', - config('services.wiki_pages.refname'), - $this->template - ); - $this->template = str_replace( - '', - config('services.wiki_pages.version'), - $this->template - ); - - $this->template = str_replace( - '', - Carbon::now()->format('Y-m-d'), - $this->template - ); - $this->template = str_replace( - '', - $this->getManufacturerCode($model), - $this->template - ); - $this->template = str_replace( - '', - $name, - $this->template - ); - - $this->template = sprintf("%s\n%s", $itemTemplate, $this->template); - - $text = $this->prepareTemplate($model); - $this->reset(); - $this->template = $originalTemplate; - - return $text; - } - - protected function fixText(string $type, string &$text, array $additions = []): string - { - $needles = [ - 'a', - 'e', - 'i', - 'o', - 'u', - 'panzerung', - ...$additions, - ]; - - if (Str::endsWith($type, $needles)) { - $text = str_replace('ist ein ', 'ist eine ', $text); - $text = str_replace('(r)', '', $text); - } else { - $text = str_replace('(r)', 'r', $text); - } - - return $text; - } - - private function reset(): void - { - $this->template = <<<'ITEM' -{{Item -|uuid = -|image = -|name = -|manufacturer = -}} -ITEM; - } -} diff --git a/app/Console/Commands/SC/Wiki/CreateCharArmorWikiPages.php b/app/Console/Commands/SC/Wiki/CreateCharArmorWikiPages.php deleted file mode 100644 index 2aab816c4..000000000 --- a/app/Console/Commands/SC/Wiki/CreateCharArmorWikiPages.php +++ /dev/null @@ -1,180 +0,0 @@ -''' ist ein hergestellt von [[{{subst:MFURN|}}]].{{Cite game|build=[[Star Citizen Alpha |Alpha ]]|accessdate=}} -== Beschreibung == -{{Item description}} -== Itemports == -{{Item ports}} -== Erwerb == -{{Item availability}} -== Model == -=== Varianten === -{{Item variants}} -{{Quellen}} -{{Navplate manufacturers|}} -TEMPLATE; - - protected array $typeMapping = [ - 'Char_Armor_Arms' => 'Armpanzerung', - 'Char_Armor_Torso' => 'Oberkörperpanzerung', - 'Char_Armor_Legs' => 'Beinpanzerung', - 'Char_Armor_Helmet' => 'Helm', - 'Char_Armor_Backpack' => 'Rucksack', - 'Char_Armor_Undersuit' => 'Unteranzug', - // Clothing - 'Char_Clothing_Torso_1' => 'Jacke', - 'Char_Clothing_Legs' => 'Hose', - 'Char_Clothing_Torso_0' => 'Shirt', - 'Char_Clothing_Feet' => 'Schuh', - 'Char_Clothing_Hat' => 'Hut', - 'Char_Clothing_Hands' => 'Handschuh', - 'Char_Clothing_Torso_2' => 'Gürtel', - 'Char_Clothing_Backpack' => 'Rucksack', - ]; - - protected array $subTypeMapping = [ - 'Heavy' => 'schwere(r)', - 'Medium' => 'mittlere(r)', - 'Light' => 'leichte(r)', - 'Personal' => 'persönliche(r)', - // Clothing - 'Female' => 'weibliche(r)', - 'Male' => 'männliche(r)', - 'Medical' => 'medizinische(r)', - ]; - - /** - * Execute the console command. - */ - public function handle(): int - { - $this->withProgressBar( - Armor::all(), - function (Armor $armor) { - $this->uploadWiki($armor, 'Automatische Erstellung von Kleidungs- und Rüstungsseiten'); - } - ); - - return 0; - } - - protected function prepareTemplate($model): string - { - $name = $this->getPageName($model); - $type = $this->typeMapping[$model->type] ?? $model->type; - - $pageContent = $this->template; - - $pageContent = str_replace( - ' ', - $model->sub_type === 'UNDEFINED' - ? '' - : strtolower($this->subTypeMapping[$model->sub_type] ?? $model->sub_type).' ', - $pageContent - ); - - $pageContent = str_replace( - '', - $type, - $pageContent - ); - - if (self::getSuffix($model) !== null || (str_contains($model->class_name, '_01_15') && str_contains($name, 'Black/Silver'))) { - $info = sprintf(" Dieses Item wird im Spiel als '''%s''' angezeigt.", $model->name); - - $pageContent = str_replace( - '', - $info, - $pageContent - ); - } else { - $pageContent = str_replace('', '', $pageContent); - } - - $this->fixText($type, $pageContent, ['panzerung']); - - if (str_contains($model->class_name, '_01_15') && str_contains($name, 'Black/Silver')) { - $pageContent .= "\n[[Category:Gegenstand mit nicht eindeutigem Namen im Spiel]]"; - } - - return $pageContent; - } - - protected static function getSuffix(Armor|Clothes $armor): ?string - { - $suffix = match (true) { - str_contains($armor->class_name, '_hd_sec') => ' Hurston Security', - str_contains($armor->class_name, '_irn') => ' Iron', - str_contains($armor->class_name, '_gld') => ' Gold', - str_contains($armor->class_name, '_microtech') => ' microTech', - str_contains($armor->class_name, '_carrack') && ! str_contains($armor->name, 'Carrack') => ' Carrack Edition', - str_contains($armor->class_name, '_9tails') => ' (Nine Tails)', - str_contains($armor->class_name, '_xenothreat') => ' (Xenothreat)', - default => null - }; - - if (! str_contains(strtolower($armor->name), strtolower(trim($suffix ?? '', ' ()')))) { - return $suffix; - } - - return null; - } - - protected function getPageName($model): string - { - $suffix = self::getSuffix($model) ?? ''; - - $name = $model->name.($suffix); - if (str_contains($model->class_name, '_01_15') && str_contains($name, 'Black/Silver')) { - $name = str_replace('Black', 'Tan', $name); - } - - if ($model->name === 'Venture Helmet White' && $model->class_name === 'rsi_explorer_armor_light_helmet_01_01_10') { - $name = str_replace('White', 'White/Red', $name); - } - - $name = str_replace("$suffix$suffix", $suffix, $name); - $name = preg_replace('/^\s*-\s*/', '', $name); - - return $name; - } - - protected function getManufacturerCode($model): string - { - return $model->manufacturer->code; - } - - public static function getNameForModel($model): string - { - return (new self)->getPageText($model); - } -} diff --git a/app/Console/Commands/SC/Wiki/CreateClothingWikiPages.php b/app/Console/Commands/SC/Wiki/CreateClothingWikiPages.php deleted file mode 100644 index 6b9cdc9f5..000000000 --- a/app/Console/Commands/SC/Wiki/CreateClothingWikiPages.php +++ /dev/null @@ -1,44 +0,0 @@ -withProgressBar( - Clothes::all(), - function (Clothes $armor) { - $this->uploadWiki($armor, 'Automatische Erstellung von Kleidungs- und Rüstungsseiten'); - } - ); - - return 0; - } -} diff --git a/app/Console/Commands/SC/Wiki/CreateCommodityWikiPages.php b/app/Console/Commands/SC/Wiki/CreateCommodityWikiPages.php deleted file mode 100644 index f3e8945c2..000000000 --- a/app/Console/Commands/SC/Wiki/CreateCommodityWikiPages.php +++ /dev/null @@ -1,238 +0,0 @@ -with(['items']) - ->get() - ->filter(function (Shop $shop) { - return strpos($shop->name_raw, 'IAE Expo') === false; - }) - ->filter(function (Shop $shop) { - return strpos($shop->name_raw, 'removed') === false; - }) - ->filter(function (Shop $shop) { - return strpos($shop->name_raw, 'Teach\'s') === false; - }) - ->filter(function (Shop $shop) { - return strpos($shop->name_raw, 'Levski') === false; - }) - ->filter(function (Shop $shop) { - return strpos($shop->name_raw, 'Rentals') === false; - }) - ->filter(function (Shop $shop) { - return strpos($shop->name_raw, 'New Deal') === false; - }) - ->filter(function (Shop $shop) { - return strpos($shop->name_raw, 'Astro Armada') === false; - }) - ->filter(function (Shop $shop) { - return $shop->position !== 'Unknown Position'; - }) - ->filter(function (Shop $shop) { - return $shop->name !== 'Unknown Name'; - }); - - $this->createProgressBar($data->count()); - - $data->each(function (Shop $shop) { - $this->uploadWiki($shop); - - $this->advanceBar(); - }); - - if (config('services.wiki_approve_revs.access_secret') !== null) { - $this->approvePages($data); - } - - return 0; - } - - public function uploadWiki(Shop $shop) - { - $items = $shop - ->items - ->filter(function (Item $item) { - return ! str_contains($item->name, '[PLACEHOLDER]'); - }) - ->filter(function (Item $item) { - return ! in_array($item->type, $this->ignoredTypes, true); - }) - ->filter(function (Item $item) { - return ! str_contains($item->type, 'Char_Clothing'); - }) - ->sortBy('name') - ->map(function (Item $item) use ($shop) { - return SmwSubObjectMapper::map( - $this->mapItem($item, $shop), - ' ', - [], - str_replace(['.', '[PH]'], '', $item->name) - ); - }) - ->implode("\n"); - - $title = sprintf('Spieldaten/Handelswaren/%s/%s', $shop->position, $shop->name); - - if (empty(trim($items))) { - dump('Delete: '.$title); - try { - $token = $this->getCsrfToken('services.wiki_translations'); - MediaWikiApi::action('delete', 'POST') - ->withAuthentication() - ->addParam('title', $title) - ->csrfToken($token) - ->addParam('reason', 'Deleting empty commodity page') - ->request(); - - return; - } catch (ErrorException|GuzzleException $e) { - $this->error($e->getMessage()); - - return; - } - } - - // phpcs:disable - $format = <<<'FORMAT' - -{{Alert|color=info|title=Information|content=Diese Seite enthält Daten über Kauf- und Mietpreise von Handelswaren in Star Citizen.
Diese Daten werden automatisch durch die Star Citizen Wiki API verwaltet.}} -%s - -[[Kategorie:Instandhaltung]] -[[Kategorie:Alpha %s]] -
-FORMAT; - // phpcs:enable - - try { - $token = $this->getCsrfToken('services.wiki_translations'); - $response = MediaWikiApi::edit($title) - ->withAuthentication() - ->text(sprintf($format, $items, config('api.sc_data_version'))) - ->csrfToken($token) - ->summary('Updating Commodity Prices') - ->request(); - } catch (ErrorException|GuzzleException $e) { - $this->error($e->getMessage()); - - return; - } - - if ($response->hasErrors()) { - $this->error(implode(', ', $response->getErrors())); - } - } - - private function approvePages(Collection $data): void - { - $this->info('Approving Pages'); - $this->createProgressBar($data->count()); - - $data->map(function (Shop $shop) { - return sprintf('Spieldaten/Handelswaren/%s/%s', $shop->position, $shop->name); - }) - ->each(function ($page) { - $this->loginWikiBotAccount('services.wiki_approve_revs'); - - dispatch(new ApproveRevisions([$page], false)); - $this->advanceBar(); - }); - } - - private function mapItem(Item $item, Shop $shop): array - { - $formatter = new NumberFormatter(config('app.locale'), NumberFormatter::TYPE_DEFAULT); - - return [ - 'UUID' => $item->uuid, - 'Name' => str_replace('[PH]', '', $item->name), - 'Basispreis' => $formatter->format($item->shop_data->base_price).'aUEC', - 'Preis' => $formatter->format($item->shop_data->offsetted_price).'aUEC', - 'Minimalpreis' => $formatter->format($item->shop_data->priceRange['min']).'aUEC', - 'Maximalpreis' => $formatter->format($item->shop_data->priceRange['max']).'aUEC', - 'Preisoffset' => $formatter->format($item->shop_data->base_price_offset), - 'Rabatt' => $formatter->format($item->shop_data->max_discount), - 'Premium' => $formatter->format($item->shop_data->max_premium), - 'Bestand' => $formatter->format($item->shop_data->inventory), - 'Maximalbestand' => $formatter->format($item->shop_data->max_inventory), - 'Wiederauffüllungsrate' => $formatter->format($item->shop_data->refresh_rate), - 'Typ' => $item->type.'@en', - 'Kaufbar' => $item->shop_data->buyable, - 'Verkaufbar' => $item->shop_data->sellable, - 'Mietbar' => $item->shop_data->rentable, - 'Händler' => $shop->name, - 'Ort' => $shop->position, - 'Spielversion' => $item->version, - ]; - } -} diff --git a/app/Console/Commands/SC/Wiki/CreateFoodWikiPages.php b/app/Console/Commands/SC/Wiki/CreateFoodWikiPages.php deleted file mode 100644 index bf74adb97..000000000 --- a/app/Console/Commands/SC/Wiki/CreateFoodWikiPages.php +++ /dev/null @@ -1,93 +0,0 @@ -''' ist ein Lebensmittel. Es wird hergestellt von [[{{subst:MFURN|}}]].{{Cite game|build=[[Star Citizen Alpha |Alpha ]]|accessdate=}} -== Beschreibung == -{{Item description}} -== Erwerb == -{{Item availability}} -== Quellen == - -{{Navplate food and drinks}} -TEMPLATE; - - /** - * Execute the console command. - */ - public function handle(): int - { - $this->withProgressBar( - Food::query() - ->whereRelation('item', 'name', 'NOT LIKE', '%PLACEHOLDER%') - ->whereRelation('item', 'class_name', 'NOT LIKE', '%test%') - ->get(), - function (Food $food) { - $this->uploadWiki($food, 'Automatische Erstellung von Lebensmitteln'); - } - ); - - return 0; - } - - /** - * @param Food $model - */ - protected function prepareTemplate($model): string - { - $pageContent = $this->template; - - $effects = ''; - if ($model->nutritional_density_rating && $model->hydration_efficacy_index) { - $effects = ', welches [[NDR|hunger]] und [[HEI|durst]] stillt'; - } elseif ($model->hydration_efficacy_index) { - $effects = ', welches den [[HEI|durst]] stillt'; - } elseif ($model->nutritional_density_rating) { - $effects = ', welches den [[NDR|hunger]] sättigt'; - } - - $pageContent = str_replace( - '', - $effects, - $pageContent - ); - - return $pageContent; - } - - protected function getPageName($model): string - { - return $model->item->name; - } - - protected function getManufacturerCode($model): string - { - return $model->item->manufacturer->code; - } - - protected function getUUID($model): string - { - return $model->item->uuid; - } -} diff --git a/app/Console/Commands/SC/Wiki/CreateShipItemWikiPages.php b/app/Console/Commands/SC/Wiki/CreateShipItemWikiPages.php deleted file mode 100644 index 07aeb20ae..000000000 --- a/app/Console/Commands/SC/Wiki/CreateShipItemWikiPages.php +++ /dev/null @@ -1,153 +0,0 @@ -''' ist ein Größe hergestellt von [[{{subst:MFURN|}}]].{{Cite game|build=[[Star Citizen Alpha |Alpha ]]|accessdate=}} -== Beschreibung == -{{Item description}} -== Erwerb == -{{Item availability}} -== Standardausrüstung von == -{{Standardausrüstung}} -{{Quellen}} -{{Navplate manufacturers|}} -{{ Navplate}} -TEMPLATE; - - protected array $typeMapping = [ - 'BombLauncher' => 'Bombenwerfer', - 'Cooler' => 'Kühler', - 'EMP' => 'EMP-Generator', - 'Missile' => 'Rakete', - 'MissileLauncher' => 'Raketenwerfer', - 'PowerPlant' => 'Generator', - 'QuantumDrive' => 'Quantenantrieb', - 'QuantumInterdictionGenerator' => 'Quantum Enforcement Device', - 'SalvageModifier' => 'Bergungsmodifikator', - 'Shield' => 'Schildgenerator', - 'TowingBeam' => 'Abschleppstrahl', - 'TractorBeam' => 'Traktorstrahl', - 'WeaponGun' => 'Fahrzeugwaffe', - 'WeaponMining' => 'Bergbaulaser', - 'WeaponDefensive' => 'Defensivmittel', - ]; - - protected array $classMappings = [ - 'Civilian' => 'Zivil', - 'Competition' => 'Wettkampf', - 'Military' => 'Militär', - 'Industrial' => 'Industrie', - 'Stealth' => 'Stealth', - ]; - - /** - * Execute the console command. - */ - public function handle(): int - { - $this->withProgressBar( - VehicleItem::query() - ->where('sc_items.name', '<>', '<= PLACEHOLDER =>') - ->whereIn('type', [ - 'BombLauncher', - 'Cooler', - 'EMP', - 'Missile', - 'MissileLauncher', - 'PowerPlant', - 'QuantumDrive', - 'QuantumInterdictionGenerator', - 'SalvageModifier', - 'Shield', - 'TowingBeam', - 'TractorBeam', - 'WeaponGun', - 'WeaponMining', - 'Radar', - ]) - ->get(), - function (VehicleItem $item) { - $this->uploadWiki($item, 'Automatische Erstellung von Fahrzeugitems'); - } - ); - - return 0; - } - - protected function prepareTemplate($model): string - { - $pageContent = $this->template; - $type = ($this->typeMapping[$model->type] ?? $model->type); - - $pageContent = str_replace( - '', - $model->size.((! $model->grade && ! $model->class) ? ' ' : ''), - $pageContent - ); - - $pageContent = str_replace( - '', - $model->grade ? ', Grad '.$model->grade.', ' : '', - $pageContent - ); - - $pageContent = str_replace( - '', - $model->class ? ($this->classMappings[$model->class] ?? $model->class).'-' : '', - $pageContent - ); - - $pageContent = str_replace( - ' ', - $type.' ', - $pageContent - ); - - $this->fixText($type, $pageContent); - - return $pageContent; - } - - /** - * @param VehicleItem $model - */ - protected function getPageName($model): string - { - $name = $model->name; - - if (in_array($name, ['Liberator', 'Odyssey', 'Nova', 'Vulcan', 'Eclipse', 'Centurion', 'Citadel', 'Castra', 'Mercury'])) { - $name = sprintf('%s (%s)', $name, ($this->typeMapping[$model->type] ?? $model->type)); - } - - return $name; - } - - /** - * @param VehicleItem $model - */ - protected function getManufacturerCode($model): string - { - return $model->manufacturer->code; - } -} diff --git a/app/Console/Commands/SC/Wiki/CreateWeaponAttachmentWikiPages.php b/app/Console/Commands/SC/Wiki/CreateWeaponAttachmentWikiPages.php deleted file mode 100644 index f9e9a8736..000000000 --- a/app/Console/Commands/SC/Wiki/CreateWeaponAttachmentWikiPages.php +++ /dev/null @@ -1,125 +0,0 @@ -''' ist ein Größe hergestellt von [[{{subst:MFURN|}}]].{{Cite game|build=[[Star Citizen Alpha |Alpha ]]|accessdate=}} -== Beschreibung == -{{Item description}} -== Erwerb == -{{Item availability}} -{{Quellen}} -{{Navplate manufacturers|}} -TEMPLATE; - - protected array $typeMapping = [ - 'Ballistic Compensator' => 'ballistischer Kompensator', - 'Flash Hider' => 'Mündungsfeuerdämpfer', - 'Energy Stabilizer' => 'Energie-Stabilisator', - 'Suppressor' => 'Schalldämpfer', - 'Projection' => 'Projektionsvisier', - 'Reflex' => 'Reflexvisier', - 'Telescopic' => 'Zielfernrohr', - 'Monitor' => 'Monitorvisier', - 'Flashlight' => 'Taschenlampe', - 'Laser Pointer' => 'Laserpointer', - - // Raw Subtype - 'Magazine' => 'Magazin', - 'Utility' => 'Waffenaufsatz', - 'IronSight' => 'Zielfernrohr', - ]; - - /** - * Execute the console command. - */ - public function handle(): int - { - $this->withProgressBar( - Attachment::query() - ->where('sc_items.name', '<>', '<= PLACEHOLDER =>') - ->whereIn('sub_type', [ - 'Magazine', - 'Barrel', - 'IronSight', - 'Utility', - 'BottomAttachment', - ]) - ->get(), - function (Attachment $item) { - $this->uploadWiki($item, 'Automatische Erstellung von Waffenbefestigungen'); - } - ); - - return 0; - } - - /** - * @param Attachment $model - */ - protected function prepareTemplate($model): string - { - $pageContent = $this->template; - $type = ($this->typeMapping[$model->attachment_type] ?? $this->typeMapping[$model->sub_type] ?? $model->sub_type); - - if ($model->size === null) { - $pageContent = str_replace( - 'Größe ', - '', - $pageContent - ); - } else { - $pageContent = str_replace( - '', - (string) ($model->getAttributes()['size'] ?? $model->size ?? 0), - $pageContent - ); - } - - $pageContent = str_replace( - ' ', - $type.' ', - $pageContent - ); - - $this->fixText($type, $pageContent); - - return $pageContent; - } - - /** - * @param Attachment $model - */ - protected function getPageName($model): string - { - return $model->name; - } - - /** - * @param Attachment $model - */ - protected function getManufacturerCode($model): string - { - return $model->manufacturer->code; - } -} diff --git a/app/Console/Commands/SC/Wiki/CreateWeaponWikiPages.php b/app/Console/Commands/SC/Wiki/CreateWeaponWikiPages.php deleted file mode 100644 index ad8343b2b..000000000 --- a/app/Console/Commands/SC/Wiki/CreateWeaponWikiPages.php +++ /dev/null @@ -1,180 +0,0 @@ -''' ist ein Größe hergestellt von [[{{subst:MFURN|}}]].{{Cite game|build=[[Star Citizen Alpha 3.22.1|Alpha 3.22.1]]|accessdate=}} -== Beschreibung == -{{Item description}} -== Itemports == -{{Item ports}} -== Statistik == -{{Weapon damage stats}} -== Erwerb == -{{Item availability}} -== Model == -=== Varianten === -{{Item variants}} -{{Quellen}} -{{Navplate manufacturers|}} -{{Navplate personal weapons}} -TEMPLATE; - - protected array $typeMapping = [ - 'Knife' => 'Messer', - 'Railgun' => 'Railgun', - 'Missile Launcher' => 'Raketenwerfer', - 'Grenade Launcher' => 'Granatwerfer', - 'Grenade' => 'Granate', - 'LMG' => 'leichtes Maschinengewehr', - 'Pistol' => 'Pistole', - 'Assault Rifle' => 'Sturmgewehr', - 'Shotgun' => 'Schrotflinte', - 'SMG' => 'Maschinenpistole', - 'Sniper Rifle' => 'Scharfschützengewehr', - 'Medical Device' => 'Medizinalgerät', - 'Utility' => 'Hilfsmittel', - 'Tractor Beam' => 'Traktorstrahl', - 'Frag Pistol' => 'Splitterpistole', - 'Toy Pistol' => 'Spielzeugpistole', - 'Large' => 'große Waffe', - 'Medium' => 'mittlere Waffe', - 'Small' => 'kleine Waffe', - 'Gadget' => 'Hilfsmittel', - ]; - - protected array $classMapping = [ - 'Ballistic' => 'Ballistik', - 'Energy (Laser)' => 'Laserenergie', - 'Laser' => 'Laser', - 'Energy (Plasma)' => 'Plasmaenergie', - 'Electron' => 'Elektronen', - ]; - - /** - * Execute the console command. - */ - public function handle(): int - { - $this->withProgressBar( - PersonalWeapon::all(), - function (PersonalWeapon $item) { - $this->uploadWiki($item, 'Automatische Erstellung von Waffenseiten'); - } - ); - - return 0; - } - - /** - * @param PersonalWeapon $model - */ - protected function prepareTemplate($model): string - { - $pageContent = $this->template; - $type = ($this->typeMapping[$model->weapon_type] ?? $this->typeMapping[$model->sub_type] ?? $model->sub_type); - - $pageContent = str_replace( - '', - (string) $model->size, - $pageContent - ); - - $pageContent = str_replace( - '', - isset($this->classMapping[$model->weapon_class]) ? $this->classMapping[$model->weapon_class].' ' : '', - $pageContent - ); - - $pageContent = str_replace( - '', - $type, - $pageContent - ); - - $this->fixText($type, $pageContent); - - if ($type === 'Messer') { - $pageContent = str_replace("== Statistik ==\n{{Weapon damage stats}}\n", '', $pageContent); - } - - $descriptionDataTargets = [ - 'Magazine Size' => 'Magazingröße', - 'Rate Of Fire' => 'Feuerrate', - 'Effective Range' => 'effektive Reichweite', - ]; - $dataFragments = []; - - foreach ($descriptionDataTargets as $target => $desc) { - $data = $model->getDescriptionDatum($target); - if ($data !== null) { - $line = sprintf('%s von %s', $desc, $data); - if ($target === 'Magazine Size') { - if ($data === 'Integrated Battery') { - $line = 'integrierte Batterie'; - } else { - $line .= ' Schuss'; - } - } - $dataFragments[] = $line; - } - } - - if (! empty($dataFragments)) { - $last = array_pop($dataFragments); - $dataFragments = sprintf( - ' Die Waffe besitzt eine %s, und eine %s.', - implode(', eine ', $dataFragments), - $last - ); - } else { - $dataFragments = ''; - } - - $pageContent = str_replace( - '', - $dataFragments, - $pageContent - ); - - $this->fixText($type, $pageContent); - - return $pageContent; - } - - /** - * @param PersonalWeapon $model - */ - protected function getPageName($model): string - { - return $model->name; - } - - /** - * @param PersonalWeapon $model - */ - protected function getManufacturerCode($model): string - { - return $model->manufacturer->code; - } -} diff --git a/app/Console/Commands/SC/Wiki/UploadItemImages.php b/app/Console/Commands/SC/Wiki/UploadItemImages.php deleted file mode 100644 index 0f0a6bbc3..000000000 --- a/app/Console/Commands/SC/Wiki/UploadItemImages.php +++ /dev/null @@ -1,248 +0,0 @@ - 'Armpanzerung', - 'Char_Armor_Torso' => 'Oberkörperpanzerung', - 'Char_Armor_Legs' => 'Beinpanzerung', - 'Char_Armor_Helmet' => 'Helm', - 'Char_Armor_Backpack' => 'Rucksack', - 'Char_Armor_Undersuit' => 'Unteranzug', - 'Char_Clothing_Torso_1' => 'Jacke', - 'Char_Clothing_Legs' => 'Hose', - 'Char_Clothing_Torso_0' => 'Shirt', - 'Char_Clothing_Feet' => 'Schuh', - 'Char_Clothing_Hat' => 'Hut', - 'Char_Clothing_Hands' => 'Handschuh', - 'Char_Clothing_Torso_2' => 'Gürtel', - 'Char_Clothing_Backpack' => 'Rucksack', - - 'Cooler' => 'Kühler', - 'Power Plant' => 'Generator', - 'Quantum Drive' => 'Quantenantrieb', - 'Shield Generator' => 'Schildgenerator', - 'WeaponGun' => 'Fahrzeugwaffe', - - 'Magazine' => 'Magazin', - 'Ballistic Compensator' => 'Ballistischer Kompensator', - 'Flash Hider' => 'Mündungsfeuerdämpfer', - 'Energy Stabilizer' => 'Energie-Stabilisator', - 'Suppressor' => 'Schalldämpfer', - 'Scope' => 'Zielfernrohr', - 'MedGel Refill' => 'MedGel-Nachfüllpackung', - 'Multi-Tool Attachment' => 'Multi-Tool-Aufsatz', - 'Battery' => 'Batterie', - 'Flashlight' => 'Taschenlampe', - 'Laser Pointer' => 'Laserpointer', - - 'Light Backpack' => 'Leichter Rucksack', - 'Medium Backpack' => 'Mittlerer Rucksack', - 'Heavy Backpack' => 'Schwerer Rucksack', - - 'Backpack' => 'Rucksack', - 'Bandana' => 'Bandana', - 'Beanie' => 'Mütze', - 'Boots' => 'Stiefel', - 'Gloves' => 'Handschuh', - 'Gown' => 'Kittel', - 'Hat' => 'Hut', - 'Head Cover' => 'Kopfbedeckung', - 'Jacket' => 'Jacke', - 'Pants' => 'Hose', - 'Shirt' => 'Hemd', - 'Shoes' => 'Schuh', - 'Slippers' => 'Hausschuhe', - 'Sweater' => 'Pullover', - 'T-Shirt' => 'T-Shirt', - 'Unknown Type' => 'Unbekannter Typ', - - 'Food' => 'Lebensmittel', - 'Drink' => 'Getränk', - ]; - - /** - * Upload images for armor parts, personal weapons and ship items - */ - public function handle(): int - { - $this->http = Http::baseUrl(config('services.item_thumbnail_url')); - $this->upload = new UploadWikiImage(true); - - $this->info('Uploading Char Armor Images...'); - $this->withProgressBar(Armor::all(), function (Armor $armor) { - $this->work($armor, true); - }); - - $this->info('Uploading Clothing Images...'); - $this->withProgressBar(Clothes::all(), function (Clothes $armor) { - $this->work($armor, true); - }); - - $this->info('Uploading Weapon Personal Images...'); - $this->withProgressBar(PersonalWeapon::all(), function (PersonalWeapon $armor) { - $this->work($armor, true); - }); - - $this->info('Uploading Weapon Attachment Images...'); - $this->withProgressBar(Attachment::all(), function (Attachment $armor) { - $this->work($armor, true); - }); - - $this->info('Uploading Food Images...'); - $this->withProgressBar(Food::all(), function (Food $armor) { - $this->work($armor, true); - }); - - $this->info('Uploading Ship Item Images...'); - $this->withProgressBar(VehicleItem::all(), function (VehicleItem $armor) { - $this->work($armor, true); - }); - - $this->info('Done'); - - return 0; - } - - private function work($item, bool $normalizeCategory = false): void - { - if ($item instanceof CommodityItem) { - $item = $item->item; - } - - $url = sprintf('%s.jpg', $item->uuid); - - $this->headResponse = $this->http->head($url); - if (! $this->headResponse->successful()) { - return; - } - - $name = preg_replace('/[^\w-]/', ' ', $item->name); - $name = trim(preg_replace('/\s+/', ' ', $name)); - - if ($item instanceof Clothing) { - $name = CreateCharArmorWikiPages::getNameForModel($item); - } - - if (str_contains($name, '+')) { - $name = str_replace('+', ' (Plus)', $name); - } - - $source = sprintf('%s%s', config('services.item_thumbnail_url'), $url); - - $metadata = [ - 'filesize' => $this->headResponse->header('Content-Length'), - 'date' => $this->headResponse->header('Last-Modified'), - 'sources' => $source, - ]; - - $categories = [ - sprintf('{{subst:MFURN|%s}}', $item->manufacturer->code), - ]; - - if ($normalizeCategory) { - $this->normalizeCategory($item, $name, $metadata, $categories); - } else { - $categories[] = $name; - } - - if (! isset($metadata['description'])) { - $metadata['description'] = sprintf( - '[[%s]] vom Hersteller [[{{subst:MFURN|%s}}]]', - $name, - $item->manufacturer->code, - ); - } - - if (isset($this->typeTranslations[$item->type])) { - $categories[] = $this->typeTranslations[$item->type]; - - $type = $this->typeTranslations[$item->type]; - if ($item->type === 'WeaponGun') { - $type = 'Fahrzeugwaffe'; - } - - $metadata['description'] = sprintf( - '%s [[%s]] vom Hersteller [[{{subst:MFURN|%s}}]]', - $type, - $name, - $item->manufacturer->code, - ); - } - - $categories = collect($categories)->map(function ($category) { - return sprintf('[[Kategorie:%s]]', $category); - })->implode("\n"); - - try { - $this->upload->upload(sprintf('%s.jpg', $name), $source, $metadata, $categories); - } catch (Exception $e) { - $this->error($e->getMessage()); - } - } - - /** - * Removes the color from the items name - * Adds categories and a description - * - * @param CommodityItem $item - */ - private function normalizeCategory($item, string $name, array &$metadata, array &$categories): void - { - if (isset($this->typeTranslations[$item->type])) { - $categories[] = $this->typeTranslations[$item->type]; - - $metadata['description'] = sprintf( - '%s [[%s]] vom Hersteller [[{{subst:MFURN|%s}}]]', - $this->typeTranslations[$item->type], - $name, - $item->manufacturer->code, - ); - } - } -} diff --git a/app/Console/Commands/StarCitizen/Galactapedia/ImportArticleProperties.php b/app/Console/Commands/StarCitizen/Galactapedia/ImportArticleProperties.php deleted file mode 100644 index 35ab074a5..000000000 --- a/app/Console/Commands/StarCitizen/Galactapedia/ImportArticleProperties.php +++ /dev/null @@ -1,44 +0,0 @@ -with('templates')->get(); - - $this->createProgressBar($articles->count()); - - $articles - ->each(function (Article $article) { - ImportArticleProperty::dispatch($article); - $this->advanceBar(); - }); - - return 0; - } -} diff --git a/app/Console/Commands/StarCitizen/Galactapedia/ImportArticles.php b/app/Console/Commands/StarCitizen/Galactapedia/ImportArticles.php deleted file mode 100644 index 457b5eefd..000000000 --- a/app/Console/Commands/StarCitizen/Galactapedia/ImportArticles.php +++ /dev/null @@ -1,34 +0,0 @@ -info('Dispatching Galactapedia Sync'); + + SyncGalactapediaJob::dispatch(); + FilterCache::bust(FilterCache::NAMESPACE_GALACTAPEDIA); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/StarCitizen/Galactapedia/TranslateArticles.php b/app/Console/Commands/StarCitizen/Galactapedia/TranslateArticles.php index b2af12714..63bc8a955 100644 --- a/app/Console/Commands/StarCitizen/Galactapedia/TranslateArticles.php +++ b/app/Console/Commands/StarCitizen/Galactapedia/TranslateArticles.php @@ -4,14 +4,13 @@ namespace App\Console\Commands\StarCitizen\Galactapedia; -use App\Console\Commands\AbstractQueueCommand; use App\Jobs\StarCitizen\Galactapedia\TranslateArticle; use App\Models\StarCitizen\Galactapedia\Article; use App\Models\System\Language; -use Illuminate\Database\Eloquent\Builder; +use Illuminate\Console\Command; use Illuminate\Database\Eloquent\Collection; -class TranslateArticles extends AbstractQueueCommand +class TranslateArticles extends Command { /** * The name and signature of the console command. @@ -33,25 +32,10 @@ class TranslateArticles extends AbstractQueueCommand public function handle(): int { Article::query() - ->whereHas( - 'translations', - function (Builder $query) { - $query - ->where('locale_code', Language::ENGLISH) - ->whereRaw("translation <> ''"); - } - ) - ->chunk( - 100, - function (Collection $articles) { - $articles->each( - function (Article $article) { - TranslateArticle::dispatch($article); - } - ); - } - ); + ->whereNotNull('translation->'.Language::ENGLISH) + ->where('translation->'.Language::ENGLISH, '!=', '') + ->chunk(100, fn (Collection $articles) => $articles->each(fn (Article $article) => TranslateArticle::dispatch($article))); - return 0; + return Command::SUCCESS; } } diff --git a/app/Console/Commands/StarCitizen/Galactapedia/Wiki/ApproveArticles.php b/app/Console/Commands/StarCitizen/Galactapedia/Wiki/ApproveArticles.php deleted file mode 100644 index 3d171c68d..000000000 --- a/app/Console/Commands/StarCitizen/Galactapedia/Wiki/ApproveArticles.php +++ /dev/null @@ -1,51 +0,0 @@ -get() - ->map(function (Article $article) { - return $article->title; - }) - ->chunk(25) - ->each(function (Collection $chunk) { - dispatch(new ApproveRevisions($chunk->toArray(), false, true)); - }); - - return 0; - } -} diff --git a/app/Console/Commands/StarCitizen/Galactapedia/Wiki/CreateWikiPages.php b/app/Console/Commands/StarCitizen/Galactapedia/Wiki/CreateWikiPages.php deleted file mode 100644 index 25500f4f0..000000000 --- a/app/Console/Commands/StarCitizen/Galactapedia/Wiki/CreateWikiPages.php +++ /dev/null @@ -1,37 +0,0 @@ -info('Dispatching Galactapedia Wiki Page Creation'); - - dispatch(new CreateGalactapediaWikiPages); - - return 0; - } -} diff --git a/app/Console/Commands/StarCitizen/Galactapedia/Wiki/UploadImages.php b/app/Console/Commands/StarCitizen/Galactapedia/Wiki/UploadImages.php deleted file mode 100644 index e2dd2327f..000000000 --- a/app/Console/Commands/StarCitizen/Galactapedia/Wiki/UploadImages.php +++ /dev/null @@ -1,48 +0,0 @@ -info('Dispatching Galactapedia image upload'); - - $articles = Article::all(); - - $this->createProgressBar($articles->count()); - - Article::all()->chunk(100)->each(function (Collection $collection) { - $collection->each(function (Article $article) { - UploadGalactapediaWikiImages::dispatch($article); - $this->advanceBar(); - }); - }); - - return 0; - } -} diff --git a/app/Console/Commands/StarCitizen/ShipMatrix/DownloadShipMatrix.php b/app/Console/Commands/StarCitizen/ShipMatrix/DownloadShipMatrix.php deleted file mode 100644 index f95cb34d6..000000000 --- a/app/Console/Commands/StarCitizen/ShipMatrix/DownloadShipMatrix.php +++ /dev/null @@ -1,64 +0,0 @@ -dispatcher = $dispatcher; - } - - /** - * Execute the console command. - */ - public function handle(): int - { - if ($this->option('import') === true) { - $this->info('Downloading Ship Matrix and starting import'); - DownloadShipMatrixJob::withChain( - [ - new CheckShipMatrixStructure, - new ImportShipMatrix, - ] - )->dispatch(); - } else { - $this->info('Dispatching Ship Matrix Download Job'); - $this->dispatcher->dispatch(new DownloadShipMatrixJob); - } - - return 0; - } -} diff --git a/app/Console/Commands/StarCitizen/ShipMatrix/ImportShipMatrix.php b/app/Console/Commands/StarCitizen/ShipMatrix/ImportShipMatrix.php deleted file mode 100644 index 60a07549d..000000000 --- a/app/Console/Commands/StarCitizen/ShipMatrix/ImportShipMatrix.php +++ /dev/null @@ -1,45 +0,0 @@ -info('Dispatching Ship Matrix Parsing Job'); - - $file = $this->option('file'); - if ($file !== null) { - $file = explode('vehicles', $this->option('file'))[1] ?? null; - } - - ImportShipMatrixJob::dispatch($file); - - return 0; - } -} diff --git a/app/Console/Commands/StarCitizen/Starmap/DownloadStarmap.php b/app/Console/Commands/StarCitizen/Starmap/DownloadStarmap.php deleted file mode 100644 index 7712598a7..000000000 --- a/app/Console/Commands/StarCitizen/Starmap/DownloadStarmap.php +++ /dev/null @@ -1,75 +0,0 @@ -dispatcher = $dispatcher; - } - - /** - * Execute the console command. - */ - public function handle(): int - { - $this->info('Dispatching Starmap Download'); - - $this->createDiskIfNotExists(); - - $this->dispatcher->dispatch(new DownloadStarmapJob($this->option('force') === true)); - - if ($this->option('import') === true) { - $this->info('Starting Import'); - $this->dispatcher->dispatch(new ImportStarmap); - } - - return 0; - } - - /** - * Create the starmap directory if it does not exist - */ - private function createDiskIfNotExists(): void - { - if (! File::exists(config('filesystems.disks.starmap.root'))) { - Storage::makeDirectory(config('filesystems.disks.starmap.root')); - } - } -} diff --git a/app/Console/Commands/StarCitizen/Starmap/ImportStarmap.php b/app/Console/Commands/StarCitizen/Starmap/ImportStarmap.php deleted file mode 100644 index 6edd32d1e..000000000 --- a/app/Console/Commands/StarCitizen/Starmap/ImportStarmap.php +++ /dev/null @@ -1,35 +0,0 @@ -info('Importing starmap'); - - ImportStarmapJob::dispatch(); - - return 0; - } -} diff --git a/app/Console/Commands/StarCitizen/Starmap/SyncStarmap.php b/app/Console/Commands/StarCitizen/Starmap/SyncStarmap.php new file mode 100644 index 000000000..fa3a99adc --- /dev/null +++ b/app/Console/Commands/StarCitizen/Starmap/SyncStarmap.php @@ -0,0 +1,39 @@ +info('Dispatching Starmap Sync'); + + SyncStarmapJob::dispatch(); + FilterCache::bust(FilterCache::NAMESPACE_STARSYSTEMS); + + return 0; + } +} diff --git a/app/Console/Commands/StarCitizen/Starmap/TranslateSystems.php b/app/Console/Commands/StarCitizen/Starmap/TranslateSystems.php index e84f51097..3bc10f0ee 100644 --- a/app/Console/Commands/StarCitizen/Starmap/TranslateSystems.php +++ b/app/Console/Commands/StarCitizen/Starmap/TranslateSystems.php @@ -4,9 +4,9 @@ namespace App\Console\Commands\StarCitizen\Starmap; -use App\Console\Commands\AbstractQueueCommand as QueueCommand; +use Illuminate\Console\Command; -class TranslateSystems extends QueueCommand +class TranslateSystems extends Command { /** * The name and signature of the console command. diff --git a/app/Console/Commands/StarCitizen/Stat/DownloadStats.php b/app/Console/Commands/StarCitizen/Stat/DownloadStats.php deleted file mode 100644 index a4db51aac..000000000 --- a/app/Console/Commands/StarCitizen/Stat/DownloadStats.php +++ /dev/null @@ -1,62 +0,0 @@ -dispatcher = $dispatcher; - } - - /** - * Execute the console command. - */ - public function handle(): int - { - if ($this->option('import') === true) { - $this->info('Downloading funding statistics and starting import'); - DownloadStatsJob::withChain( - [ - new ImportStat, - ] - )->dispatch(); - } else { - $this->info('Starting funding statistics download'); - $this->dispatcher->dispatch(new DownloadStatsJob); - } - - return 0; - } -} diff --git a/app/Console/Commands/StarCitizen/Stat/ImportStats.php b/app/Console/Commands/StarCitizen/Stat/ImportStats.php deleted file mode 100644 index a6cc77c2b..000000000 --- a/app/Console/Commands/StarCitizen/Stat/ImportStats.php +++ /dev/null @@ -1,53 +0,0 @@ -dispatcher = $dispatcher; - } - - /** - * Execute the console command. - */ - public function handle(): int - { - $this->info('Starting funding statistics import'); - $this->dispatcher->dispatch(new ImportStat); - - return 0; - } -} diff --git a/app/Console/Commands/StarCitizen/SyncStats.php b/app/Console/Commands/StarCitizen/SyncStats.php new file mode 100644 index 000000000..948f5046d --- /dev/null +++ b/app/Console/Commands/StarCitizen/SyncStats.php @@ -0,0 +1,37 @@ +info('Dispatching Stats Sync'); + + SyncStatsJob::dispatch(); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/StarCitizen/Vehicle/ImportLoaner.php b/app/Console/Commands/StarCitizen/Vehicle/ImportLoaner.php index 6204d8bf8..34118bf65 100644 --- a/app/Console/Commands/StarCitizen/Vehicle/ImportLoaner.php +++ b/app/Console/Commands/StarCitizen/Vehicle/ImportLoaner.php @@ -2,9 +2,9 @@ namespace App\Console\Commands\StarCitizen\Vehicle; -use App\Console\Commands\AbstractQueueCommand; +use Illuminate\Console\Command; -class ImportLoaner extends AbstractQueueCommand +class ImportLoaner extends Command { /** * The name and signature of the console command. @@ -27,7 +27,7 @@ public function handle(): int { $this->info('Importing Loaners'); - \App\Jobs\StarCitizen\Vehicle\Import\ImportLoaner::dispatch(); + \App\Jobs\StarCitizen\Vehicle\ImportLoaner::dispatch(); return 0; } diff --git a/app/Console/Commands/StarCitizen/Vehicle/ImportMsrp.php b/app/Console/Commands/StarCitizen/Vehicle/ImportMsrp.php index 360b9500c..4353daab3 100644 --- a/app/Console/Commands/StarCitizen/Vehicle/ImportMsrp.php +++ b/app/Console/Commands/StarCitizen/Vehicle/ImportMsrp.php @@ -2,10 +2,10 @@ namespace App\Console\Commands\StarCitizen\Vehicle; -use App\Console\Commands\AbstractQueueCommand; -use App\Jobs\StarCitizen\Vehicle\Import\ImportMsrp as ImportMsrpJob; +use App\Jobs\StarCitizen\Vehicle\ImportMsrp as ImportMsrpJob; +use Illuminate\Console\Command; -class ImportMsrp extends AbstractQueueCommand +class ImportMsrp extends Command { /** * The name and signature of the console command. diff --git a/app/Console/Commands/StarCitizen/Vehicle/ImportShipMatrix.php b/app/Console/Commands/StarCitizen/Vehicle/ImportShipMatrix.php new file mode 100644 index 000000000..fbd6e239f --- /dev/null +++ b/app/Console/Commands/StarCitizen/Vehicle/ImportShipMatrix.php @@ -0,0 +1,39 @@ +info('Dispatching Ship Matrix Download and Import Job'); + + ImportShipMatrixJob::dispatch(); + FilterCache::bust(FilterCache::NAMESPACE_SHIPMATRIX); + + return 0; + } +} diff --git a/app/Console/Commands/Transcript/ImportMetadata.php b/app/Console/Commands/Transcript/ImportMetadata.php deleted file mode 100644 index b69194bd3..000000000 --- a/app/Console/Commands/Transcript/ImportMetadata.php +++ /dev/null @@ -1,35 +0,0 @@ -option('chunkAll')); - - return 0; - } -} diff --git a/app/Console/Commands/Transcript/TranslateTranscripts.php b/app/Console/Commands/Transcript/TranslateTranscripts.php deleted file mode 100644 index c9df7f754..000000000 --- a/app/Console/Commands/Transcript/TranslateTranscripts.php +++ /dev/null @@ -1,39 +0,0 @@ -info('Dispatching Transcript Translation'); - if (! $this->hasArgument('limit')) { - dispatch(new \App\Jobs\Transcript\Translate\TranslateTranscripts); - } else { - dispatch(new \App\Jobs\Transcript\Translate\TranslateTranscripts((int) $this->argument('limit'))); - } - - return 0; - } -} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php deleted file mode 100644 index 2e08f6c38..000000000 --- a/app/Console/Kernel.php +++ /dev/null @@ -1,286 +0,0 @@ -schedule = $schedule; - - $this->scheduleStatJobs(); - - if (config('schedule.ship_matrix.enabled')) { - $this->scheduleVehicleJobs(); - } - - if (config('schedule.comm_links.enabled')) { - $this->scheduleCommLinkJobs(); - } - - if (config('schedule.starmap.enabled')) { - $this->scheduleStarmapJobs(); - } - - if (config('schedule.galactapedia.enabled')) { - $this->scheduleGalactapediaJobs(); - } - } - - /** - * Register the Closure based commands for the application. - */ - protected function commands(): void - { - require base_path('routes/console.php'); - } - - /** - * Stat related Jobs. - */ - private function scheduleStatJobs(): void - { - $this->schedule - ->command(DownloadStats::class, ['--import']) - ->dailyAt('20:00'); - } - - /** - * Comm-Link related Jobs. - */ - private function scheduleCommLinkJobs(): void - { - /* Check for new Comm-Links */ - $this->schedule - ->command(CommLinkSchedule::class) - ->everyFifteenMinutes(); - - /* Run CommLink Notification only once each day */ - $this->schedule->call(function () { - NewCommLinksDownloaded::dispatch(); - })->dailyAt('18:00'); - - /* Re-Download all Comm-Links monthly */ - $this->schedule - ->command(ReDownloadCommLinks::class, ['--skip=false']) - ->monthly() - ->after( - function () { - CommLinksChangedEvent::dispatch(); - } - ); - - /* Download Comm-Link Images */ - if (config('schedule.comm_links.download_local') === true) { - $this->schedule->command(DownloadCommLinkImages::class)->daily()->withoutOverlapping(); - } - - /* Update Proof Read Status */ - $this->schedule - ->job(UpdateCommLinkProofReadStatus::class) - ->daily() - ->withoutOverlapping(); - } - - /** - * Ship Matrix related Jobs. - */ - private function scheduleVehicleJobs(): void - { - $hours = config('schedule.ship_matrix.at', []); - // Ensure first and second key exists - $hours = array_merge([1, 13], $hours); - - $this->schedule - ->command(DownloadShipMatrix::class, ['--import']) - ->twiceDaily( - $hours[0], - $hours[1], - ); - - $this->schedule - ->command(ImportMsrp::class) - ->daily(); - - $this->schedule - ->command(ImportLoaner::class) - ->daily(); - } - - /** - * Starmap download and import job - */ - private function scheduleStarmapJobs(): void - { - $this->schedule - ->command(DownloadStarmap::class, ['--import']) - ->monthly(); - } - - /** - * Galactapedia Jobs - */ - private function scheduleGalactapediaJobs(): void - { - $this->schedule - ->command(ImportCategories::class) - ->onSuccess(function () { - $this->call(ImportArticles::class); - }) - ->dailyAt('2:00') - ->withoutOverlapping(); - - $this->schedule - ->command(ImportArticleProperties::class) - ->dailyAt('2:30') - ->withoutOverlapping(); - - $this->schedule - ->command(TranslateArticles::class) - ->dailyAt('3:00') - ->withoutOverlapping(); - - if (config('schedule.galactapedia.create_wiki_pages')) { - $this->schedule - ->command(CreateWikiPages::class) - ->dailyAt('3:30') - ->withoutOverlapping(); - } - } -} diff --git a/app/Contracts/HasChangelogsInterface.php b/app/Contracts/HasChangelogsInterface.php deleted file mode 100644 index 82b554d0b..000000000 --- a/app/Contracts/HasChangelogsInterface.php +++ /dev/null @@ -1,21 +0,0 @@ -request = $request; - } -} diff --git a/app/Events/ModelUpdating.php b/app/Events/ModelUpdating.php deleted file mode 100644 index 7b4748ad6..000000000 --- a/app/Events/ModelUpdating.php +++ /dev/null @@ -1,33 +0,0 @@ -model = $model; - } -} diff --git a/app/Events/Rsi/CommLink/CommLinksChanged.php b/app/Events/Rsi/CommLink/CommLinksChanged.php deleted file mode 100644 index 280d0aa3d..000000000 --- a/app/Events/Rsi/CommLink/CommLinksChanged.php +++ /dev/null @@ -1,33 +0,0 @@ -commLinks = CommLinksChangedModel::query()->where('type', 'update')->get(); - - CommLinksChangedModel::query()->where('type', 'update')->delete(); - } -} diff --git a/app/Events/Rsi/CommLink/NewCommLinksDownloaded.php b/app/Events/Rsi/CommLink/NewCommLinksDownloaded.php deleted file mode 100644 index 5f8c991de..000000000 --- a/app/Events/Rsi/CommLink/NewCommLinksDownloaded.php +++ /dev/null @@ -1,33 +0,0 @@ -commLinks = CommLinkChangedModel::query()->where('type', 'creation')->get(); - - CommLinkChangedModel::query()->where('type', 'creation')->delete(); - } -} diff --git a/app/Events/StarCitizen/ShipMatrix/ShipMatrixStructureChanged.php b/app/Events/StarCitizen/ShipMatrix/ShipMatrixStructureChanged.php deleted file mode 100644 index 3c069b5eb..000000000 --- a/app/Events/StarCitizen/ShipMatrix/ShipMatrixStructureChanged.php +++ /dev/null @@ -1,16 +0,0 @@ -is('api*')) { - return $this->returnApiResponse($request, $exception); - } - - return parent::render($request, $exception); - } - - /** - * Convert an authentication exception into an unauthenticated response. - * - * @param \Illuminate\Http\Request $request The HTTP Request - * @param \Illuminate\Auth\AuthenticationException $exception The Auth Exception - * @return \Illuminate\Http\Response - */ - protected function unauthenticated($request, AuthenticationException $exception) - { - if ($request->expectsJson()) { - return response()->json(['error' => 'Unauthenticated.'], 401); - } - - return redirect()->guest('login'); - } - - /** - * @param \Illuminate\Http\Request $request - */ - protected function wantsJson($request): bool - { - return $request->wantsJson() || $request->query('format', null) === 'json'; - } - - /** - * Get the status code from the exception. - */ - protected function getStatusCode(Throwable $exception): int - { - $statusCode = null; - - if ($exception instanceof ValidationException) { - $statusCode = $exception->status; - } elseif ($exception instanceof HttpExceptionInterface) { - $statusCode = $exception->getStatusCode(); - } else { - // By default throw 500 - $statusCode = 500; - } - - // Be extra defensive - if ($statusCode < 100 || $statusCode > 599) { - $statusCode = 500; - } - - return $statusCode; - } - - private function returnApiResponse($request, Throwable $exception) - { - $request->headers->set('Accept', 'application/json'); - - if (config('app.debug')) { - return parent::render($request, $exception); - } - - return new Response([ - 'code' => $this->getStatusCode($exception), - 'message' => $exception->getMessage(), - ], $this->getStatusCode($exception)); - } -} diff --git a/app/Exceptions/InvalidDataException.php b/app/Exceptions/InvalidDataException.php deleted file mode 100644 index 32a39df26..000000000 --- a/app/Exceptions/InvalidDataException.php +++ /dev/null @@ -1,13 +0,0 @@ - User::query()->count(), + 'totalJobs' => DB::table('jobs')->count(), + 'failedJobs' => DB::table('failed_jobs')->count(), + ]; + + return view('admin.dashboard.index', compact('stats')); + } +} diff --git a/app/Http/Controllers/Admin/FailedJobController.php b/app/Http/Controllers/Admin/FailedJobController.php new file mode 100644 index 000000000..d221651fe --- /dev/null +++ b/app/Http/Controllers/Admin/FailedJobController.php @@ -0,0 +1,38 @@ +orderBy('failed_at', 'desc') + ->paginate(25); + + return view('admin.jobs.index', compact('jobs')); + } + + public function destroy(int $id): RedirectResponse + { + DB::table('failed_jobs')->where('id', $id)->delete(); + + return redirect()->route('admin.jobs.index') + ->with('success', 'Failed job deleted successfully.'); + } + + public function truncate(): RedirectResponse + { + DB::table('failed_jobs')->truncate(); + + return redirect()->route('admin.jobs.index') + ->with('success', 'All failed jobs have been deleted.'); + } +} diff --git a/app/Http/Controllers/Admin/GameVersionController.php b/app/Http/Controllers/Admin/GameVersionController.php new file mode 100644 index 000000000..5ba9050a9 --- /dev/null +++ b/app/Http/Controllers/Admin/GameVersionController.php @@ -0,0 +1,34 @@ +orderBy('released_at', 'desc') + ->get(); + + return view('admin.game-versions.index', compact('versions')); + } + + public function setDefault(GameVersion $gameVersion): RedirectResponse + { + DB::transaction(static function () use ($gameVersion) { + GameVersion::query()->update(['is_default' => false]); + $gameVersion->update(['is_default' => true]); + }); + + return redirect()->route('admin.game-versions.index') + ->with('success', "Game version {$gameVersion->code} set as default."); + } +} diff --git a/app/Http/Controllers/Admin/TranslationController.php b/app/Http/Controllers/Admin/TranslationController.php new file mode 100644 index 000000000..e409a7936 --- /dev/null +++ b/app/Http/Controllers/Admin/TranslationController.php @@ -0,0 +1,79 @@ +select('id', 'cig_id', 'title', 'translation') + ->orderBy('cig_id', 'desc') + ->paginate(25, ['*'], 'commlinks_page'); + + $articles = Article::query() + ->select('id', 'cig_id', 'title', 'translation') + ->orderBy('created_at', 'desc') + ->paginate(25, ['*'], 'articles_page'); + + $smSizes = Size::query()->get(); + $smFocuses = Focus::query()->get(); + $smTypes = Type::query()->get(); + + return view('admin.translations.index', compact('commLinks', 'articles', 'smSizes', 'smFocuses', 'smTypes')); + } + + public function edit(string $type, int $id): View + { + $model = $this->resolveModel($type, $id); + + $translations = [ + Language::ENGLISH => $model->getTranslation('translation', Language::ENGLISH, false) ?? '', + Language::GERMAN => $model->getTranslation('translation', Language::GERMAN, false) ?? '', + Language::CHINESE => $model->getTranslation('translation', Language::CHINESE, false) ?? '', + ]; + + return view('admin.translations.edit', compact('model', 'type', 'translations')); + } + + public function update(UpdateTranslationRequest $request, string $type, int $id): RedirectResponse + { + $model = $this->resolveModel($type, $id); + + $translationsData = array_filter($request->input('translations'), function ($translation) { + return ! empty($translation); + }); + + $model->setTranslations('translation', $translationsData); + $model->save(); + + return redirect()->route('admin.translations.index') + ->with('success', 'Translations updated successfully.'); + } + + private function resolveModel(string $type, int $id): Model + { + return match ($type) { + 'comm-link' => CommLink::findOrFail($id), + 'article' => Article::findOrFail($id), + 'smSize' => Size::findOrFail($id), + 'smFocus' => Focus::findOrFail($id), + 'smType' => Type::findOrFail($id), + default => abort(404), + }; + } +} diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php new file mode 100644 index 000000000..e6221d348 --- /dev/null +++ b/app/Http/Controllers/Admin/UserController.php @@ -0,0 +1,35 @@ +orderBy('created_at', 'desc') + ->paginate(25); + + return view('admin.users.index', compact('users')); + } + + public function destroy(User $user): RedirectResponse + { + if ($user->id === auth()->id()) { + return redirect()->route('admin.users.index') + ->with('error', 'You cannot delete your own account.'); + } + + $user->delete(); + + return redirect()->route('admin.users.index') + ->with('success', 'User deleted successfully.'); + } +} diff --git a/app/Http/Controllers/Api/AbstractApiController.php b/app/Http/Controllers/Api/AbstractApiController.php deleted file mode 100644 index 19f3dc591..000000000 --- a/app/Http/Controllers/Api/AbstractApiController.php +++ /dev/null @@ -1,336 +0,0 @@ -request = $request; - $this->manager = new Manager; - $this->manager->parseIncludes($request->get('include', '')); - - $this->processRequestParams(); - } - - /** - * Processes all possible Request Parameters - */ - protected function processRequestParams(): void - { - $this->processLimit(); - //$this->processIncludes(); - $this->processLocale(); - } - - /** - * Processes the 'limit' Request-Parameter - */ - private function processLimit(): void - { - if ($this->request->has(self::LIMIT) && $this->request->get(self::LIMIT, null) !== null) { - $itemLimit = (int) $this->request->get(self::LIMIT); - - if ($itemLimit > 0) { - $this->limit = $itemLimit; - } elseif ($itemLimit === 0) { - $this->limit = 0; - } else { - $this->errors[self::LIMIT] = static::INVALID_LIMIT_STRING; - } - } - } - - /** - * Processes the 'locale' Request-Parameter - */ - private function processLocale(): void - { - if ($this->request->has(self::LOCALE) && $this->request->get(self::LOCALE, null) !== null) { - $this->setLocale($this->request->get(self::LOCALE)); - } - } - - /** - * Set the Locale - */ - protected function setLocale(string $localeCode): void - { - if (in_array($localeCode, config('language.codes'), true)) { - $this->localeCode = $localeCode; - - if ($this->transformer instanceof LocalizableTransformerInterface) { - $this->transformer->setLocale($localeCode); - } - } else { - $this->errors[self::LOCALE] = sprintf(static::INVALID_LOCALE_STRING, $localeCode); - } - } - - /** - * Disables the pagination by setting the limit to 0 - * - * @return $this - */ - protected function disablePagination(): self - { - $this->limit = 0; - - return $this; - } - - /** - * Creates the API Response, Collection if no pagination, Paginator if a limit is set - * Item if a single model is given - * - * @param Builder|Model|Collection $query - */ - protected function getResponse($query): Response - { - if ($query === null) { - $query = collect(); - } - - if ($query instanceof Model) { - $resource = new Item($query, $this->transformer); - $resource->setMeta($this->getMeta()); - - $datum = $this->manager->createData($resource); - - return new Response($datum, 200); - } - - if ($this->limit === 0 || $query instanceof Collection) { - if ($query instanceof Builder) { - $query = $query->get(); - } - - $resource = new \League\Fractal\Resource\Collection($query, $this->transformer); - $resource->setMeta($this->getMeta()); - - return new Response($this->manager->createData($resource), 200); - } - - $paginate = $query->paginate($this->limit); - - ApiRouteCalled::dispatch([ - 'url' => $this->request->fullUrl(), - 'user-agent' => $this->request->userAgent() ?? 'Star Citizen Wiki API', - 'forwarded-for' => $this->request->header('X-Forwarded-For', '127.0.0.1'), - ]); - - $resource = new \League\Fractal\Resource\Collection( - $query->get(), - $this->transformer - ); - $resource->setMeta($this->getMeta()); - $resource->setPaginator(new IlluminatePaginatorAdapter($paginate)); - - return new Response($this->manager->createData($resource), 200); - } - - /** - * Generates the Meta Array - * - * @return array Meta Array - */ - protected function getMeta(): array - { - $meta = [ - 'processed_at' => Carbon::now()->toDateTimeString(), - ]; - - if (! empty($this->errors)) { - $meta['errors'] = $this->errors; - } - - if (! empty($this->transformer->getAvailableIncludes())) { - $meta['valid_relations'] = array_map( - 'Illuminate\Support\Str::snake', - $this->transformer->getAvailableIncludes() - ); - } - - return array_merge($meta, $this->extraMeta); - } - - /** - * Processes the 'include' Model Relations Request-Parameter - */ - private function processIncludes(): void - { - if ($this->request->has('include') && $this->request->get('include', null) !== null) { - $this->checkIncludes($this->request->get('include', [])); - } - } - - /** - * Processes the given 'include' model relation key - * - * @param string|array $relations - */ - protected function checkIncludes($relations): void - { - if (! is_array($relations)) { - $relations = explode(',', $relations); - } - - collect($relations)->transform( - static function ($relation) { - return trim($relation); - } - ) - ->transform( - static function ($relation) { - return Str::camel($relation); - } - ) - ->each( - function ($relation) { - if (! in_array($relation, $this->transformer->getAvailableIncludes(), true)) { - $this->errors['include'][] = sprintf(static::INVALID_RELATION_STRING, Str::snake($relation)); - } - } - ); - } - - /** - * Cleans the name for query use - */ - protected function cleanQueryName(string $name): string - { - return str_replace('_', ' ', urldecode($name)); - } -} diff --git a/app/Http/Controllers/Api/Game/Concerns/FiltersJsonColumns.php b/app/Http/Controllers/Api/Game/Concerns/FiltersJsonColumns.php new file mode 100644 index 000000000..8582b25f0 --- /dev/null +++ b/app/Http/Controllers/Api/Game/Concerns/FiltersJsonColumns.php @@ -0,0 +1,149 @@ +jsonExpression($path, $cast); + + return $query->orderByRaw("{$expression} {$direction} nulls last"); + } + ); + } + + /** + * Apply filter to a regular database column with array value support. + * + * Filters out null and empty string values from the input. + * + * @param Builder $query The Eloquent query builder + * @param string $column The column name to filter + * @param mixed $value Single value or array of values to filter by + */ + protected function applyColumnFilter(Builder $query, string $column, mixed $value): void + { + $values = is_array($value) ? $value : [$value]; + $values = array_values(array_filter($values, static fn ($item) => $item !== null && $item !== '')); + + if ($values === []) { + return; + } + + $query->whereIn($column, $values); + } + + /** + * Build a Laravel JSON column path expression. + * + * Converts dot notation to Laravel's arrow notation for JSON queries. + * Example: 'FlightCharacteristics.Speeds.Scm' becomes 'data->FlightCharacteristics->Speeds->Scm' + * + * @param string $baseColumn The base column name (e.g., 'game_vehicle_data.data') + * @param string $path Dot-notation JSON path + * @return string Laravel JSON column expression + */ + protected function laravelJsonColumn(string $baseColumn, string $path): string + { + return $baseColumn.'->'.str_replace('.', '->', $path); + } + + /** + * Apply filter to a JSON field path with optional PostgreSQL casting. + * + * Supports filtering by single or multiple values. Filters out null and empty values. + * Uses Laravel's JSON where clauses for non-cast queries, or raw PostgreSQL expressions for cast queries. + * + * @param Builder $query The Eloquent query builder + * @param string $path Dot-notation JSON path (e.g., 'FlightCharacteristics.Speeds.Scm') + * @param mixed $value Single value or array of values to filter by + * @param string|null $cast PostgreSQL cast type ('numeric', 'text', etc.) or null for no casting + */ + protected function applyJsonFilter(Builder $query, string $path, mixed $value, ?string $cast = null): void + { + $values = is_array($value) ? $value : [$value]; + $values = array_values(array_filter($values, static fn ($item) => $item !== null && $item !== '')); + + if ($values === []) { + return; + } + + if ($cast === null || $cast === '') { + $jsonColumn = $this->laravelJsonColumn($this->getJsonTableName().'.'.$this->getJsonColumnName(), $path); + + $query->whereIn($jsonColumn, $values); + + return; + } + + $expression = $this->jsonExpression($path, $cast); + $query->whereIn(DB::raw($expression), $values); + } + + /** + * Build a PostgreSQL JSONB path expression with optional casting. + * + * Creates a PostgreSQL expression using the #>> operator for text extraction, + * with optional casting to a specific PostgreSQL type. + * + * Example without cast: (game_vehicle_data.data #>> '{FlightCharacteristics,Speeds,Scm}') + * Example with cast: ((game_vehicle_data.data #>> '{FlightCharacteristics,Speeds,Scm}')::numeric) + * + * @param string $path Dot-notation JSON path + * @param string|null $cast PostgreSQL cast type or null for no casting + * @return string PostgreSQL JSONB expression + */ + protected function jsonExpression(string $path, ?string $cast = null): string + { + $segments = array_map('trim', explode('.', $path)); + $pathExpression = implode(',', $segments); + + $expression = $this->getJsonTableName().'.'.$this->getJsonColumnName()." #>> '{".$pathExpression."}'"; + + if ($cast === null || $cast === '') { + return $expression; + } + + return sprintf('(%s)::%s', $expression, $cast); + } +} diff --git a/app/Http/Controllers/Api/Game/ItemController.php b/app/Http/Controllers/Api/Game/ItemController.php new file mode 100644 index 000000000..3fcd4144b --- /dev/null +++ b/app/Http/Controllers/Api/Game/ItemController.php @@ -0,0 +1,611 @@ +gameVersionCode(); + $category = $request->route()->defaults['category'] ?? 'items'; + + return QueryBuilder::for(ItemData::class, $request) + ->forRequestedOrDefaultVersion($versionCode) + ->forCategory($category) + ->allowedFilters($this->allowedFilters()) + ->allowedSorts(array_merge( + [ + 'name', + 'class_name', + 'class', + 'size', + 'grade', + 'type', + 'sub_type', + 'classification', + AllowedSort::custom('manufacturer', new SortByRelation, 'manufacturer.name'), + AllowedSort::custom('manufacturer.name', new SortByRelation, 'manufacturer.name'), + ], + $this->allowedJsonSorts() + )) + ->defaultSort('name') + ->allowedIncludes($this->allowedIncludes()) + ->with(['item', 'gameVersion']); + } + + /** + * Get JSON-backed sort fields from configuration. + * + * @return array + */ + private function allowedJsonSorts(): array + { + $sortConfig = config('sorts.items', []); + $allowedSorts = []; + + foreach ($sortConfig as $sortKey => $config) { + $allowedSorts[] = $this->jsonSort( + $config['path'], // $sortKey, + 'stdItem.'.$config['path'], + $config['cast'] ?? 'numeric' + ); + } + + return $allowedSorts; + } + + /** + * @return array + */ + private function allowedFilters(): array + { + return [ + AllowedFilter::scope('category'), + AllowedFilter::exact('type'), + AllowedFilter::exact('sub_type'), + AllowedFilter::callback('manufacturer', static function ($query, mixed $value): void { + $values = is_array($value) ? $value : [$value]; + + $query->whereHas('manufacturer', static function ($manufacturerQuery) use ($values): void { + $manufacturerQuery + ->whereIn('name', $values) + ->orWhereIn('code', $values); + }); + }), + AllowedFilter::callback('manufacturer.name', static function ($query, mixed $value): void { + $values = is_array($value) ? $value : [$value]; + + $query->whereHas('manufacturer', static function ($manufacturerQuery) use ($values): void { + $manufacturerQuery + ->whereIn('name', $values) + ->orWhereIn('code', $values); + }); + }), + AllowedFilter::partial('class_name'), + AllowedFilter::partial('name'), + AllowedFilter::partial('classification'), + AllowedFilter::exact('size'), + AllowedFilter::exact('grade'), + AllowedFilter::exact('class'), + AllowedFilter::custom('variants', new ItemVariantsFilter), + ]; + } + + #[OA\Get( + path: '/api/items', + description: 'Returns paginated in-game items for the requested category and version with optional filters/includes.', + summary: 'In-Game Item Overview', + tags: ['In-Game', 'Items'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + new OA\Parameter(ref: '#/components/parameters/include'), + new OA\Parameter( + name: 'sort', + in: 'query', + description: 'Sort field. Prefix with "-" for descending. Supports 250+ JSON fields. Examples: name, -grade, weapon.damage.alpha_total, -shield_controller.face_type. Use comma for multiple: grade,-name', + schema: new OA\Schema( + type: 'string', + example: '-weapon.damage.alpha_total' + ) + ), + new OA\Parameter(name: 'filter[variants]', in: 'query', schema: new OA\Schema(type: 'boolean')), + new OA\Parameter(name: 'filter[category]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[type]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[sub_type]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[manufacturer]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[manufacturer.name]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[class_name]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[name]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[classification]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[size]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[grade]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[class]', in: 'query', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of Items', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/game_item') + ) + ), + ] + )] + /** + * Get paginated list of items with optional sorting. + * + * Common sort examples: + * - Basic: ?sort=name, ?sort=-grade, ?sort=size + * - Manufacturer: ?sort=manufacturer.name + * - Weapons: ?sort=-weapon.damage.alpha_total, ?sort=weapon.rate_of_fire + * - Shields: ?sort=-shield.max_health, ?sort=shield_controller.face_type + * - Mining: ?sort=mining_laser.power_transfer, ?sort=-mining_module.charges + * - Power: ?sort=-resource_network.usage.power.maximum + * - Multiple: ?sort=grade,-weapon.damage.alpha_total + */ + public function index(Request $request): AnonymousResourceCollection + { + $versionCode = $this->gameVersionCode(); + + $include = str_replace('related_items', '', $request->input('include', '')); + if (! empty($include)) { + $request->merge(['include' => $include]); + } + + $query = $this->buildBaseQuery($request); + $items = $query->jsonPaginate(); + + return ItemResource::collection( + $this->transformToItems($items, $versionCode) + ); + } + + #[OA\Get( + path: '/api/items/{identifier}', + description: 'Retrieve a specific item by name or UUID with metadata and includes.', + summary: 'In-Game Item Detail', + tags: ['In-Game', 'Items'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/locale'), + new OA\Parameter(ref: '#/components/parameters/include'), + new OA\Parameter( + name: 'identifier', + in: 'path', + required: true, + schema: new OA\Schema( + description: 'Item name or UUID', + type: 'string', + ), + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'An Item', + content: new OA\JsonContent(ref: '#/components/schemas/game_item') + ), + ] + )] + public function show(Request $request, string $identifier): ItemResource|RedirectResponse + { + $original = $identifier; + $versionCode = $this->gameVersionCode(); + $identifier = $this->cleanQueryName($identifier); + $isUuid = Str::isUuid($identifier); + + try { + $itemData = null; + + if ($isUuid) { + $itemData = QueryBuilder::for(ItemData::class, $request) + ->forRequestedOrDefaultVersion($versionCode) + ->whereHas('item', fn (Builder $q) => $q->where('uuid', $identifier)) + ->allowedIncludes($this->allowedIncludes(includeRelatedItems: true)) + ->with(['entityTags', 'item', 'gameVersion', 'baseVariant']) + ->first(); + } + + if ($itemData === null) { + $underscored = str_replace(' ', '_', $identifier); + $itemData = QueryBuilder::for(ItemData::class, $request) + ->forRequestedOrDefaultVersion($versionCode) + ->where(function (Builder $q) use ($identifier, $underscored, $original) { + $q->where('name', $identifier) + ->orWhereRaw('upper(name) = ?', [strtoupper($identifier)]) + ->orWhere('class_name', $underscored) + ->orWhereRaw('upper(class_name) = ?', [strtoupper($original)]) + ->orWhere('class_name', 'LIKE', "%_{$underscored}"); + }) + ->allowedIncludes($this->allowedIncludes(includeRelatedItems: true)) + ->with(['entityTags', 'item', 'gameVersion', 'baseVariant']) + ->first(); + } + + if ($itemData === null) { + throw new ModelNotFoundException; + } + + $item = $itemData->item; + $item->setRelation('data', collect([$itemData])); + } catch (ModelNotFoundException) { + throw new NotFoundHttpException('No Item with specified UUID or Name found.'); + } + + if ($item->data->first()?->type === 'NOITEM_Vehicle') { + return redirect(sprintf('/api/vehicles/%s', $item->uuid)); + } + + return new ItemResource($item); + } + + #[OA\Post( + path: '/api/items/search', + description: 'Deprecated. Use GET /api/items?filter[name]={value} for name search. Note: OR search across name/uuid/type is no longer supported. This endpoint will be removed in a future version.', + summary: 'In-Game Item Search (Deprecated)', + requestBody: new OA\RequestBody( + description: 'Item Name or (sub)type', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema(type: 'object'), + example: '{"query": "Arrow"}', + ), + ] + ), + tags: ['In-Game', 'Items', 'Search'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + new OA\Parameter(ref: '#/components/parameters/locale'), + new OA\Parameter(ref: '#/components/parameters/include'), + new OA\Parameter(ref: '#/components/parameters/sort'), + new OA\Parameter(name: 'filter[variants]', in: 'query', schema: new OA\Schema(type: 'boolean')), + new OA\Parameter(name: 'filter[category]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[type]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[sub_type]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[manufacturer]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[manufacturer.name]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[class_name]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[name]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[classification]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[size]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[grade]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[class]', in: 'query', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A List of matching Items', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/game_item') + ) + ), + ], + deprecated: true + )] + public function search(SearchRequest $request): AnonymousResourceCollection|\Illuminate\Http\JsonResponse + { + $versionCode = $this->gameVersionCode(); + $toSearch = $request->validated('query'); + + $query = $this->buildBaseQuery($request) + ->where(function (Builder $query) use ($toSearch) { + $query->where('name', 'like', "%{$toSearch}%") + ->orWhereHas('item', fn (Builder $q) => $q->where('uuid', $toSearch)) + ->orWhere('type', $toSearch) + ->orWhere('sub_type', $toSearch); + }); + + $items = $query->jsonPaginate(); + + return ItemResource::collection( + $this->transformToItems($items, $versionCode) + )->additional([ + 'meta' => ['deprecated' => true], + ])->response()->header('Deprecated', 'true'); + } + + #[OA\Get( + path: '/api/items/filters', + description: 'Return all available filter values for in-game items, grouped by field.', + summary: 'In-Game Item Filters', + tags: ['In-Game', 'Items'], + parameters: [ + new OA\Parameter(name: 'version', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[category]', in: 'query', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Available filters for in-game items.', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'filters', + properties: [ + new OA\Property(property: 'type', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'sub_type', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'classification', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'size', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'grade', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'class', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'manufacturer', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + ], + type: 'object' + ), + ], + type: 'object' + ) + ), + ] + )] + public function filters(Request $request): JsonResponse + { + $versionCode = $this->gameVersionCode(); + $category = $request->input('filter.category'); + + if (! is_string($category) || $category === '') { + $category = $request->route()->defaults['category'] ?? 'items'; + } else { + $category = trim($category); + } + + $filtersHash = $this->filtersCacheHash($request); + + $filters = FilterCache::rememberForever( + FilterCache::NAMESPACE_ITEMS, + FilterCache::itemsFiltersKey($versionCode, $category, $filtersHash), + function () use ($request, $versionCode, $category): array { + $baseQuery = QueryBuilder::for(ItemData::class, $request) + ->forRequestedOrDefaultVersion($versionCode) + ->forCategory($category) + ->allowedFilters($this->allowedFilters()); + + $facets = [ + 'type' => [ + 'expr' => 'game_item_data.type', + 'cast' => null, + ], + 'sub_type' => [ + 'expr' => 'game_item_data.sub_type', + 'cast' => null, + ], + 'classification' => [ + 'expr' => 'game_item_data.classification', + 'cast' => null, + ], + 'size' => [ + 'expr' => 'game_item_data.size', + 'cast' => static fn ($value) => $value === null ? null : (int) $value, + ], + 'grade' => [ + 'expr' => 'game_item_data.grade', + 'cast' => static fn ($value) => $value === null ? null : (int) $value, + 'labelResolver' => static fn ($value, $Lbl) => match ($value) { + 1 => 'A', + 2 => 'B', + 3 => 'C', + 4 => 'D', + 5 => 'E', + 6 => 'F', + 7 => 'G', + default => null, + }, + ], + 'class' => [ + 'expr' => 'game_item_data.class', + 'cast' => null, + ], + 'manufacturer' => [ + 'expr' => 'game_manufacturers.name', + 'join' => static fn ($q) => $q->leftJoin('game_manufacturers', 'game_item_data.manufacturer_id', '=', 'game_manufacturers.id'), + 'cast' => null, + ], + ]; + + $out = []; + + foreach ($facets as $key => $facet) { + $expr = $facet['expr']; + + $q = clone $baseQuery; + + if (isset($facet['join'])) { + ($facet['join'])($q); + } + + $rows = $q + ->select([ + DB::raw("{$expr} as value"), + DB::raw('count(*) as count'), + ]) + ->groupByRaw($expr) + ->orderByRaw("{$expr} IS NULL, {$expr}") + ->get(); + + $out[$key] = FilterValues::fromRows($rows, $facet['cast'] ?? null, $facet['labelResolver'] ?? null); + } + + return $out; + } + ); + + return response()->json([ + 'filters' => $filters, + ]); + } + + private function filtersCacheHash(Request $request): string + { + $filters = $this->normalizeFilterParams($request->input('filter', [])); + unset($filters['category']); + + if ($filters === []) { + return 'all'; + } + + return hash('sha256', json_encode($filters) ?: ''); + } + + /** + * @return array + */ + private function normalizeFilterParams(mixed $filters): array + { + if (! is_array($filters) || $filters === []) { + return []; + } + + $normalized = []; + + foreach ($filters as $field => $value) { + if (! is_string($field) || $field === '') { + continue; + } + + $normalizedValue = $this->normalizeFilterValue($value); + + if ($normalizedValue === null) { + continue; + } + + $normalized[$field] = $normalizedValue; + } + + ksort($normalized); + + return $normalized; + } + + private function normalizeFilterValue(mixed $value): ?string + { + if (is_array($value)) { + $values = array_map(static fn (mixed $entry): string => trim((string) $entry), $value); + $values = array_values(array_filter($values, static fn (string $entry): bool => $entry !== '')); + + if ($values === []) { + return null; + } + + sort($values); + + return implode(',', $values); + } + + if ($value === null) { + return null; + } + + $normalized = trim((string) $value); + + if ($normalized === '') { + return null; + } + + $parts = array_map('trim', explode(',', $normalized)); + $parts = array_values(array_filter($parts, static fn (string $entry): bool => $entry !== '')); + + if ($parts === []) { + return null; + } + + sort($parts); + + return implode(',', $parts); + } + + /** + * Transform ItemData collection to Items for resources. + * + * ItemResource expects Item models with loaded data relationship. + * This method transforms the ItemData query results back to Item models. + */ + private function transformToItems($itemDataCollection, ?string $versionCode): mixed + { + if ($itemDataCollection instanceof LengthAwarePaginator) { + $items = $itemDataCollection->getCollection()->map(function (ItemData $itemData) { + $item = $itemData->item; + $item->setRelation('data', collect([$itemData])); + + return $item; + }); + + return $itemDataCollection->setCollection($items); + } + + return $itemDataCollection->map(function (ItemData $itemData) { + $item = $itemData->item; + $item->setRelation('data', collect([$itemData])); + + return $item; + }); + } +} diff --git a/app/Http/Controllers/Api/Game/ManufacturerController.php b/app/Http/Controllers/Api/Game/ManufacturerController.php new file mode 100644 index 000000000..e6b075ba2 --- /dev/null +++ b/app/Http/Controllers/Api/Game/ManufacturerController.php @@ -0,0 +1,177 @@ +select(['name']) + ->selectRaw("MIN(NULLIF(code, '')) AS code") + ->selectRaw("MIN(NULLIF(uuid, '')) AS uuid") + ->where('name', '<>', ''); + + if ($request->has('filter.name')) { + $query->where('name', 'like', '%'.$request->input('filter.name').'%'); + } + + return $query->groupBy('name')->orderBy('name'); + } + + #[OA\Get( + path: '/api/manufacturers', + description: 'Returns paginated manufacturers grouped by name with optional pagination.', + summary: 'In-Game Manufacturers Overview', + tags: ['In-Game', 'Manufacturers'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + new OA\Parameter(name: 'filter[name]', description: 'Partial match on manufacturer name', in: 'query', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of Manufacturers', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items( + ref: '#/components/schemas/manufacturer_link' + ) + ) + ), + ] + )] + public function index(Request $request): AnonymousResourceCollection + { + $query = $this->buildBaseQuery($request) + ->jsonPaginate() + ->appends($request->query()); + + return ManufacturerLinkResource::collection($query); + } + + #[OA\Get( + path: '/api/manufacturers/{manufacturer}', + description: 'Retrieve a manufacturer by name, UUID, or code together with its products.', + summary: 'In-Game Manufacturer Detail', + tags: ['In-Game', 'Manufacturers'], + parameters: [ + new OA\Parameter( + name: 'manufacturer', + in: 'path', + required: true, + schema: new OA\Schema( + description: 'Manufacturer name, uuid, or code', + type: 'string', + ), + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A Manufacturer and its products', + content: new OA\JsonContent(ref: '#/components/schemas/manufacturer') + ), + ] + )] + public function show(Request $request, string $manufacturer): ManufacturerResource + { + $identifier = $this->cleanQueryName($manufacturer); + + $isUuid = Str::isUuid($identifier); + + try { + if ($isUuid) { + $manufacturer = QueryBuilder::for(Manufacturer::class, $request) + ->where('uuid', $identifier) + ->firstOrFail(); + } else { + $manufacturer = QueryBuilder::for(Manufacturer::class, $request) + ->orWhere('name', 'LIKE', sprintf('%%%s%%', $identifier)) + ->orWhere('code', 'LIKE', sprintf('%%%s%%', $identifier)) + ->firstOrFail(); + } + } catch (ModelNotFoundException) { + throw new NotFoundHttpException('No Manufacturer with specified UUID or Name found.'); + } + + return new ManufacturerResource($manufacturer); + } + + #[OA\Post( + path: '/api/manufacturers/search', + description: 'Deprecated. Use GET /api/manufacturers?filter[name]={value} for name search. This endpoint will be removed in a future version.', + summary: 'In-Game Manufacturer Search (Deprecated)', + requestBody: new OA\RequestBody( + description: 'Manufacturer name, uuid, or code', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema(type: 'object'), + example: '{"query": "Anvil"}', + ), + ] + ), + tags: ['In-Game', 'Manufacturers', 'Search'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A List of matching Manufacturers', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/manufacturer_link') + ) + ), + ], + deprecated: true + )] + public function search(SearchRequest $request): AnonymousResourceCollection|\Illuminate\Http\JsonResponse + { + $query = $request->validated('query'); + + $manufacturers = QueryBuilder::for(Manufacturer::class) + ->select(['name']) + ->selectRaw("MIN(NULLIF(code, '')) AS code") + ->selectRaw("MIN(NULLIF(uuid, '')) AS uuid") + ->where(function (Builder $q) use ($query) { + $q->where('name', 'like', "%{$query}%") + ->orWhere('uuid', $query) + ->orWhere('code', 'LIKE', "%{$query}%"); + }) + ->groupBy('name') + ->orderBy('name') + ->jsonPaginate() + ->appends($request->query()); + + return ManufacturerLinkResource::collection($manufacturers)->additional([ + 'meta' => ['deprecated' => true], + ])->response()->header('Deprecated', 'true'); + } +} diff --git a/app/Http/Controllers/Api/Game/VehicleController.php b/app/Http/Controllers/Api/Game/VehicleController.php new file mode 100644 index 000000000..0778beb6a --- /dev/null +++ b/app/Http/Controllers/Api/Game/VehicleController.php @@ -0,0 +1,689 @@ +buildBaseQuery($request); + $vehicles = $query->jsonPaginate(); + + return VehicleResource::collection( + $this->transformToVehicles($vehicles) + ); + } + + #[OA\Get( + path: '/api/vehicles/{identifier}', + description: 'Retrieve a vehicle by name, class name, or UUID along with requested includes.', + summary: 'In-Game Vehicle Detail', + tags: ['In-Game', 'Vehicles'], + parameters: [ + new OA\Parameter( + name: 'identifier', + in: 'path', + required: true, + schema: new OA\Schema( + description: 'Vehicle name, class_name, or UUID', + type: 'string', + ), + ), + new OA\Parameter(ref: '#/components/parameters/include'), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A Vehicle', + content: new OA\JsonContent( + oneOf: [ + new OA\Schema(ref: '#/components/schemas/game_vehicle'), + new OA\Schema(ref: '#/components/schemas/ship_matrix_vehicle'), + ] + ) + ), + ] + )] + public function show(Request $request, string $identifier): AbstractBaseResource + { + $original = $identifier; + $versionCode = $this->gameVersionCode(); + $identifier = $this->cleanQueryName($identifier); + $isUuid = Str::isUuid($identifier); + $allowedIncludes = $this->allowedIncludes(); + + $this->normalizeIncludes($request, $allowedIncludes); + + try { + $vehicleData = null; + + if ($isUuid) { + $vehicleData = QueryBuilder::for(VehicleData::class, $request) + ->forRequestedOrDefaultVersion($versionCode) + ->whereHas('vehicle', fn (Builder $q) => $q->where('uuid', $identifier)) + ->allowedIncludes($allowedIncludes) + ->with(['vehicle', 'gameVersion']) + ->first(); + } + + if ($vehicleData === null) { + $underscored = str_replace(' ', '_', $identifier); + $vehicleData = QueryBuilder::for(VehicleData::class, $request) + ->forRequestedOrDefaultVersion($versionCode) + ->where(function (Builder $q) use ($identifier, $underscored, $original) { + $q->where('name', $identifier) + ->orWhereRaw('upper(display_name) = ?', [strtoupper($identifier)]) + ->orWhereRaw('upper(class_name) = ?', [strtoupper($original)]) + ->orWhere('class_name', strtoupper($underscored)) + ->orWhere('class_name', 'LIKE', "%_{$underscored}"); + }) + ->allowedIncludes($allowedIncludes) + ->with(['vehicle', 'gameVersion']) + ->first(); + } + + if ($vehicleData === null) { + $shipMatrixVehicle = ShipMatrixVehicle::query() + ->where('name', $identifier) + ->orWhere('slug', $identifier) + ->with([ + 'foci', + 'manufacturer', + 'productionStatus', + 'productionNote', + 'type', + 'size', + 'loaner', + 'skus', + ]) + ->first(); + + if ($shipMatrixVehicle !== null) { + return new ShipMatrixVehicleResource($shipMatrixVehicle); + } + + throw new ModelNotFoundException('No Vehicle with specified UUID or Name found.'); + } + + $vehicle = $vehicleData->vehicle; + $vehicle->setRelation('data', collect([$vehicleData])); + + $shipMatrixRelations = [ + 'shipMatrixVehicle.foci', + 'shipMatrixVehicle.productionStatus', + 'shipMatrixVehicle.productionNote', + 'shipMatrixVehicle.type', + 'shipMatrixVehicle.size', + 'shipMatrixVehicle.loaner', + 'shipMatrixVehicle.skus', + 'shipMatrixVehicle.manufacturer', + ]; + + if ($this->requestIncludesComponents($request)) { + $shipMatrixRelations[] = 'shipMatrixVehicle.components'; + } + + $vehicleData->load($shipMatrixRelations); + + $vehicle->load(['item' => function ($query) use ($vehicleData) { + $query->with(['data' => function ($q) use ($vehicleData) { + $q->where('game_version_id', $vehicleData->game_version_id) + ->with('descriptionData'); + }]); + }]); + } catch (ModelNotFoundException) { + throw new NotFoundHttpException('No Vehicle with specified UUID or Name found.'); + } + + return new VehicleResource($vehicle); + } + + #[OA\Post( + path: '/api/vehicles/search', + description: 'Deprecated. Use GET /api/vehicles?filter[name]={value} for name search. This endpoint will be removed in a future version.', + summary: 'In-Game Vehicle Search (Deprecated)', + requestBody: new OA\RequestBody( + description: 'Vehicle name, class_name, career, or UUID', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema(type: 'object'), + example: '{"query": "Avenger"}', + ), + ] + ), + tags: ['In-Game', 'Vehicles', 'Search'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + new OA\Parameter(ref: '#/components/parameters/sort'), + new OA\Parameter(name: 'filter[manufacturer]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[manufacturer.name]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[class_name]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[name]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[size]', in: 'query', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'filter[size_class]', in: 'query', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'filter[career]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[role]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[is_vehicle]', in: 'query', schema: new OA\Schema(type: 'boolean')), + new OA\Parameter(name: 'filter[is_gravlev]', in: 'query', schema: new OA\Schema(type: 'boolean')), + new OA\Parameter(name: 'filter[is_spaceship]', in: 'query', schema: new OA\Schema(type: 'boolean')), + new OA\Parameter(name: 'filter[mass_total]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[cargo_capacity]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[vehicle_inventory]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[crew.min]', in: 'query', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'filter[health]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[shield.hp]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[shield.face_type]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[speed.scm]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[speed.max]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[armor.health]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[cross_section.length]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[cross_section.width]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[cross_section.height]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[signature.ir_quantum]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[signature.ir_shields]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[signature.em_quantum]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[signature.em_shields]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'filter[focus]', description: 'Filter by Ship-Matrix focus slug', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[type]', description: 'Filter by Ship-Matrix type slug', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[production_status]', description: 'Filter by Ship-Matrix production status slug', in: 'query', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A List of matching Vehicles', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/game_vehicle') + ) + ), + ], + deprecated: true + )] + public function search(SearchRequest $request): AnonymousResourceCollection|\Illuminate\Http\JsonResponse + { + $toSearch = $request->validated('query'); + $isUuid = Str::isUuid($toSearch); + + $query = $this->buildBaseQuery($request) + ->where(function (Builder $query) use ($toSearch, $isUuid) { + $underscored = str_replace(' ', '_', $toSearch); + $query->where('name', 'like', "%{$toSearch}%") + ->orWhere('class_name', 'LIKE', "%{$underscored}%") + ->orWhere('career', 'LIKE', "%{$toSearch}%"); + + if ($isUuid) { + $query->orWhereHas('vehicle', fn (Builder $q) => $q->where('uuid', $toSearch)); + } + }); + + $vehicles = $query->jsonPaginate(); + + return VehicleResource::collection( + $this->transformToVehicles($vehicles) + )->additional([ + 'meta' => ['deprecated' => true], + ])->response()->header('Deprecated', 'true'); + } + + #[OA\Get( + path: '/api/vehicles/filters', + description: 'Return all available filter values for in-game vehicles.', + summary: 'In-Game Vehicle Filters', + tags: ['In-Game', 'Vehicles'], + parameters: [ + new OA\Parameter(name: 'version', in: 'query', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Available filters for in-game vehicles.', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'filters', + properties: [ + new OA\Property(property: 'manufacturer', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'is_vehicle', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'is_gravlev', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'is_spaceship', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'size', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'role', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'career', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + ], + type: 'object' + ), + ], + type: 'object' + ) + ), + ] + )] + public function filters(Request $request): JsonResponse + { + $versionCode = $this->gameVersionCode(); + $vehicleType = $request->route()->defaults['vehicle_type'] ?? 'vehicles'; + + $filters = FilterCache::rememberForever( + FilterCache::NAMESPACE_VEHICLES, + FilterCache::vehiclesKey($versionCode, $vehicleType), + static function () use ($versionCode, $vehicleType): array { + $baseQuery = VehicleData::query() + ->forRequestedOrDefaultVersion($versionCode) + ->forVehicleType($vehicleType); + + $facets = [ + 'manufacturer' => [ + 'expr' => 'game_manufacturers.name', + 'join' => static fn ($q) => $q->leftJoinRelationship('manufacturer'), + 'cast' => null, + ], + 'is_vehicle' => [ + 'expr' => 'game_vehicle_data.is_vehicle', + 'cast' => static fn ($value) => $value === null ? null : (bool) $value, + ], + 'is_gravlev' => [ + 'expr' => 'game_vehicle_data.is_gravlev', + 'cast' => static fn ($value) => $value === null ? null : (bool) $value, + ], + 'is_spaceship' => [ + 'expr' => 'game_vehicle_data.is_spaceship', + 'cast' => static fn ($value) => $value === null ? null : (bool) $value, + ], + 'size' => [ + 'expr' => 'game_vehicle_data.size', + 'cast' => static fn ($value) => $value === null ? null : (int) $value, + ], + 'role' => [ + 'expr' => 'game_vehicle_data.role', + 'cast' => null, + ], + 'career' => [ + 'expr' => 'game_vehicle_data.career', + 'cast' => null, + ], + 'shield.face_type' => [ + 'expr' => "(game_vehicle_data.data #>> '{ShieldController,FaceType}')", + 'cast' => null, + ], + ]; + + $out = []; + + foreach ($facets as $key => $facet) { + $expr = $facet['expr']; + + $q = clone $baseQuery; + + if (isset($facet['join'])) { + ($facet['join'])($q); + } + + $rows = $q + ->select([ + DB::raw("{$expr} as value"), + DB::raw('count(*) as count'), + ]) + ->groupByRaw($expr) + ->orderByRaw("{$expr} IS NULL, {$expr}") + ->get(); + + $out[$key] = FilterValues::fromRows($rows, $facet['cast'] ?? null); + } + + return $out; + } + ); + + return response()->json([ + 'filters' => $filters, + ]); + } + + /** + * Normalize requested includes: case-insensitive, aliases, and filtering to allowed list. + */ + private function normalizeIncludes(Request $request, array $allowedIncludes): void + { + $includeParam = $request->query('include'); + + if ($includeParam === null || $includeParam === '') { + return; + } + + $allowedLookup = collect($allowedIncludes) + ->mapWithKeys(fn (string $include) => [strtolower($include) => $include]) + ->toArray(); + + $aliases = [ + 'components' => 'shipmatrixvehicle.components', + ]; + + $resolved = collect(explode(',', (string) $includeParam)) + ->map(fn (string $include) => strtolower(trim($include))) + ->filter() + ->map(function (string $include) use ($aliases, $allowedLookup) { + $include = $aliases[$include] ?? $include; + + return $allowedLookup[$include] ?? null; + }) + ->filter() + ->unique() + ->values(); + + if ($resolved->isEmpty()) { + $request->query->remove('include'); + + return; + } + + $request->query->set('include', $resolved->implode(',')); + } + + /** + * Allow components to be requested via include=components or include=shipMatrixVehicle.components. + */ + private function requestIncludesComponents(Request $request): bool + { + $includeParam = $request->query('include', ''); + + if ($includeParam === '') { + return false; + } + + $includes = collect(explode(',', (string) $includeParam)) + ->map(fn (string $include) => strtolower(trim($include))) + ->filter(); + + return $includes->contains('components') || $includes->contains('shipmatrixvehicle.components'); + } + + /** + * Build base query with filters, sorts, and includes for vehicles. + */ + private function buildBaseQuery(Request $request): QueryBuilder + { + $versionCode = $this->gameVersionCode(); + $vehicleType = $request->route()->defaults['vehicle_type'] ?? 'vehicles'; + $allowedIncludes = $this->allowedIncludes(); + + $this->normalizeIncludes($request, $allowedIncludes); + + return QueryBuilder::for(VehicleData::class, $request) + ->forRequestedOrDefaultVersion($versionCode) + ->forVehicleType($vehicleType) + ->allowedFilters($this->allowedFilters()) + ->allowedSorts($this->allowedSorts()) + ->defaultSort('name') + ->allowedIncludes($allowedIncludes) + ->with(['vehicle', 'gameVersion']); + } + + /** + * Query builder includes for vehicles. + */ + private function allowedIncludes(): array + { + return [ + 'manufacturer', + 'shipMatrixVehicle', + 'shipMatrixVehicle.components', + ]; + } + + /** + * Allowed filters for in-game vehicles, including JSON-backed fields. + */ + private function allowedFilters(): array + { + $manufacturerFilter = static function (Builder $query, mixed $value): void { + $values = is_array($value) ? $value : [$value]; + + $query->whereHas('manufacturer', static function ($manufacturerQuery) use ($values): void { + $manufacturerQuery + ->whereIn('name', $values) + ->orWhereIn('code', $values); + }); + }; + + return [ + AllowedFilter::callback('manufacturer', $manufacturerFilter), + AllowedFilter::callback('manufacturer.name', $manufacturerFilter), + AllowedFilter::partial('class_name'), + AllowedFilter::partial('name'), + AllowedFilter::partial('career'), + AllowedFilter::partial('role'), + AllowedFilter::exact('is_vehicle'), + AllowedFilter::exact('is_gravlev'), + AllowedFilter::exact('is_spaceship'), + AllowedFilter::exact('size'), + AllowedFilter::exact('size_class', 'size'), + AllowedFilter::callback('mass_total', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'MassTotal', $value, 'numeric'); + }), + AllowedFilter::callback('cargo_capacity', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'Cargo', $value, 'numeric'); + }), + AllowedFilter::callback('vehicle_inventory', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'Stowage', $value, 'numeric'); + }), + AllowedFilter::callback('crew.min', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'Crew', $value, 'numeric'); + }), + AllowedFilter::callback('health', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'Health', $value, 'numeric'); + }), + AllowedFilter::callback('shield.hp', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'ShieldsTotal.Hp', $value, 'numeric'); + }), + AllowedFilter::callback('shield.face_type', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'ShieldController.FaceType', $value); + }), + AllowedFilter::callback('speed.scm', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'FlightCharacteristics.Speeds.Scm', $value, 'numeric'); + }), + AllowedFilter::callback('speed.max', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'FlightCharacteristics.Speeds.Max', $value, 'numeric'); + }), + AllowedFilter::callback('armor.health', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'Armor.Health', $value, 'numeric'); + }), + AllowedFilter::callback('cross_section.length', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'CrossSection.X', $value, 'numeric'); + }), + AllowedFilter::callback('cross_section.width', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'CrossSection.Y', $value, 'numeric'); + }), + AllowedFilter::callback('cross_section.height', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'CrossSection.Z', $value, 'numeric'); + }), + AllowedFilter::callback('signature.ir_quantum', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'Emission.IrQuantum', $value, 'numeric'); + }), + AllowedFilter::callback('signature.ir_shields', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'Emission.IrShields', $value, 'numeric'); + }), + AllowedFilter::callback('signature.em_quantum', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'Emission.EmQuantum', $value, 'numeric'); + }), + AllowedFilter::callback('signature.em_shields', function (Builder $query, mixed $value): void { + $this->applyJsonFilter($query, 'Emission.EmShields', $value, 'numeric'); + }), + ]; + } + + /** + * Allowed sorts for in-game vehicles, including JSON-backed fields. + */ + private function allowedSorts(): array + { + return array_merge( + [ + 'name', + 'class_name', + 'career', + 'role', + 'is_vehicle', + 'is_gravlev', + 'is_spaceship', + 'size', + AllowedSort::custom('manufacturer', new SortByRelation, 'manufacturer.name'), + AllowedSort::custom('manufacturer.name', new SortByRelation, 'manufacturer.name'), + AllowedSort::custom('msrp', new SortByRelation, 'shipmatrixVehicle.msrp'), + AllowedSort::field('size_class', 'size'), + ], + $this->allowedJsonSorts() + ); + } + + /** + * Get JSON-backed sort fields from configuration. + * + * @return array + */ + private function allowedJsonSorts(): array + { + $sortConfig = config('sorts.vehicles', []); + $allowedSorts = []; + + foreach ($sortConfig as $sortKey => $config) { + $allowedSorts[] = $this->jsonSort( + $config['path'], + $config['path'], + $config['cast'] ?? 'numeric' + ); + } + + return $allowedSorts; + } + + /** + * Transform VehicleData collection to Vehicles for resources. + * + * VehicleLinkResource expects Vehicle models with loaded data relationship. + * This method transforms the VehicleData query results back to Vehicle models. + */ + private function transformToVehicles($vehicleDataCollection): mixed + { + if ($vehicleDataCollection instanceof LengthAwarePaginator) { + $vehicles = $vehicleDataCollection->getCollection()->map(function (VehicleData $vehicleData) { + $vehicle = $vehicleData->vehicle; + $vehicle->setRelation('data', collect([$vehicleData])); + + return $vehicle; + }); + + return $vehicleDataCollection->setCollection($vehicles); + } + + return $vehicleDataCollection->map(function (VehicleData $vehicleData) { + $vehicle = $vehicleData->vehicle; + $vehicle->setRelation('data', collect([$vehicleData])); + + return $vehicle; + }); + } +} diff --git a/app/Http/Controllers/Api/Rsi/CommLink/CommLinkController.php b/app/Http/Controllers/Api/Rsi/CommLink/CommLinkController.php new file mode 100644 index 000000000..76365c653 --- /dev/null +++ b/app/Http/Controllers/Api/Rsi/CommLink/CommLinkController.php @@ -0,0 +1,257 @@ +allowedIncludes(CommLinkResource::validIncludes()) + ->allowedFilters([ + AllowedFilter::exact('id', 'cig_id'), + AllowedFilter::partial('title'), + AllowedFilter::exact('channel', 'channel.name'), + AllowedFilter::exact('category', 'category.name'), + AllowedFilter::exact('series', 'series.name'), + AllowedFilter::custom('created_at', new DateFilter('created_at')), + ]) + ->allowedSorts([ + AllowedSort::field('id', 'cig_id'), + 'title', + 'images_count', + 'links_count', + AllowedSort::custom('channel', new SortByRelation, 'channel.name'), + AllowedSort::custom('category', new SortByRelation, 'category.name'), + AllowedSort::custom('series', new SortByRelation, 'series.name'), + 'created_at', + ]) + ->orderByDesc('cig_id') + ->jsonPaginate() + ->appends(request()->query()); + + return CommLinkResource::collection($query); + } + + #[OA\Get( + path: '/api/comm-links/filters', + description: 'Return all available filter values for Comm-Links.', + summary: 'Comm-Link Filters', + tags: ['Comm-Links', 'RSI-Website'], + responses: [ + new OA\Response( + response: 200, + description: 'Available filters for Comm-Links.', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'filters', + properties: [ + new OA\Property(property: 'category', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'channel', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'series', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + ], + type: 'object' + ), + ], + type: 'object' + ) + ), + ] + )] + public function filters(Request $request): JsonResponse + { + $isAuthenticated = $request->user() !== null; + + $filters = FilterCache::rememberForever( + FilterCache::NAMESPACE_COMM_LINKS, + FilterCache::commLinksKey($isAuthenticated), + static function (): array { + $baseQuery = (new CommLink)->newQueryWithoutRelationships()->toBase(); + + $facets = [ + 'category' => [ + 'expr' => 'comm_link_categories.name', + 'join' => static fn ($q) => $q->leftJoin('comm_link_categories', 'comm_links.category_id', '=', 'comm_link_categories.id'), + 'cast' => null, + ], + 'channel' => [ + 'expr' => 'comm_link_channels.name', + 'join' => static fn ($q) => $q->leftJoin('comm_link_channels', 'comm_links.channel_id', '=', 'comm_link_channels.id'), + 'cast' => null, + ], + 'series' => [ + 'expr' => 'comm_link_series.name', + 'join' => static fn ($q) => $q->leftJoin('comm_link_series', 'comm_links.series_id', '=', 'comm_link_series.id'), + 'cast' => null, + ], + ]; + + $out = []; + + foreach ($facets as $key => $facet) { + $expr = $facet['expr']; + + $q = clone $baseQuery; + + if (isset($facet['join'])) { + ($facet['join'])($q); + } + + $rows = $q + ->select([ + DB::raw("{$expr} as value"), + DB::raw('count(*) as count'), + ]) + ->groupByRaw($expr) + ->orderByRaw("{$expr} IS NULL, {$expr}") + ->get(); + + $out[$key] = FilterValues::fromRows($rows, $facet['cast'] ?? null); + } + + return $out; + } + ); + + return response()->json([ + 'filters' => $filters, + ]); + } + + #[OA\Get( + path: '/api/comm-links/{id}', + description: 'Retrieve a single comm-link by ID with the requested related resources.', + summary: 'Comm-Link Detail', + tags: ['Comm-Links', 'RSI-Website'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/comm_link_includes'), + new OA\Parameter( + name: 'id', + in: 'path', + required: true, + schema: new OA\Schema( + description: 'Comm-Link ID, starting from 12663', + type: 'integer', + format: 'int64', + minimum: 12663 + ), + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A singular Comm-Link', + content: new OA\JsonContent(ref: '#/components/schemas/comm_link') + ), + new OA\Response( + response: 404, + description: 'No Comm-Link with specified ID found.', + ), + ] + )] + public function show(Request $request): AbstractBaseResource + { + ['comm_link' => $commLink] = Validator::validate( + [ + 'comm_link' => $request->id, + ], + [ + 'comm_link' => 'required|int|min:12663', + ] + ); + + try { + $commLink = QueryBuilder::for(CommLink::class) + ->where('cig_id', $commLink) + ->allowedIncludes(CommLinkResource::validIncludes()) + ->firstOrFail(); + $commLink->append(['prev', 'next']); + } catch (ModelNotFoundException $e) { + throw new NotFoundHttpException('No Comm-Link with specified ID found.'); + } + + $resource = new CommLinkResource($commLink); + $resource->addMetadata([ + 'prev_id' => optional($commLink->prev)->cig_id ?? -1, + 'next_id' => optional($commLink->next)->cig_id ?? -1, + ]); + + return $resource; + } +} diff --git a/app/Http/Controllers/Api/Rsi/CommLink/CommLinkSearchController.php b/app/Http/Controllers/Api/Rsi/CommLink/CommLinkSearchController.php new file mode 100644 index 000000000..bb75b0f37 --- /dev/null +++ b/app/Http/Controllers/Api/Rsi/CommLink/CommLinkSearchController.php @@ -0,0 +1,295 @@ +validate((new CommLinkSearchRequest)->rules()); + + $query = $request->get('keyword') ?? $request->get('query'); + + $commLinks = QueryBuilder::for(CommLink::class) + ->where('title', 'ilike', "%{$query}%") + ->orWhere('cig_id', $query) + ->allowedIncludes(CommLinkResource::validIncludes()) + ->allowedFilters([ + AllowedFilter::exact('category', 'category.name'), + AllowedFilter::exact('series', 'series.name'), + AllowedFilter::exact('channel', 'channel.name'), + ]) + ->jsonPaginate() + ->appends(request()->query()); + + return CommLinkResource::collection($commLinks)->additional([ + 'meta' => ['deprecated' => true], + ])->response()->header('Deprecated', 'true'); + } + + #[OA\Post( + path: '/api/comm-links/reverse-image-link-search', + description: 'Return comm-links that reference the same RSI-hosted image URL.', + summary: 'Comm-Link Reverse Image Link Search', + requestBody: new OA\RequestBody( + description: 'Url to an image hosted on (media.)robertsspaceindustries.com', + required: true, + content: [ + 'url' => new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + ), + example: '{"url": "https://robertsspaceindustries.com/i/cc75a45005a236c6e015dfc2782a2f55ed1e84a2/ADdPNihJzmPbNuTnFsH1DqUeqBRpXdSXVVtgJTyDDgscGKrzJuoFjResiiucPBBDeyrBscqRyZz4qxNsSbWvqUwdG/alien-week-2022-front.webp"}', + ), + ] + ), + tags: ['Comm-Links', 'RSI-Website', 'Search'], + responses: [ + new OA\Response( + response: 200, + description: 'List of Comm-Links that use that image', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/comm_link_link') + ) + ), + new OA\Response( + response: 404, + description: 'No Comm-Link found.', + ), + ], + )] + public function reverseImageLinkSearch(ReverseImageLinkSearchRequest $request): AnonymousResourceCollection + { + $image = Image::query(); + + $dir = $this->getDirHashFromImageUrl($request->get('url', '')); + if ($dir === 'i') { + $path = parse_url( + ImageParser::cleanImgSource($request->get('url')), + PHP_URL_PATH + ); + $parts = explode('/', $path); + array_pop($parts); + $path = implode('/', $parts); + + $image->where('src', 'LIKE', $path.'%'); + } else { + $image->where('dir', $dir); + } + + /** @var Image $image */ + $image = $image->firstOr( + ['*'], + function () { + return []; + } + ); + + return CommLinkResource::collection(optional($image)->commLinks); + } + + #[OA\Post( + path: '/api/v2/comm-links/reverse-image-search', + description: 'Search comm-links by uploading an image and specifying a similarity threshold.', + summary: 'Comm-Link Reverse Image Search', + requestBody: new OA\RequestBody( + required: true, + content: [ + 'multipart/form-data' => new OA\MediaType( + mediaType: 'multipart/form-data', + schema: new OA\Schema( + required: ['image'], + properties: [ + new OA\Property( + property: 'image', + description: 'The image to reverse-search', + type: 'string', + format: 'binary', + ), + ], + type: 'object', + ), + ), + ] + ), + tags: ['Comm-Links', 'RSI-Website', 'Search'], + parameters: [ + new OA\Parameter( + name: 'similarity', + in: 'query', + required: false, + schema: new OA\Schema( + type: 'integer', + maximum: 100, + minimum: 1, + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of Comm-Links that use that image', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/comm_link_link') + ) + ), + new OA\Response( + response: 404, + description: 'No Comm-Link found.', + ), + ], + )] + public function reverseImageSearch(ReverseImageSearchRequest $request, PdqHasher $hasher): AnonymousResourceCollection + { + $this->checkExtensionsLoaded(); + + try { + $hashResult = $hasher->hashContents($request->imageContents()); + } catch (RuntimeException $exception) { + throw new HttpException(422, $exception->getMessage(), $exception); + } + + $data = ImageHashModel::similarImagesForHash( + $hashResult->toBitString(), + $request->similarity() + ); + + return ImageHashResource::collection($data); + } + + #[OA\Get( + path: '/api/comm-link-images/{image}/similar', + description: 'Find Comm-Link images similar to an existing RSI-hosted image.', + summary: 'Comm-Link Reverse Image Similar Search', + tags: ['Comm-Links', 'RSI-Website', 'Search'], + parameters: [ + new OA\Parameter( + name: 'image', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer'), + ), + new OA\Parameter( + name: 'similarity', + description: 'Threshold similarity percentage (defaults to 50)', + in: 'query', + required: false, + schema: new OA\Schema( + type: 'integer', + maximum: 100, + minimum: 1, + ), + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of Comm-Link images that match the requested similarity', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/comm_link_link') + ) + ), + new OA\Response( + response: 404, + description: 'Comm-Link image not found.', + ), + ], + )] + public function similarSearch(SimilarSearchRequest $request): AnonymousResourceCollection + { + $data = $request->validated(); + + /** @var Image $image */ + $image = Image::query()->findOrFail($data['image']); + + $similarity = $data['similarity'] ?? 50; + + return ImageHashResource::collection($image->similarImages($similarity, 50)); + } + + /** + * Returns the RSI directory hash of an image url + * + * @param string $url The RSI Media URl + * @return string The directory hash of the image + */ + private function getDirHashFromImageUrl(string $url): string + { + return ImageParser::getDirHash( + parse_url( + ImageParser::cleanImgSource($url), + PHP_URL_PATH + ) + ); + } + + /** + * Checks if either GD or Imagick is loaded + * + * @throws HttpException + */ + private function checkExtensionsLoaded(): void + { + if (! extension_loaded('gd')) { + app('Log')::error('Required extension "GD" not available.'); + + throw new HttpException(501, 'Required extension "GD" not available.'); + } + } +} diff --git a/app/Http/Controllers/Api/Rsi/CommLink/ImageController.php b/app/Http/Controllers/Api/Rsi/CommLink/ImageController.php new file mode 100644 index 000000000..ec7c480bb --- /dev/null +++ b/app/Http/Controllers/Api/Rsi/CommLink/ImageController.php @@ -0,0 +1,161 @@ +with(['commLinks']) + ->orderByDesc('created_at') + ->whereNull('base_image_id') + ->jsonPaginate() + ->appends(request()->query()); + + return ImageResource::collection($query); + } + + #[OA\Get( + path: '/api/comm-link-images/{image}', + description: 'Retrieve a single comm-link image with related metadata.', + summary: 'Comm-Link Image Detail', + tags: ['Comm-Links', 'RSI-Website', 'Images'], + parameters: [ + new OA\Parameter( + name: 'image', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer'), + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A singular Comm-Link Image', + content: new OA\JsonContent(ref: '#/components/schemas/comm_link_image') + ), + new OA\Response( + response: 404, + description: 'Comm-Link image not found.', + ), + ] + )] + public function show(int $image): ImageResource + { + $model = Image::query() + ->with(['commLinks', 'duplicates', 'baseImage']) + ->find($image); + + if ($model === null) { + throw new NotFoundHttpException('Comm-Link image not found.'); + } + + return new ImageResource($model); + } + + #[OA\Get( + path: '/api/comm-link-images/random', + description: 'Retrieve random comm-link images, optionally filtered by tag.', + summary: 'Comm-Link Images Random', + tags: ['Comm-Links', 'RSI-Website', 'Images'], + parameters: [ + new OA\Parameter(name: 'limit', in: 'query', schema: new OA\Schema(type: 'integer', maximum: 100)), + new OA\Parameter(name: 'filter[tags]', in: 'query', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Retrieve a random Comm-Link Image. Limit parameter sets the number of random images', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/comm_link_image') + ) + ), + ] + )] + public function random(Request $request): AnonymousResourceCollection + { + $query = QueryBuilder::for(Image::class, $request) + ->allowedFilters([ + AllowedFilter::partial('tags', 'tags.name'), + ]) + ->whereRelation('metadata', 'size', '>=', 250 * 1024) + ->inRandomOrder() + ->limit($request->has('limit') ? min($request->get('limit'), 100) : 1) + ->get(); + + return ImageResource::collection($query); + } + + #[OA\Post( + path: '/api/comm-link-images/search', + description: 'Search comm-link images by filename with optional tag filtering.', + summary: 'Comm-Link Image Search by filename', + tags: ['Comm-Links', 'RSI-Website', 'Images', 'Search'], + parameters: [ + new OA\Parameter(name: 'filter[tags]', in: 'query', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Search for a Comm-Link Image by its filename.', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/comm_link_image') + ) + ), + ] + )] + public function search(SearchRequest $request): AnonymousResourceCollection + { + $query = QueryBuilder::for(Image::class, $request) + ->allowedFilters([ + AllowedFilter::partial('tags', 'tags.name'), + ]) + ->whereNull('base_image_id') + ->whereRaw('LOWER(src) LIKE ?', [sprintf('%%%s%%', strtolower($request->validated('query')))]) + ->whereRelation('metadata', 'size', '>', 0) + ->limit(100) + ->orderByDesc('created_at') + ->get(); + + return ImageResource::collection($query); + } +} diff --git a/app/Http/Controllers/Api/StarCitizen/GalactapediaController.php b/app/Http/Controllers/Api/StarCitizen/GalactapediaController.php new file mode 100644 index 000000000..811ab9e03 --- /dev/null +++ b/app/Http/Controllers/Api/StarCitizen/GalactapediaController.php @@ -0,0 +1,305 @@ +allowedFilters([ + AllowedFilter::scope('category'), + AllowedFilter::scope('tag'), + AllowedFilter::scope('template'), + AllowedFilter::partial('title'), + AllowedFilter::custom('created_at', new DateFilter('created_at')), + ]) + ->allowedSorts([ + 'title', + 'categories_count', + 'tags_count', + 'related_articles_count', + ]) + ->defaultSort('-id') + ->with(['categories', 'tags', 'templates']) + ->withCount([ + 'categories', + 'tags', + 'templates', + 'related as related_articles_count', + ]); + } + + #[OA\Get( + path: '/api/galactapedia', + description: 'Return paginated Galactapedia articles with category, tag, and template filters.', + summary: 'Galactapedia Overview', + tags: ['Galactapedia', 'RSI-Website'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + new OA\Parameter(name: 'filter[category]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[tag]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[template]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[title]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[created_at]', in: 'query', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of Galactapedia Articles', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/galactapedia_article') + ) + ), + ] + )] + public function index(Request $request): AnonymousResourceCollection + { + $query = $this->buildBaseQuery($request) + ->jsonPaginate() + ->appends(request()->query()); + + return ArticleResource::collection($query); + } + + #[OA\Get( + path: '/api/galactapedia/filters', + description: 'Return all available filter values for Galactapedia articles.', + summary: 'Galactapedia Filters', + tags: ['Galactapedia', 'RSI-Website'], + responses: [ + new OA\Response( + response: 200, + description: 'Available filters for Galactapedia.', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'filters', + properties: [ + new OA\Property(property: 'category', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'tag', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'template', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + ], + type: 'object' + ), + ], + type: 'object' + ) + ), + ] + )] + public function filters(): JsonResponse + { + $filters = FilterCache::rememberForever( + FilterCache::NAMESPACE_GALACTAPEDIA, + FilterCache::galactapediaKey(), + static function (): array { + $baseQuery = (new Article)->newQueryWithoutRelationships()->toBase(); + + $facets = [ + 'category' => [ + 'expr' => 'galactapedia_categories.name', + 'join' => static fn ($q) => $q + ->leftJoin('galactapedia_article_categories', 'galactapedia_articles.id', '=', 'galactapedia_article_categories.article_id') + ->leftJoin('galactapedia_categories', 'galactapedia_article_categories.category_id', '=', 'galactapedia_categories.id'), + 'cast' => null, + ], + 'tag' => [ + 'expr' => 'galactapedia_tags.name', + 'join' => static fn ($q) => $q + ->leftJoin('galactapedia_article_tags', 'galactapedia_articles.id', '=', 'galactapedia_article_tags.article_id') + ->leftJoin('galactapedia_tags', 'galactapedia_article_tags.tag_id', '=', 'galactapedia_tags.id'), + 'cast' => null, + ], + 'template' => [ + 'expr' => 'galactapedia_templates.template', + 'join' => static fn ($q) => $q + ->leftJoin('galactapedia_article_templates', 'galactapedia_articles.id', '=', 'galactapedia_article_templates.article_id') + ->leftJoin('galactapedia_templates', 'galactapedia_article_templates.template_id', '=', 'galactapedia_templates.id'), + 'cast' => null, + ], + ]; + + $out = []; + + foreach ($facets as $key => $facet) { + $expr = $facet['expr']; + + $q = clone $baseQuery; + + if (isset($facet['join'])) { + ($facet['join'])($q); + } + + $rows = $q + ->select([ + DB::raw("{$expr} as value"), + DB::raw('count(*) as count'), + ]) + ->groupByRaw($expr) + ->orderByRaw("{$expr} IS NULL, {$expr}") + ->get(); + + $out[$key] = FilterValues::fromRows($rows, $facet['cast'] ?? null); + } + + return $out; + } + ); + + return response()->json([ + 'filters' => $filters, + ]); + } + + #[OA\Get( + path: '/api/galactapedia/{id}', + description: 'Retrieve a Galactapedia article by ID with available includes and translations.', + summary: 'Galactapedia Article', + tags: ['Galactapedia', 'RSI-Website'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/locale'), + new OA\Parameter( + name: 'id', + in: 'path', + required: true, + schema: new OA\Schema( + description: 'Galactapedia Article ID', + type: 'string', + ), + ), + new OA\Parameter( + name: 'include', + in: 'query', + schema: new OA\Schema( + description: 'Available Galactapedia includes', + type: 'array', + items: new OA\Items( + type: 'string', + enum: [ + 'translations', + 'tags', + 'categories', + 'related_articles', + 'properties', + ] + ), + ), + explode: false, + allowReserved: true + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A singular Article', + content: new OA\JsonContent(ref: '#/components/schemas/galactapedia_article') + ), + new OA\Response( + response: 404, + description: 'No Article with specified ID found.', + ), + ] + )] + public function show(Request $request): AbstractBaseResource + { + ['article' => $identifier] = Validator::validate( + [ + 'article' => $request->article, + ], + [ + 'article' => 'required|string|min:10|max:12', + ] + ); + + $identifier = $this->cleanQueryName($identifier); + + try { + $model = QueryBuilder::for(Article::class, $request) + ->where('cig_id', $identifier) + ->with(ArticleResource::validIncludes()) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + throw new NotFoundHttpException('No Article with specified ID found.'); + } + + return new ArticleResource($model); + } + + #[OA\Post( + path: '/api/galactapedia/search', + description: 'Deprecated. Use GET /api/galactapedia?filter[title]={value} for title search. This endpoint will be removed in a future version.', + summary: 'Galactapedia Article Search (Deprecated)', + requestBody: new OA\RequestBody( + description: 'Article (partial) title, template or slug', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + ), + example: '{"query": "Banu"}', + ), + ] + ), + tags: ['Galactapedia', 'RSI-Website', 'Search'], + responses: [ + new OA\Response( + response: 200, + description: 'List of articles matching the query', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/galactapedia_article') + ) + ), + ], + deprecated: true, + )] + public function search(SearchRequest $request): AnonymousResourceCollection|\Illuminate\Http\JsonResponse + { + $query = $request->validated('query'); + + $queryBuilder = QueryBuilder::for(Article::class, $request) + ->where('title', 'ilike', "%{$query}%") + ->orWhere('slug', 'like', "%{$query}%") + ->orWhere('cig_id', $query) + ->orWhereHas('templates', function (Builder $builder) use ($query) { + return $builder->where('template', 'like', "%{$query}%"); + }) + ->jsonPaginate() + ->appends(request()->query()); + + return ArticleResource::collection($queryBuilder)->additional([ + 'meta' => ['deprecated' => true], + ])->response()->header('Deprecated', 'true'); + } +} diff --git a/app/Http/Controllers/Api/StarCitizen/Starmap/CelestialObjectController.php b/app/Http/Controllers/Api/StarCitizen/Starmap/CelestialObjectController.php new file mode 100644 index 000000000..d963872c2 --- /dev/null +++ b/app/Http/Controllers/Api/StarCitizen/Starmap/CelestialObjectController.php @@ -0,0 +1,218 @@ +allowedIncludes(CelestialObjectResource::validIncludes()) + ->allowedFilters([ + AllowedFilter::exact('starsystem', 'starsystem.name'), + AllowedFilter::partial('name'), + AllowedFilter::exact('designation'), + AllowedFilter::exact('type'), + ]) + ->with(['starsystem']) + ->allowedSorts([ + AllowedSort::field('id', 'cig_id'), + AllowedSort::custom('starsystem', new SortByRelation, 'starsystem.name'), + 'name', + 'designation', + 'type', + 'fairchanceact', + 'habitable', + 'latitude', + 'longitude', + 'sensor_population', + 'sensor_economy', + 'sensor_danger', + ]); + } + + #[OA\Get( + path: '/api/celestial-objects', + description: 'Returns paginated celestial objects with optional relationships.', + summary: 'Starmap Celestial Objects Overview', + tags: ['Starmap', 'RSI-Website'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + new OA\Parameter(name: 'filter[starsystem]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[name]', description: 'Partial match on celestial object name', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[designation]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[type]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'sort', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter( + name: 'include', + description: 'Include additional relationships (affiliation, starsystem).', + in: 'query', + schema: new OA\Schema(type: 'string'), + explode: false, + allowReserved: true, + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of Celestial Objects', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/celestial_object') + ) + ), + ] + )] + public function index(Request $request): AnonymousResourceCollection + { + $query = $this->buildBaseQuery($request) + ->jsonPaginate() + ->appends(request()->query()); + + return CelestialObjectResource::collection($query); + } + + #[OA\Get( + path: '/api/celestial-objects/{code}', + description: 'Retrieve a celestial object by code or identifier, optionally including relations.', + summary: 'Celestial Object Detail', + tags: ['Starmap', 'RSI-Website'], + parameters: [ + new OA\Parameter( + name: 'code', + in: 'path', + required: true, + schema: new OA\Schema( + description: 'Celestial Object code or identifier', + type: 'string', + ), + ), + new OA\Parameter( + name: 'include', + description: 'Include additional relationships (affiliation, starsystem).', + in: 'query', + schema: new OA\Schema(type: 'string'), + explode: false, + allowReserved: true, + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A Celestial Object', + content: new OA\JsonContent(ref: '#/components/schemas/celestial_object') + ), + new OA\Response( + response: 404, + description: 'No Celestial Object with specified code or name found.' + ), + ] + )] + public function show(Request $request): AbstractBaseResource + { + ['code' => $code] = Validator::validate( + [ + 'code' => $request->code, + ], + [ + 'code' => 'required|string|min:1|max:255', + ] + ); + + $code = mb_strtoupper(urldecode($code)); + + try { + /** @var CelestialObject $starsystem */ + $starsystem = QueryBuilder::for(CelestialObject::class, $request) + ->where('code', $code) + ->orWhere('cig_id', $code) + ->orWhere('name', 'LIKE', "%$code%") + ->allowedIncludes(CelestialObjectResource::validIncludes()) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + throw new NotFoundHttpException('No Celestial Object with specified Code or Name found.'); + } + + return new CelestialObjectResource($starsystem); + } + + #[OA\Post( + path: '/api/celestial-objects/search', + description: 'Deprecated. Use GET /api/celestial-objects?filter[name]={value} for name search. This endpoint will be removed in a future version.', + summary: 'Celestial Object Search (Deprecated)', + requestBody: new OA\RequestBody( + description: 'Partial celestial object code or name to search for', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + properties: [ + new OA\Property(property: 'query', type: 'string'), + ], + type: 'object', + ), + example: '{"query": "Pleiades"}', + ), + ], + ), + tags: ['Starmap', 'RSI-Website', 'Search'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of matching Celestial Objects', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/celestial_object') + ) + ), + ], + deprecated: true + )] + public function search(SearchRequest $request): AnonymousResourceCollection|\Illuminate\Http\JsonResponse + { + $query = mb_strtoupper($request->validated('query')); + + $objects = $this->buildBaseQuery($request) + ->where(function (Builder $builder) use ($query) { + $builder->where('code', $query) + ->orWhere('cig_id', $query) + ->orWhere('name', 'LIKE', "%$query%"); + }) + ->jsonPaginate() + ->appends(request()->query()); + + return CelestialObjectResource::collection($objects)->additional([ + 'meta' => ['deprecated' => true], + ])->response()->header('Deprecated', 'true'); + } +} diff --git a/app/Http/Controllers/Api/StarCitizen/Starmap/StarsystemController.php b/app/Http/Controllers/Api/StarCitizen/Starmap/StarsystemController.php new file mode 100644 index 000000000..56bb3bc9c --- /dev/null +++ b/app/Http/Controllers/Api/StarCitizen/Starmap/StarsystemController.php @@ -0,0 +1,301 @@ +allowedIncludes([]) + ->allowedFilters([ + AllowedFilter::exact('affiliation', 'affiliation.name'), + AllowedFilter::exact('code'), + AllowedFilter::partial('name'), + AllowedFilter::exact('status'), + AllowedFilter::exact('type'), + AllowedFilter::exact('size', 'aggregated_size'), + ]) + ->allowedSorts([ + 'name', + 'code', + 'status', + 'type', + 'aggregated_size', + 'aggregated_population', + 'aggregated_economy', + 'aggregated_danger', + ]); + } + + #[OA\Get( + path: '/api/starsystems', + description: 'Returns paginated starsystems, optionally including related resources.', + summary: 'Starmap Starsystems Overview', + tags: ['Starmap', 'RSI-Website'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + new OA\Parameter(name: 'filter[affiliation]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[code]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[name]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[status]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[type]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[size]', in: 'query', schema: new OA\Schema(type: 'number')), + new OA\Parameter(name: 'sort', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter( + name: 'include', + description: 'Include additional relationships (affiliation, celestialObjects).', + in: 'query', + schema: new OA\Schema(type: 'string'), + explode: false, + allowReserved: true, + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of Starsystems', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/starsystem') + ) + ), + ] + )] + public function index(Request $request): AnonymousResourceCollection + { + $query = $this->buildBaseQuery($request) + ->jsonPaginate() + ->appends(request()->query()); + + return StarsystemResource::collection($query); + } + + #[OA\Get( + path: '/api/starsystems/{code}', + description: 'Retrieve a starsystem by code or identifier, with optional includes.', + summary: 'Starsystem Detail', + tags: ['Starmap', 'RSI-Website'], + parameters: [ + new OA\Parameter( + name: 'code', + in: 'path', + required: true, + schema: new OA\Schema( + description: 'Starsystem code or identifier (e.g. SOL)', + type: 'string', + ), + ), + new OA\Parameter( + name: 'include', + description: 'Include additional relationships (affiliation, celestialObjects).', + in: 'query', + schema: new OA\Schema(type: 'string'), + explode: false, + allowReserved: true, + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A Starsystem', + content: new OA\JsonContent(ref: '#/components/schemas/starsystem') + ), + new OA\Response( + response: 404, + description: 'No Starsystem with specified code found.' + ), + ] + )] + public function show(Request $request): StarsystemResource + { + ['code' => $code] = Validator::validate( + [ + 'code' => $request->code, + ], + [ + 'code' => 'required|string|min:1|max:255', + ] + ); + + $code = mb_strtoupper(urldecode($code)); + + /** @var Starsystem $starsystem */ + $starsystem = QueryBuilder::for(Starsystem::class, $request) + ->where('code', $code) + ->orWhere('cig_id', $code) + ->orWhere('name', 'LIKE', "%$code%") + ->allowedIncludes(StarsystemResource::validIncludes()) + ->firstOrFail(); + + return new StarsystemResource($starsystem); + } + + #[OA\Post( + path: '/api/starsystems/search', + description: 'Deprecated. Use GET /api/starsystems?filter[name]={value} for name search. This endpoint will be removed in a future version.', + summary: 'Starsystem Search (Deprecated)', + requestBody: new OA\RequestBody( + description: 'Partial starsystem code or name to search for', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + properties: [ + new OA\Property(property: 'query', type: 'string'), + ], + type: 'object', + ), + example: '{"query": "Sol"}', + ), + ], + ), + tags: ['Starmap', 'RSI-Website', 'Search'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of matching Starsystems', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/starsystem') + ) + ), + ], + deprecated: true + )] + public function search(SearchRequest $request): AnonymousResourceCollection|\Illuminate\Http\JsonResponse + { + $query = mb_strtoupper($request->validated('query')); + + $starsystems = $this->buildBaseQuery($request) + ->where(function (Builder $builder) use ($query) { + $builder->where('code', $query) + ->orWhere('cig_id', $query) + ->orWhere('name', 'LIKE', "%$query%"); + }) + ->jsonPaginate() + ->appends(request()->query()); + + return StarsystemResource::collection($starsystems)->additional([ + 'meta' => ['deprecated' => true], + ])->response()->header('Deprecated', 'true'); + } + + #[OA\Get( + path: '/api/starsystems/filters', + description: 'Return all available filter values for starsystems.', + summary: 'Starsystem Filters', + tags: ['Starmap', 'RSI-Website'], + responses: [ + new OA\Response( + response: 200, + description: 'Available filters for starsystems.', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'filters', + properties: [ + new OA\Property(property: 'affiliation', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'status', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'type', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'size', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + ], + type: 'object' + ), + ], + type: 'object' + ) + ), + ] + )] + public function filters(): JsonResponse + { + $filters = FilterCache::rememberForever( + FilterCache::NAMESPACE_STARSYSTEMS, + FilterCache::starsystemsKey(), + static function (): array { + $baseQuery = (new Starsystem)->newQueryWithoutRelationships()->toBase(); + + $facets = [ + 'affiliation' => [ + 'expr' => 'starmap_affiliations.name', + 'join' => static fn ($q) => $q + ->leftJoin('starmap_starsystem_affiliation', 'starmap_starsystems.id', '=', 'starmap_starsystem_affiliation.starsystem_id') + ->leftJoin('starmap_affiliations', 'starmap_starsystem_affiliation.affiliation_id', '=', 'starmap_affiliations.id'), + 'cast' => null, + ], + 'status' => [ + 'expr' => 'starmap_starsystems.status', + 'cast' => null, + ], + 'type' => [ + 'expr' => 'starmap_starsystems.type', + 'cast' => null, + ], + 'size' => [ + 'expr' => 'starmap_starsystems.aggregated_size', + 'cast' => static fn ($value) => $value === null ? null : (float) $value, + ], + ]; + + $out = []; + + foreach ($facets as $key => $facet) { + $expr = $facet['expr']; + + $q = clone $baseQuery; + + if (isset($facet['join'])) { + ($facet['join'])($q); + } + + $rows = $q + ->select([ + DB::raw("{$expr} as value"), + DB::raw('count(*) as count'), + ]) + ->groupByRaw($expr) + ->orderByRaw("{$expr} IS NULL, {$expr}") + ->get(); + + $out[$key] = FilterValues::fromRows($rows, $facet['cast'] ?? null); + } + + return $out; + } + ); + + return response()->json([ + 'filters' => $filters, + ]); + } +} diff --git a/app/Http/Controllers/Api/StarCitizen/StatController.php b/app/Http/Controllers/Api/StarCitizen/StatController.php new file mode 100644 index 000000000..06a601a10 --- /dev/null +++ b/app/Http/Controllers/Api/StarCitizen/StatController.php @@ -0,0 +1,67 @@ +orderByDesc('created_at')->first(); + + return new StatResource($stat); + } + + #[OA\Get( + path: '/api/stats', + description: 'Return paginated historical fund and fleet statistics.', + summary: 'Fund / Fleet Stats', + tags: ['Stats', 'RSI-Website'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of stats', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/stat') + ) + ), + ] + )] + public function index(Request $request): AnonymousResourceCollection + { + $query = QueryBuilder::for(Stat::class, $request) + ->orderByDesc('created_at') + ->jsonPaginate() + ->appends(request()->query()); + + return StatResource::collection($query); + } +} diff --git a/app/Http/Controllers/Api/StarCitizen/VehicleController.php b/app/Http/Controllers/Api/StarCitizen/VehicleController.php new file mode 100644 index 000000000..f5873a84a --- /dev/null +++ b/app/Http/Controllers/Api/StarCitizen/VehicleController.php @@ -0,0 +1,312 @@ +allowedFilters([ + AllowedFilter::exact('manufacturer', 'manufacturer.name'), + AllowedFilter::exact('size', 'size.slug'), + AllowedFilter::scope('type'), + AllowedFilter::scope('focus'), + AllowedFilter::scope('production_status'), + AllowedFilter::partial('name'), + ]) + ->allowedSorts([ + AllowedSort::field('id', 'cig_id'), + 'chassis_id', + 'name', + 'msrp', + 'updated_at', + 'length', + AllowedSort::field('width', 'beam'), + 'height', + 'cargo_capacity', + AllowedSort::field('min_crew'), + AllowedSort::field('max_crew'), + AllowedSort::field('msrp'), + AllowedSort::custom('manufacturer', new SortByRelation, 'manufacturer.name'), + AllowedSort::custom('focus', new SortByRelation, 'focus.slug'), + AllowedSort::custom('type', new SortByRelation, 'type.slug'), + AllowedSort::custom('size', new SortByRelation, 'size.slug'), + ]); + } + + #[OA\Get( + path: '/api/shipmatrix/vehicles', + description: 'Returns paginated Ship Matrix vehicles with optional filters for manufacturer, size, and status.', + summary: 'Ship Matrix Vehicles Overview', + tags: ['Ship-Matrix', 'Vehicles'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + new OA\Parameter(name: 'filter[manufacturer]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[size]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[type]', description: 'Filter by vehicle type slug', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[focus]', description: 'Filter by vehicle focus slug (comma-separated for multiple)', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[production_status]', description: 'Filter by production status slug', in: 'query', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of Ship-Matrix Vehicles', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/ship_matrix_vehicle') + ) + ), + ] + )] + public function index(Request $request): AnonymousResourceCollection + { + $query = $this->buildBaseQuery($request); + $vehicles = $query->jsonPaginate(); + + return VehicleResource::collection($vehicles); + } + + #[OA\Get( + path: '/api/shipmatrix/vehicles/filters', + description: 'Return all available filter values for Ship Matrix vehicles.', + summary: 'Ship Matrix Vehicle Filters', + tags: ['Ship-Matrix', 'Vehicles'], + responses: [ + new OA\Response( + response: 200, + description: 'Available filters for Ship Matrix vehicles.', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'filters', + properties: [ + new OA\Property(property: 'manufacturer', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'size', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'type', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'focus', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + new OA\Property(property: 'production_status', type: 'array', items: new OA\Items(ref: '#/components/schemas/filter_value')), + ], + type: 'object' + ), + ], + type: 'object' + ) + ), + ] + )] + public function filters(): JsonResponse + { + $filters = FilterCache::rememberForever( + FilterCache::NAMESPACE_SHIPMATRIX, + FilterCache::shipMatrixKey(), + static function (): array { + $baseQuery = (new Vehicle)->newQueryWithoutRelationships()->toBase(); + + $facets = [ + 'manufacturer' => [ + 'expr' => 'shipmatrix_manufacturers.name', + 'join' => static fn ($q) => $q->leftJoin('shipmatrix_manufacturers', 'shipmatrix_vehicles.manufacturer_id', '=', 'shipmatrix_manufacturers.id'), + 'cast' => null, + ], + 'size' => [ + 'expr' => 'shipmatrix_vehicle_sizes.slug', + 'join' => static fn ($q) => $q->leftJoin('shipmatrix_vehicle_sizes', 'shipmatrix_vehicles.size_id', '=', 'shipmatrix_vehicle_sizes.id'), + 'cast' => null, + ], + 'type' => [ + 'expr' => 'shipmatrix_vehicle_types.slug', + 'join' => static fn ($q) => $q->leftJoin('shipmatrix_vehicle_types', 'shipmatrix_vehicles.type_id', '=', 'shipmatrix_vehicle_types.id'), + 'cast' => null, + ], + 'focus' => [ + 'expr' => 'shipmatrix_vehicle_foci.slug', + 'join' => static fn ($q) => $q + ->leftJoin('shipmatrix_vehicle_vehicle_focus', 'shipmatrix_vehicles.id', '=', 'shipmatrix_vehicle_vehicle_focus.vehicle_id') + ->leftJoin('shipmatrix_vehicle_foci', 'shipmatrix_vehicle_vehicle_focus.focus_id', '=', 'shipmatrix_vehicle_foci.id'), + 'cast' => null, + ], + 'production_status' => [ + 'expr' => 'shipmatrix_production_statuses.slug', + 'join' => static fn ($q) => $q->leftJoin('shipmatrix_production_statuses', 'shipmatrix_vehicles.production_status_id', '=', 'shipmatrix_production_statuses.id'), + 'cast' => null, + ], + ]; + + $out = []; + + foreach ($facets as $key => $facet) { + $expr = $facet['expr']; + + $q = clone $baseQuery; + + if (isset($facet['join'])) { + ($facet['join'])($q); + } + + $rows = $q + ->select([ + DB::raw("{$expr} as value"), + DB::raw('count(*) as count'), + ]) + ->groupByRaw($expr) + ->orderByRaw("{$expr} IS NULL, {$expr}") + ->get(); + + $out[$key] = FilterValues::fromRows($rows, $facet['cast'] ?? null); + } + + return $out; + } + ); + + return response()->json([ + 'filters' => $filters, + ]); + } + + #[OA\Get( + path: '/api/shipmatrix/vehicles/{slug}', + description: 'Retrieve a Ship Matrix vehicle by slug with optional related data.', + summary: 'Ship Matrix Vehicle Detail', + tags: ['Ship-Matrix', 'Vehicles'], + parameters: [ + new OA\Parameter( + name: 'slug', + in: 'path', + required: true, + schema: new OA\Schema( + description: 'Vehicle slug', + type: 'string', + ), + ), + new OA\Parameter( + name: 'include', + description: 'Include additional relationships (components, loaner, skus)', + in: 'query', + schema: new OA\Schema(type: 'string') + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A Ship-Matrix Vehicle', + content: new OA\JsonContent(ref: '#/components/schemas/ship_matrix_vehicle') + ), + new OA\Response( + response: 404, + description: 'Vehicle not found' + ), + ] + )] + public function show(Request $request, string $slug): VehicleResource + { + try { + $vehicle = Vehicle::query() + ->where('slug', urldecode($slug)) + ->firstOrFail(); + + // Handle optional includes + $includes = collect(explode(',', $request->get('include', ''))) + ->map('trim') + ->filter() + ->toArray(); + + if (in_array('components', $includes, true)) { + $vehicle->load('components'); + } + + if (in_array('loaner', $includes, true)) { + $vehicle->load('loaner'); + } + + if (in_array('skus', $includes, true)) { + $vehicle->load('skus'); + } + } catch (ModelNotFoundException) { + throw new NotFoundHttpException('No Vehicle with specified slug found.'); + } + + return new VehicleResource($vehicle); + } + + #[OA\Post( + path: '/api/shipmatrix/vehicles/search', + description: 'Deprecated. Use GET /api/shipmatrix/vehicles?filter[name]={value} for name search. This endpoint will be removed in a future version.', + summary: 'Ship Matrix Vehicle Search (Deprecated)', + requestBody: new OA\RequestBody( + description: 'Vehicle name to search for', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema(type: 'object'), + example: '{"query": "Avenger"}', + ), + ] + ), + tags: ['Ship-Matrix', 'Vehicles', 'Search'], + parameters: [ + new OA\Parameter(ref: '#/components/parameters/page'), + new OA\Parameter(ref: '#/components/parameters/page_number'), + new OA\Parameter(ref: '#/components/parameters/page_size'), + new OA\Parameter(name: 'filter[manufacturer]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[size]', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[type]', description: 'Filter by vehicle type slug', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[focus]', description: 'Filter by vehicle focus slug', in: 'query', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'filter[production_status]', description: 'Filter by production status slug', in: 'query', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A List of matching Ship-Matrix Vehicles', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/ship_matrix_vehicle') + ) + ), + ], + deprecated: true + )] + public function search(SearchRequest $request): AnonymousResourceCollection|\Illuminate\Http\JsonResponse + { + $toSearch = urldecode($request->validated('query')); + + $query = $this->buildBaseQuery($request) + ->where(function (Builder $query) use ($toSearch) { + $query->where('name', 'like', "%{$toSearch}%"); + }); + + $vehicles = $query->jsonPaginate(); + + return VehicleResource::collection($vehicles)->additional([ + 'meta' => ['deprecated' => true], + ])->response()->header('Deprecated', 'true'); + } +} diff --git a/app/Http/Controllers/Api/V1/Rsi/CommLink/Category/CategoryController.php b/app/Http/Controllers/Api/V1/Rsi/CommLink/Category/CategoryController.php deleted file mode 100644 index 3ca2ed538..000000000 --- a/app/Http/Controllers/Api/V1/Rsi/CommLink/Category/CategoryController.php +++ /dev/null @@ -1,95 +0,0 @@ -transformer = $transformer; - parent::__construct($request); - } - - #[OA\Get( - path: '/api/comm-links/categories', - tags: ['Comm-Links', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Categories', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link_category') - ) - ), - ] - )] - public function index(): Response - { - $categories = Category::query()->orderBy('name'); - - return $this->getResponse($categories); - } - - #[OA\Get( - path: '/api/comm-links/categories/{category}', - tags: ['Comm-Links', 'RSI-Website'], - parameters: [ - new OA\Parameter( - name: 'category', - description: 'Name or slug of the category', - in: 'path', - required: true, - ), - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/comm_link_category', - response: 200, - description: 'A singular Comm-Link Category', - ), - new OA\Response( - response: 404, - description: 'No Category with specified name found.', - ), - ] - )] - public function show(string $category): Response - { - try { - $category = Category::query() - ->where('name', $category) - ->orWhere('slug', $category) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $category)], 404); - } - - $this->transformer = new CommLinkTransformer; - - return $this->getResponse($category->commLinks()->orderByDesc('cig_id')); - } -} diff --git a/app/Http/Controllers/Api/V1/Rsi/CommLink/Channel/ChannelController.php b/app/Http/Controllers/Api/V1/Rsi/CommLink/Channel/ChannelController.php deleted file mode 100644 index eeab6986e..000000000 --- a/app/Http/Controllers/Api/V1/Rsi/CommLink/Channel/ChannelController.php +++ /dev/null @@ -1,95 +0,0 @@ -transformer = $transformer; - parent::__construct($request); - } - - #[OA\Get( - path: '/api/comm-links/channels', - tags: ['Comm-Links', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Channels', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link_channel') - ) - ), - ] - )] - public function index(): Response - { - $categories = Channel::query()->orderBy('name'); - - return $this->getResponse($categories); - } - - #[OA\Get( - path: '/api/comm-links/channels/{channel}', - tags: ['Comm-Links', 'RSI-Website'], - parameters: [ - new OA\Parameter( - name: 'category', - description: 'Name or slug of the category', - in: 'path', - required: true, - ), - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/comm_link_channel', - response: 200, - description: 'A singular Comm-Link Channel', - ), - new OA\Response( - response: 404, - description: 'No Channel with specified name found.', - ), - ] - )] - public function show(string $channel): Response - { - try { - $channel = Channel::query() - ->where('name', $channel) - ->orWhere('slug', $channel) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $channel)], 404); - } - - $this->transformer = new CommLinkTransformer; - - return $this->getResponse($channel->commLinks()->orderByDesc('cig_id')); - } -} diff --git a/app/Http/Controllers/Api/V1/Rsi/CommLink/CommLinkController.php b/app/Http/Controllers/Api/V1/Rsi/CommLink/CommLinkController.php deleted file mode 100644 index 6fbe8f89f..000000000 --- a/app/Http/Controllers/Api/V1/Rsi/CommLink/CommLinkController.php +++ /dev/null @@ -1,151 +0,0 @@ -transformer = $transformer; - - // Don't include translation per default - $this->transformer->setDefaultIncludes(array_slice($this->transformer->getAvailableIncludes(), 0, 2)); - - parent::__construct($request); - } - - #[OA\Get( - path: '/api/comm-links', - tags: ['Comm-Links', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'comm_link_includes', - description: 'Available Comm-Link includes', - collectionFormat: 'csv', - enum: [ - 'english', - 'german', - 'images', - 'links', - ] - ), - allowReserved: true - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Comm-Links', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link') - ) - ), - ] - )] - public function index(): Response - { - return $this->getResponse(CommLink::query()->orderByDesc('cig_id')); - } - - #[OA\Get( - path: '/api/comm-links/{id}', - tags: ['Comm-Links', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'comm_link_includes', - description: 'Available Comm-Link includes', - collectionFormat: 'csv', - enum: [ - 'english', - 'german', - 'images', - 'links', - ] - ), - allowReserved: true - ), - new OA\Parameter( - name: 'id', - in: 'path', - required: true, - schema: new OA\Schema( - schema: 'comm_link_id', - description: 'Comm-Link ID, starting from 12663', - type: 'integer', - format: 'int64', - minimum: 12663 - ), - ), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/comm_link', - response: 200, - description: 'A singular Comm-Link', - ), - new OA\Response( - response: 404, - description: 'No Comm-Link with specified ID found.', - ), - ] - )] - public function show(Request $request) - { - try { - ['comm_link' => $commLink] = Validator::validate( - [ - 'comm_link' => $request->comm_link, - ], - [ - 'comm_link' => 'required|int|min:12663', - ] - ); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - try { - $commLink = CommLink::query()->where('cig_id', $commLink)->firstOrFail(); - $commLink->append(['prev', 'next']); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $commLink)], 404); - } - - $this->extraMeta = [ - 'prev_id' => optional($commLink->prev)->cig_id ?? -1, - 'next_id' => optional($commLink->next)->cig_id ?? -1, - ]; - - return $this->getResponse($commLink); - } -} diff --git a/app/Http/Controllers/Api/V1/Rsi/CommLink/CommLinkSearchController.php b/app/Http/Controllers/Api/V1/Rsi/CommLink/CommLinkSearchController.php deleted file mode 100644 index e05b105a6..000000000 --- a/app/Http/Controllers/Api/V1/Rsi/CommLink/CommLinkSearchController.php +++ /dev/null @@ -1,387 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - #[OA\Post( - path: '/api/comm-links/search', - requestBody: new OA\RequestBody( - description: '(Partial) Comm-Link Title or ID', - required: true, - content: [ - new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - schema: 'query', - type: 'json', - ), - example: '{"query": "Banu Merchantman"}', - ), - ] - ), - tags: ['Comm-Links', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/comm_link', - response: 200, - description: 'A singular Comm-Link', - ), - new OA\Response( - response: 404, - description: 'No Comm-Link with found.', - ), - ], - )] - public function searchByTitle(Request $request) - { - try { - $request->validate((new CommLinkSearchRequest)->rules()); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - $query = $request->get('keyword') ?? $request->get('query'); - - return $this->disablePagination() - ->getResponse( - CommLink::query() - ->where('title', 'LIKE', sprintf('%%%s%%', $query)) - ->orWhere('cig_id', 'LIKE', "%{$query}%") - ->limit(100) - ); - } - - #[OA\Post( - path: '/api/comm-links/reverse-image-link-search', - requestBody: new OA\RequestBody( - description: 'Url to an image hosted on (media.)robertsspaceindustries.com', - required: true, - content: [ - 'url' => new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - schema: 'url', - type: 'json', - ), - example: '{"url": "https://robertsspaceindustries.com/i/cc75a45005a236c6e015dfc2782a2f55ed1e84a2/ADdPNihJzmPbNuTnFsH1DqUeqBRpXdSXVVtgJTyDDgscGKrzJuoFjResiiucPBBDeyrBscqRyZz4qxNsSbWvqUwdG/alien-week-2022-front.webp"}', - ), - ] - ), - tags: ['Comm-Links', 'RSI-Website'], - responses: [ - new OA\Response( - response: 200, - description: 'List of Comm-Links that use that image', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link_link') - ) - ), - new OA\Response( - response: 404, - description: 'No Comm-Link found.', - ), - ], - )] - public function reverseImageLinkSearch(Request $request) - { - try { - $request->validate((new ReverseImageLinkSearchRequest)->rules()); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - /** @var Image $image */ - $image = Image::query() - ->where( - 'dir', - $this->getDirHashFromImageUrl($request->get('url', '')) - ) - ->firstOr( - ['*'], - function () { - return []; - } - ); - - return $this->disablePagination() - ->getResponse(optional($image)->commLinks); - } - - #[OA\Post( - path: '/api/comm-links/reverse-image-search', - requestBody: new OA\RequestBody( - required: true, - content: [ - 'image' => new OA\MediaType( - mediaType: 'application/octet-stream', - schema: new OA\Schema( - schema: 'image', - description: 'The image to reverse-search', - type: 'string', - format: 'binary', - ), - ), - ] - ), - tags: ['Comm-Links', 'RSI-Website'], - parameters: [ - new OA\Parameter( - name: 'similarity', - in: 'query', - required: true, - schema: new OA\Schema( - schema: 'image_similarity', - type: 'integer', - maximum: 100, - minimum: 1, - ) - ), - new OA\Parameter( - name: 'method', - in: 'query', - required: true, - schema: new OA\Schema( - schema: 'image_search_method', - collectionFormat: 'csv', - default: 'perceptual', - enum: [ - 'perceptual', - 'difference', - 'average', - ] - ) - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Comm-Links that use that image', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link_link') - ) - ), - new OA\Response( - response: 404, - description: 'No Comm-Link found.', - ), - ], - )] - public function reverseImageSearch(Request $request) - { - $this->checkExtensionsLoaded(); - try { - $request->validate((new ReverseImageSearchRequest)->rules()); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - $this->transformer = new ImageHashTransformer; - $this->transformer->includeAllAvailableIncludes(); - - $hashConfig = $this->getHashConfigForMethod($request->get('method')); - $hashConfig['similarity'] = (int) $request->get('similarity'); - $hashData = $this->hashImage($hashConfig['hasher'], $request->file('image')); - - return $this->disablePagination() - ->getResponse( - $this->getHashesFromDatabase($hashConfig, $hashData) - ->map( - function (object $data) { - $id = $data->comm_link_image_id; - $image = Image::query()->find($id); - $image->similarity = round((1 - $data->distance / 64) * 100); - - return $image; - } - ) - ->sortByDesc('similarity') - ); - } - - /** - * Returns the RSI directory hash of an image url - * - * @param string $url The RSI Media URl - * @return string The directory hash of the image - */ - private function getDirHashFromImageUrl(string $url): string - { - return ImageParser::getDirHash( - parse_url( - ImageParser::cleanImgSource($url), - PHP_URL_PATH - ) - ); - } - - /** - * Checks if either GD or Imagick is loaded - * - * @throws HttpException - */ - private function checkExtensionsLoaded(): void - { - if (! extension_loaded('gd') && ! extension_loaded('imagick')) { - app('Log')::error('Required extension "GD" or "Imagick" not available.'); - - $this->response->error('Required extension "GD" or "Imagick" not available.', 501); - } - } - - /** - * Hash config based on hash method - */ - private function getHashConfigForMethod(string $hashMethod): array - { - switch ($hashMethod) { - case 'average': - return [ - 'hasher' => new ImageHash(new AverageHash), - 'prefix' => 'a', - 'table' => 'average_hash', - ]; - - case 'difference': - return [ - 'hasher' => new ImageHash(new DifferenceHash), - 'prefix' => 'd', - 'table' => 'difference_hash', - ]; - - case 'perceptual': - default: - return [ - 'hasher' => new ImageHash(new PerceptualHash2), - 'prefix' => 'p', - 'table' => 'perceptual_hash', - ]; - } - } - - /** - * Hashes an uploaded image - * - * @param ImageHash $hasher The hasher with set hash method - * @param UploadedFile $file The uploaded file - */ - private function hashImage(ImageHash $hasher, $file): array - { - $hash = $hasher->hash($file)->toHex(); - - return [ - 'hash' => $hash, - 'decoded' => array_map('hexdec', (str_split($hash, strlen($hash) / 2))), - ]; - } - - /** - * Return hashes based on database connection type - * - * - * @return Builder[]|Collection|\Illuminate\Support\Collection - */ - private function getHashesFromDatabase(array $hashConfig, array $hashData) - { - // Since SQLITE does not support the BIT_COUNT operation we only search for exact hash matches - if (config('database.default') === 'sqlite') { - return $this->getHashesFromSQLiteStore($hashConfig['table'], $hashData['hash']); - } - - return $this->getHashesFromSQLStore( - $hashConfig['prefix'], - $hashData['decoded'], - $hashConfig['similarity'] - ); - } - - /** - * Get the image hashes that equal the provided hash - * - * @param string $hashMethod Hash method average, distance, perceptual - * @param string $hash The image hash - * @return Builder[]|Collection - */ - private function getHashesFromSQLiteStore(string $hashMethod, string $hash) - { - return ImageHashModel::query() - ->where($hashMethod, $hash) - ->get('comm_link_image_id'); - } - - /** - * Get the image hashes matching the provided hash method and hamming distance - * - * @param string $prefix Hash Attribute prefix - * @param array $decodedHash Image hash split in the middle and hex decoded - * @param int $distance The maximum hamming distance - */ - private function getHashesFromSQLStore( - string $prefix, - array $decodedHash, - int $distance - ): \Illuminate\Support\Collection { - return DB::table('comm_link_image_hashes') - ->select('comm_link_image_id') - ->selectRaw( - 'BIT_COUNT('.$prefix.'_hash_1 ^ ?) + BIT_COUNT('.$prefix.'_hash_2 ^ ?) AS distance', - [ - $decodedHash[0], - $decodedHash[1], - ] - ) - ->havingRaw('distance <= ?', [$distance]) - ->limit(50) - ->get(); - } -} diff --git a/app/Http/Controllers/Api/V1/Rsi/CommLink/Series/SeriesController.php b/app/Http/Controllers/Api/V1/Rsi/CommLink/Series/SeriesController.php deleted file mode 100644 index c5606dda5..000000000 --- a/app/Http/Controllers/Api/V1/Rsi/CommLink/Series/SeriesController.php +++ /dev/null @@ -1,95 +0,0 @@ -transformer = $transformer; - parent::__construct($request); - } - - #[OA\Get( - path: '/api/comm-links/series', - tags: ['Comm-Links', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Series', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link_series') - ) - ), - ] - )] - public function index(): Response - { - $categories = Series::query()->orderBy('name'); - - return $this->getResponse($categories); - } - - #[OA\Get( - path: '/api/comm-links/series/{series}', - tags: ['Comm-Links', 'RSI-Website'], - parameters: [ - new OA\Parameter( - name: 'category', - description: 'Name or slug of the series', - in: 'path', - required: true, - ), - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/comm_link_series', - response: 200, - description: 'A singular Comm-Link Series', - ), - new OA\Response( - response: 404, - description: 'No Series with specified name found.', - ), - ] - )] - public function show(string $series): Response - { - try { - $series = Series::query() - ->where('name', $series) - ->orWhere('slug', $series) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $series)], 404); - } - - $this->transformer = new CommLinkTransformer; - - return $this->getResponse($series->commLinks()->orderByDesc('cig_id')); - } -} diff --git a/app/Http/Controllers/Api/V1/Rsi/Transcript/TranscriptController.php b/app/Http/Controllers/Api/V1/Rsi/Transcript/TranscriptController.php deleted file mode 100644 index cd311ec74..000000000 --- a/app/Http/Controllers/Api/V1/Rsi/Transcript/TranscriptController.php +++ /dev/null @@ -1,180 +0,0 @@ -transformer = $transformer; - - // Don't include translation per default - $this->transformer->setDefaultIncludes(array_slice($this->transformer->getAvailableIncludes(), 0, 2)); - - parent::__construct($request); - } - - /** - * Returns all Transcripts - * - * @Get("/{?page,limit,include}") - * - * @Versions({"v1"}) - * - * @Parameters({ - * - * @Parameter("page", type="integer", required=false, description="Pagination page", default=1), - * @Parameter( - * "include", - * type="string", - * required=false, - * description="Relations to include. Valid relations are shown in the meta data" - * ), - * @Parameter( - * "limit", - * type="integer", - * required=false, - * description="Items per page, set to 0, to return all items", - * default=10 - * ), - * }) - * - * @Request(headers={"Accept": "application/x.StarCitizenWikiApi.v1+json"}) - * - * @Response(200, body={ - * "data": { - * { - * "title": "Inside Star Citizen: Report Purport | Summer 2021", - * "youtube_id": "9gYBBb_FsCE", - * "youtube_url": "https://www.youtube.com/watch?v=9gYBBb_FsCE", - * "playlist_name": "Inside Star Citizen", - * "upload_date": "2021-09-23", - * "runtime": "1041", - * "runtime_formatted": "00:17:21", - * "thumbnail": "https://i.ytimg.com/vi/9gYBBb_FsCE/maxresdefault.jpg", - * "description": "YouTube Description", - * }, - * { - * "title": "...", - * } - * }, - * "meta": { - * "processed_at": "2020-12-07 14:45:18", - * "valid_relations": { - * "english", - * "german" - * }, - * "pagination": { - * "total": 1550, - * "count": 15, - * "per_page": 15, - * "current_page": 1, - * "total_pages": 104, - * "links": { - * "next": "https:\/\/api.star-citizen.wiki\/api\/transcripts?page=2" - * } - * } - * } - * }) - */ - public function index(): Response - { - return $this->getResponse(Transcript::query()->orderByDesc('upload_date')); - } - - /** - * Returns a singular transcript by its youtube-id - * - * @Get("/{ID}{?include}") - * - * @Versions({"v1"}) - * - * @Parameters({ - * - * @Parameter("ID", type="string", required=true, description="YouTube Video ID"), - * @Parameter( - * "include", - * type="string", - * required=false, - * description="Relations to include. Valid relations are shown in the meta data" - * ), - * }) - * - * @Transaction({ - * - * @Request(headers={"Accept": "application/x.StarCitizenWikiApi.v1+json"}), - * - * @Response(200, body={ - * "data": { - * "title": "Inside Star Citizen: Report Purport | Summer 2021", - * "youtube_id": "9gYBBb_FsCE", - * "youtube_url": "https://www.youtube.com/watch?v=9gYBBb_FsCE", - * "playlist_name": "Inside Star Citizen", - * "upload_date": "2021-09-23", - * "runtime": "1041", - * "runtime_formatted": "00:17:21", - * "thumbnail": "https://i.ytimg.com/vi/9gYBBb_FsCE/maxresdefault.jpg", - * "description": "YouTube Description", - * }, - * "meta": { - * "processed_at": "2020-12-07 14:52:11", - * "valid_relations": { - * "english", - * "german" - * }, - * "prev_id": "zKdN8N44d1g", - * "next_id": null - * } - * }), - * }) - * - * @throws ValidationException - */ - public function show(Request $request): Response - { - ['transcripts' => $transcript] = Validator::validate( - [ - 'transcripts' => $request->transcript, - ], - [ - 'transcripts' => 'required|string|min:11|max:20', - ] - ); - - try { - $transcript = Transcript::query()->where('youtube_id', $transcript)->firstOrFail(); - $transcript->append(['prev', 'next']); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $transcript)], 404); - } - - $this->extraMeta = [ - 'prev_id' => optional($transcript->prev)->youtube_id ?? null, - 'prev_title' => optional($transcript->prev)->title ?? null, - 'next_id' => optional($transcript->next)->youtube_id ?? null, - 'next_title' => optional($transcript->next)->title ?? null, - ]; - - return $this->getResponse($transcript); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizen/Galactapedia/GalactapediaController.php b/app/Http/Controllers/Api/V1/StarCitizen/Galactapedia/GalactapediaController.php deleted file mode 100644 index 21436c518..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizen/Galactapedia/GalactapediaController.php +++ /dev/null @@ -1,206 +0,0 @@ -transformer = $transformer; - parent::__construct($request); - } - - #[OA\Get( - path: '/api/galactapedia', - tags: ['Galactapedia', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'galacapedia_includes', - description: 'Available Galactapedia includes', - collectionFormat: 'csv', - enum: [ - 'english', - 'tags', - 'categories', - 'related_articles', - 'properties', - ] - ), - allowReserved: true - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Galactapedia Articles', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/galactapedia_article') - ) - ), - ] - )] - public function index(): Response - { - return $this->getResponse(Article::query()->orderByDesc('id')); - } - - #[OA\Get( - path: '/api/galactapedia/{id}', - tags: ['Galactapedia', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'galactapedia_include', - in: 'query', - schema: new OA\Schema( - schema: 'galacapedia_includes', - description: 'Available Galactapedia includes', - collectionFormat: 'csv', - enum: [ - 'english', - 'tags', - 'categories', - 'related_articles', - 'properties', - ] - ), - allowReserved: true - ), - new OA\Parameter( - name: 'id', - in: 'path', - required: true, - schema: new OA\Schema( - schema: 'galactapedia_id', - description: 'Galactapedia Article ID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/galactapedia_article', - response: 200, - description: 'A singular Article', - ), - new OA\Response( - response: 404, - description: 'No Article with specified ID found.', - ), - ] - )] - public function show(Request $request) - { - try { - ['article' => $article] = Validator::validate( - [ - 'article' => $request->article, - ], - [ - 'article' => 'required|string|min:10|max:12', - ] - ); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - $article = urldecode($article); - - try { - $model = Article::query() - ->where('cig_id', $article) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $article)], 404); - } - - $this->transformer->includeAllAvailableIncludes(); - - return $this->getResponse($model); - } - - #[OA\Post( - path: '/api/galactapedia/search', - requestBody: new OA\RequestBody( - description: 'Article (partial) title, template or slug', - required: true, - content: [ - new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - schema: 'query', - type: 'json', - ), - example: '{"query": "Banu"}', - ), - ] - ), - tags: ['Galactapedia', 'RSI-Website'], - responses: [ - new OA\Response( - response: 200, - description: 'List of articles matching the query', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/galactapedia_article') - ) - ), - new OA\Response( - response: 404, - description: 'No Article found.', - ), - ], - )] - public function search(Request $request) - { - $rules = (new GalactapediaSearchRequest)->rules(); - try { - $request->validate($rules); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - $query = urldecode($request->get('query')); - $queryBuilder = Article::query() - ->where('title', 'like', "%{$query}%") - ->orWhere('slug', 'like', "%{$query}%") - ->orWhere('cig_id', 'like', "%{$query}%") - ->orWhereHas('templates', function (Builder $builder) use ($query) { - return $builder->where('template', 'like', "%{$query}%"); - }); - - if ($queryBuilder->count() === 0) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $query)], 404); - } - - return $this->getResponse($queryBuilder); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizen/Manufacturer/ManufacturerController.php b/app/Http/Controllers/Api/V1/StarCitizen/Manufacturer/ManufacturerController.php deleted file mode 100644 index bd4f3a080..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizen/Manufacturer/ManufacturerController.php +++ /dev/null @@ -1,212 +0,0 @@ -transformer = $transformer; - parent::__construct($request); - } - - #[OA\Get( - path: '/api/manufacturers', - tags: ['Manufacturers', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'manufacturer_includes', - collectionFormat: 'csv', - enum: [ - 'vehicles', - 'ships', - ] - ), - allowReserved: true - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Manufacturers', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/manufacturer') - ) - ), - ] - )] - public function index(): Response - { - return $this->getResponse(Manufacturer::query()); - } - - #[OA\Get( - path: '/api/manufacturer/{code}', - tags: ['Manufacturers', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'manufacturer_includes', - collectionFormat: 'csv', - enum: [ - 'vehicles', - 'ships', - ] - ), - allowReserved: true - ), - new OA\Parameter( - name: 'code', - in: 'path', - required: true, - schema: new OA\Schema( - schema: 'manufacturer_code', - description: 'Manufacturer Code, e.g. RSI', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/manufacturer', - response: 200, - description: 'A singular manufacturer', - ), - new OA\Response( - response: 404, - description: 'No Manufacturer with specified CODE found.', - ), - ] - )] - public function show(Request $request) - { - try { - ['manufacturer' => $manufacturer] = Validator::validate( - [ - 'manufacturer' => $request->manufacturer, - ], - [ - 'manufacturer' => 'required|string|min:1|max:255', - ] - ); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - $manufacturer = urldecode($manufacturer); - - try { - $model = Manufacturer::query() - ->where('name_short', $manufacturer) - ->orWhere('name', $manufacturer) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $manufacturer)], 404); - } - - return $this->getResponse($model); - } - - #[OA\Post( - path: '/api/manufacturers/search', - requestBody: new OA\RequestBody( - description: 'Name', - required: true, - content: [ - new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - schema: 'query', - type: 'json', - ), - example: '{"query": "RSI"}', - ), - ] - ), - tags: ['Manufacturers', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'manufacturer_includes', - collectionFormat: 'csv', - enum: [ - 'vehicles', - 'ships', - ] - ), - allowReserved: true - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of manufacturers matching the query', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/manufacturer') - ) - ), - new OA\Response( - response: 404, - description: 'No manufacturer found.', - ), - ], - )] - public function search(Request $request) - { - $rules = (new ManufacturerSearchRequest)->rules(); - - try { - $request->validate($rules); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - $query = urldecode($request->get('query')); - $queryBuilder = Manufacturer::query() - ->where('name_short', 'like', "%{$query}%") - ->orWhere('name', 'like', "%{$query}%"); - - if ($queryBuilder->count() === 0) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $query)], 404); - } - - return $this->getResponse($queryBuilder); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizen/Starmap/CelestialObject/CelestialObjectController.php b/app/Http/Controllers/Api/V1/StarCitizen/Starmap/CelestialObject/CelestialObjectController.php deleted file mode 100644 index aad7b812b..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizen/Starmap/CelestialObject/CelestialObjectController.php +++ /dev/null @@ -1,68 +0,0 @@ -transformer = $transformer; - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(CelestialObject::query()); - } - - /** - * @param string|int $code - */ - public function show($code): Response - { - $code = urldecode($code); - - try { - /** @var CelestialObject $celestialObject */ - $celestialObject = CelestialObject::query() - ->where('code', $code) - ->orWhere('cig_id', $code) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $code)], 404); - } - - return $this->getResponse($celestialObject); - } - - /** - * Search Endpoint - */ - public function search(): Response - { - $query = $this->request->get('query', ''); - $query = urldecode($query); - $queryBuilder = CelestialObject::query()->where('name', 'like', "%{$query}%"); - - if ($queryBuilder->count() === 0) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $query)], 404); - } - - return $this->getResponse($queryBuilder); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizen/Starmap/Starsystem/StarsystemController.php b/app/Http/Controllers/Api/V1/StarCitizen/Starmap/Starsystem/StarsystemController.php deleted file mode 100644 index 5fb0d4583..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizen/Starmap/Starsystem/StarsystemController.php +++ /dev/null @@ -1,201 +0,0 @@ -transformer = $transformer; - parent::__construct($request); - } - - #[OA\Get( - path: '/api/starmap/starsystems', - tags: ['Starmap', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'starsystem_includes', - collectionFormat: 'csv', - enum: [ - 'celestial_objects', - 'jumppoints', - ] - ), - allowReserved: true - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Starsystems', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/starsystem') - ) - ), - ] - )] - public function index(Request $request): Response - { - if ($request->has('transformer') && $request->get('transformer', null) === 'link') { - $this->transformer = new StarsystemLinkTransformer; - } - - return $this->getResponse(Starsystem::query()->orderBy('name')); - } - - #[OA\Get( - path: '/api/starmap/starsystem/{code}', - tags: ['Starmap', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'starsystem_includes', - collectionFormat: 'csv', - enum: [ - 'celestial_objects', - 'jumppoints', - ] - ), - allowReserved: true - ), - new OA\Parameter( - name: 'code', - in: 'path', - required: true, - schema: new OA\Schema( - schema: 'starsystem_code', - description: 'Starsystem Code, e.g. SOL', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/starsystem', - response: 200, - description: 'A singular Starsystem', - ), - new OA\Response( - response: 404, - description: 'No System with specified Code found.', - ), - ] - )] - public function show(Request $request) - { - try { - ['code' => $code] = Validator::validate( - [ - 'code' => $request->code, - ], - [ - 'code' => 'required|string|min:1|max:255', - ] - ); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - $code = mb_strtoupper(urldecode($code)); - - try { - /** @var Starsystem $starsystem */ - $starsystem = Starsystem::query() - ->where('code', $code) - ->orWhere('cig_id', $code) - ->orWhere('name', 'LIKE', "%$code%") - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $code)], 404); - } - - return $this->getResponse($starsystem); - } - - // #[OA\Post( - // path: '/api/starmap/starsystems/search', - // requestBody: new OA\RequestBody( - // description: 'Starsystem name', - // required: true, - // content: [ - // new OA\MediaType( - // mediaType: 'application/json', - // schema: new OA\Schema( - // schema: 'query', - // type: 'json', - // ), - // example: '{"query": "SOL"}', - // ) - // ] - // ), - // tags: ['Starmap', 'RSI-Website'], - // responses: [ - // new OA\Response( - // response: 200, - // description: 'List of systems matching the query', - // content: new OA\JsonContent( - // type: 'array', - // items: new OA\Items(ref: '#/components/schemas/starsystem') - // ) - // ), - // new OA\Response( - // response: 404, - // description: 'No System found.', - // ) - // ], - // )] - public function search(Request $request) - { - $rules = (new StarsystemRequest)->rules(); - - try { - $request->validate($rules); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - $query = urldecode($this->request->get('query', '')); - $queryBuilder = Starsystem::query()->where('name', 'like', "%{$query}%"); - - if ($queryBuilder->count() === 0) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $query)], 404); - } - - return $this->getResponse($queryBuilder); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizen/Stat/StatController.php b/app/Http/Controllers/Api/V1/StarCitizen/Stat/StatController.php deleted file mode 100644 index 600624e51..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizen/Stat/StatController.php +++ /dev/null @@ -1,67 +0,0 @@ -transformer = $transformer; - parent::__construct($request); - } - - #[OA\Get( - path: '/api/stats/latest', - tags: ['Stats', 'RSI-Website'], - responses: [ - new OA\Response( - ref: '#/components/schemas/stat', - response: 200, - description: 'List of stats' - ), - ] - )] - public function latest(): Response - { - $stat = Stat::query()->orderByDesc('created_at')->first(); - - return $this->getResponse($stat); - } - - #[OA\Get( - path: '/api/stats', - tags: ['Stats', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of stats', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/stat') - ) - ), - ] - )] - public function index(): Response - { - $stats = Stat::query()->orderByDesc('created_at'); - - return $this->getResponse($stats); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizen/Vehicle/VehicleController.php b/app/Http/Controllers/Api/V1/StarCitizen/Vehicle/VehicleController.php deleted file mode 100644 index 6c9328137..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizen/Vehicle/VehicleController.php +++ /dev/null @@ -1,223 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - #[OA\Get( - path: '/api/vehicles', - tags: ['Vehicles', 'RSI-Website', 'In-Game'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'vehicle_includes', - description: 'Available Vehicle includes', - collectionFormat: 'csv', - enum: [ - 'components', - 'hardpoints', - 'shops', - 'shops.items', - ] - ), - allowReserved: true - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Vehicles', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/vehicle') - ) - ), - ] - )] - public function index(Request $request): Response - { - if ($request->has('transformer') && $request->get('transformer') === 'link') { - $this->transformer = new VehicleLinkTransformer; - if (! $request->has('limit')) { - $this->limit = 100; - } - } - - return $this->getResponse(Vehicle::query()->orderBy('name')); - } - - #[OA\Get( - path: '/api/vehicles/{name}', - tags: ['Vehicles', 'RSI-Website', 'In-Game'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'vehicle_includes', - description: 'Available Vehicle includes', - collectionFormat: 'csv', - enum: [ - 'components', - 'hardpoints', - 'shops', - 'shops.items', - ] - ), - allowReserved: true - ), - new OA\Parameter( - name: 'name', - in: 'path', - required: true, - schema: new OA\Schema( - schema: 'vehicle_name', - description: '(Partial) Vehicle name', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/vehicle', - response: 200, - description: 'A singular vehicle' - ), - ] - )] - public function show(Request $request) - { - try { - ['vehicle' => $vehicle] = Validator::validate( - [ - 'vehicle' => $request->vehicle, - ], - [ - 'vehicle' => 'required|string|min:1|max:255', - ] - ); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - $vehicle = urldecode($vehicle); - - try { - $vehicleModel = Vehicle::query() - ->where('name', 'LIKE', "%{$vehicle}%") - ->orWhere('slug', 'LIKE', "%{$vehicle}%") - ->first(); - - if ($vehicleModel === null) { - $vehicleModel = UnpackedVehicle::query() - ->where('name', 'like', '%'.$vehicle.'%') - ->orWhere('class_name', 'like', '%'.$vehicle.'%') - ->firstOrFail(); - - $locale = $this->transformer->getLocale(); - $this->transformer = new UnpackedVehicleTransformer; - if ($locale !== null) { - $this->transformer->setLocale($locale); - } - } - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $vehicle)], 404); - } - - return $this->getResponse($vehicleModel); - } - - #[OA\Post( - path: '/api/vehicles/search', - requestBody: new OA\RequestBody( - description: 'Vehicle (partial) name or slug', - required: true, - content: [ - new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - schema: 'query', - type: 'json', - ), - example: '{"query": "Merchant"}', - ), - ] - ), - tags: ['Vehicles', 'RSI-Website', 'In-Game'], - responses: [ - new OA\Response( - response: 200, - description: 'List of vehicles matching the query', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/vehicle') - ) - ), - new OA\Response( - response: 404, - description: 'No vehicle found.', - ), - ], - )] - public function search(Request $request) - { - $rules = (new VehicleSearchRequest)->rules(); - try { - $request->validate($rules); - } catch (ValidationException $e) { - return new JsonResponse([ - 'code' => $e->status, - 'message' => $e->getMessage(), - ], $e->status); - } - - $query = urldecode($request->get('query')); - $queryBuilder = Vehicle::query() - ->where('name', 'like', "%{$query}%") - ->orWhere('slug', 'like', "%{$query}%"); - - if ($queryBuilder->count() === 0) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $query)], 404); - } - - return $this->getResponse($queryBuilder); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/CharArmor/CharArmorController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/CharArmor/CharArmorController.php deleted file mode 100644 index b0c39f19e..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/CharArmor/CharArmorController.php +++ /dev/null @@ -1,58 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(CharArmor::query()); - } - - public function show(Request $request): Response - { - ['armor' => $armor] = Validator::validate( - [ - 'armor' => $request->armor, - ], - [ - 'armor' => 'required|string|min:1|max:255', - ] - ); - - $armor = $this->cleanQueryName($armor); - - try { - $armor = CharArmor::query() - ->whereHas('item', function (Builder $query) use ($armor) { - return $query->where('name', $armor)->orWhere('uuid', $armor); - }) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $armor)], 404); - } - - return $this->getResponse($armor); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/CharArmorController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/CharArmorController.php deleted file mode 100644 index fb6cef9b6..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/CharArmorController.php +++ /dev/null @@ -1,58 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(Armor::query()); - } - - public function show(Request $request): Response - { - ['armor' => $armor] = Validator::validate( - [ - 'armor' => $request->armor, - ], - [ - 'armor' => 'required|string|min:1|max:255', - ] - ); - - $armor = $this->cleanQueryName($armor); - - try { - $armor = Armor::query() - ->whereHas('item', function (Builder $query) use ($armor) { - return $query->where('name', $armor)->orWhere('uuid', $armor); - }) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $armor)], 404); - } - - return $this->getResponse($armor); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/ClothingController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/ClothingController.php deleted file mode 100644 index 45ac0c969..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/ClothingController.php +++ /dev/null @@ -1,58 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(Clothing::query()); - } - - public function show(Request $request): Response - { - ['clothing' => $clothing] = Validator::validate( - [ - 'clothing' => $request->clothing, - ], - [ - 'clothing' => 'required|string|min:1|max:255', - ] - ); - - $clothing = $this->cleanQueryName($clothing); - - try { - $clothing = Clothing::query() - ->whereHas('item', function (Builder $query) use ($clothing) { - return $query->where('name', $clothing)->orWhere('uuid', $clothing); - }) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $clothing)], 404); - } - - return $this->getResponse($clothing); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/FoodController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/FoodController.php deleted file mode 100644 index bd4bde3d2..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/FoodController.php +++ /dev/null @@ -1,58 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(Food::query()); - } - - public function show(Request $request): Response - { - ['food' => $food] = Validator::validate( - [ - 'food' => $request->food, - ], - [ - 'food' => 'required|string|min:1|max:255', - ] - ); - - $food = $this->cleanQueryName($food); - - try { - $food = Food::query() - ->whereHas('item', function (Builder $query) use ($food) { - return $query->where('name', $food)->orWhere('uuid', $food); - }) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $food)], 404); - } - - return $this->getResponse($food); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Item/ItemController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Item/ItemController.php deleted file mode 100644 index c20f566d4..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Item/ItemController.php +++ /dev/null @@ -1,249 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - #[OA\Get( - path: '/api/items', - tags: ['In-Game', 'Items'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'item_includes', - description: 'Available Item includes', - collectionFormat: 'csv', - enum: [ - 'shops', - ] - ), - allowReserved: true - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of In-game Items', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/item') - ) - ), - ] - )] - public function index(): \Illuminate\Http\Response - { - return $this->getResponse(Item::query()->orderBy('name')); - } - - #[OA\Get( - path: '/api/items/{item}', - tags: ['In-Game', 'Items'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'item_includes', - description: 'Available Item includes', - collectionFormat: 'csv', - enum: [ - 'shops', - ] - ), - allowReserved: true - ), - new OA\Parameter( - name: 'item', - in: 'path', - required: true, - schema: new OA\Schema( - schema: 'item_name_uuid', - description: 'Item name or UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/item', - response: 200, - description: 'A singular Item', - ), - new OA\Response( - response: 404, - description: 'No Item with specified UUID or name found.', - ), - ] - )] - public function show(Request $request): Response - { - ['item' => $item] = Validator::validate( - [ - 'item' => $request->item, - ], - [ - 'item' => 'required|string|min:1|max:255', - ] - ); - - $item = $this->cleanQueryName($item); - - try { - $item = Item::query() - ->where('name', $item) - ->orWhere('uuid', $item) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $item)], 404); - } - - return $this->getResponse($item); - } - - #[OA\Post( - path: '/api/items/search', - requestBody: new OA\RequestBody( - description: 'Article (partial) name, type, sub-type or uuid', - required: true, - content: [ - new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - schema: 'query', - type: 'json', - ), - example: '{"query": "Arrowhead"}', - ), - ] - ), - tags: ['In-Game', 'Items'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'item_includes', - description: 'Available Item includes', - collectionFormat: 'csv', - enum: [ - 'shops', - 'shops.items', - ] - ), - allowReserved: true - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of items matching the query', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/item') - ) - ), - new OA\Response( - response: 404, - description: 'No item found.', - ), - ], - )] - public function search(ItemSearchRequest $request): Response - { - $rules = (new ItemSearchRequest)->rules(); - $request->validate($rules); - - $query = $this->cleanQueryName($request->get('query')); - - try { - $item = Item::query(); - - if ($request->has('shop') && $request->get('shop') !== null) { - $item - ->whereHas('shopsRaw', function ($query) use ($request) { - $query->where('shop_uuid', $request->get('shop')); - }); - } - - $item->where('name', 'like', "%{$query}%") - ->orWhere('uuid', $query) - ->orWhere('type', $query) - ->orWhere('sub_type', $query); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $query)], 404); - } - - return $this->getResponse($item); - } - - #[OA\Get( - path: '/api/items/tradeables', - tags: ['In-Game', 'Items'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'item_includes', - description: 'Available Item includes', - collectionFormat: 'csv', - enum: [ - 'shops', - ] - ), - allowReserved: true - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of tradeable In-game Items', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/item') - ) - ), - ] - )] - public function indexTradeables(): Response - { - return $this->getResponse( - Item::query() - ->whereIn('type', Inventory::EXTRA_TYPES) - ->orderBy('name') - ); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/CoolerController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/CoolerController.php deleted file mode 100644 index fcec32236..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/CoolerController.php +++ /dev/null @@ -1,59 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(Cooler::query()); - } - - public function show(Request $request): Response - { - ['item' => $item] = Validator::validate( - [ - 'item' => $request->item, - ], - [ - 'item' => 'required|string|min:1|max:255', - ] - ); - - $item = $this->cleanQueryName($item); - - try { - $item = Cooler::query() - ->whereHas('shipItem.item', function (Builder $query) use ($item) { - return $query->where('name', $item) - ->orWhere('uuid', $item); - }) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $item)], 404); - } - - return $this->getResponse($item); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/ItemController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/ItemController.php deleted file mode 100644 index 171fa8e3b..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/ItemController.php +++ /dev/null @@ -1,106 +0,0 @@ - '81e1a10a-c7bd-401f-92e1-284115dcd6e1', - '153d53e7-c5e0-445c-82ac-6aae2073b565' => '81e1a10a-c7bd-401f-92e1-284115dcd6e1', - - 'Impact I Mining Laser' => '6429e3d3-c813-4dfc-bc68-c95b54123722', - 'af6cede9-7ae7-47a6-ba91-dac5f020f698' => '6429e3d3-c813-4dfc-bc68-c95b54123722', - - 'Hofstede-S1 Mining Laser' => '077a4a94-6296-4a83-a6c4-f215f7efd1df', - 'a5b839fe-c1cc-4cbd-abb6-c9296ad84d46' => '077a4a94-6296-4a83-a6c4-f215f7efd1df', - - 'Klein-S1 Mining Laser' => 'e6b284b9-456a-4444-b5fc-7c33bf5a6945', - 'd04aed0b-3c4a-4aaf-96c6-1abf8b32c12a' => 'e6b284b9-456a-4444-b5fc-7c33bf5a6945', - ]; - - /** - * ShipController constructor. - */ - public function __construct(ShipItemTransformer $transformer, Request $request) - { - $this->transformer = $transformer; - - parent::__construct($request); - } - - /** - * View all items - */ - public function index(): Response - { - return $this->getResponse(ShipItem::query()->orderBy('name')); - } - - /** - * View a singular item - * - * @throws ValidationException - */ - public function show(Request $request): Response - { - ['item' => $item] = Validator::validate( - [ - 'item' => $request->item, - ], - [ - 'item' => 'required|string|min:1|max:255', - ] - ); - - $item = $this->cleanQueryName($item); - - if (isset($this->uuidFixes[$item])) { - $item = $this->uuidFixes[$item]; - } - - try { - $item = ShipItem::query() - ->whereRelation('item', 'name', $item) - ->orWhere('uuid', $item) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $item)], 404); - } - - return $this->getResponse($item); - } - - /** - * View a singular item - */ - public function search(ItemSearchRequest $request): Response - { - $rules = (new ItemSearchRequest)->rules(); - $request->validate($rules); - - $query = $this->cleanQueryName($request->get('query')); - - try { - $item = ShipItem::query() - ->whereRelation('item', 'name', 'like', "%{$query}%") - ->orWhere('uuid', $query); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $query)], 404); - } - - return $this->getResponse($item); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/PowerPlantController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/PowerPlantController.php deleted file mode 100644 index 87e49a8a0..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/PowerPlantController.php +++ /dev/null @@ -1,59 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(PowerPlant::query()); - } - - public function show(Request $request): Response - { - ['item' => $item] = Validator::validate( - [ - 'item' => $request->item, - ], - [ - 'item' => 'required|string|min:1|max:255', - ] - ); - - $item = $this->cleanQueryName($item); - - try { - $item = PowerPlant::query() - ->whereHas('shipItem.item', function (Builder $query) use ($item) { - return $query->where('name', $item) - ->orWhere('uuid', $item); - }) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $item)], 404); - } - - return $this->getResponse($item); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/QuantumDriveController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/QuantumDriveController.php deleted file mode 100644 index 4b218c1f7..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/QuantumDriveController.php +++ /dev/null @@ -1,59 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(QuantumDrive::query()); - } - - public function show(Request $request): Response - { - ['item' => $item] = Validator::validate( - [ - 'item' => $request->item, - ], - [ - 'item' => 'required|string|min:1|max:255', - ] - ); - - $item = $this->cleanQueryName($item); - - try { - $item = QuantumDrive::query() - ->whereHas('shipItem.item', function (Builder $query) use ($item) { - return $query->where('name', $item) - ->orWhere('uuid', $item); - }) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $item)], 404); - } - - return $this->getResponse($item); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/ShieldController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/ShieldController.php deleted file mode 100644 index 629d6b59b..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/ShieldController.php +++ /dev/null @@ -1,59 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(Shield::query()); - } - - public function show(Request $request): Response - { - ['item' => $item] = Validator::validate( - [ - 'item' => $request->item, - ], - [ - 'item' => 'required|string|min:1|max:255', - ] - ); - - $item = $this->cleanQueryName($item); - - try { - $item = Shield::query() - ->whereHas('shipItem.item', function (Builder $query) use ($item) { - return $query->where('name', $item) - ->orWhere('uuid', $item); - }) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $item)], 404); - } - - return $this->getResponse($item); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/WeaponController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/WeaponController.php deleted file mode 100644 index 9e5cad355..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Ship/WeaponController.php +++ /dev/null @@ -1,59 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(Weapon::query()); - } - - public function show(Request $request): Response - { - ['item' => $item] = Validator::validate( - [ - 'item' => $request->item, - ], - [ - 'item' => 'required|string|min:1|max:255', - ] - ); - - $item = $this->cleanQueryName($item); - - try { - $item = Weapon::query() - ->whereHas('shipItem.item', function (Builder $query) use ($item) { - return $query->where('name', $item) - ->orWhere('uuid', $item); - }) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $item)], 404); - } - - return $this->getResponse($item); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Shop/ShopController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Shop/ShopController.php deleted file mode 100644 index 05936d2ac..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/Shop/ShopController.php +++ /dev/null @@ -1,340 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - #[OA\Get( - path: '/api/shops', - tags: ['In-Game', 'Shops'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'shop_includes', - description: 'Available Shop includes', - collectionFormat: 'csv', - enum: [ - 'items', - ] - ), - allowReserved: true - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Shops', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/shop') - ) - ), - ] - )] - public function index(): Response - { - return $this->getResponse(Shop::query() - ->where('version', config(self::SC_DATA_KEY))); - } - - #[OA\Get( - path: '/api/shops/{shop}', - tags: ['In-Game', 'Shops'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'shop_includes', - description: 'Available Shop includes', - collectionFormat: 'csv', - enum: [ - 'items', - ] - ), - allowReserved: true - ), - new OA\Parameter( - name: 'shop', - in: 'path', - required: true, - schema: new OA\Schema( - schema: 'shop_name', - description: 'Shop name or position', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/shop', - response: 200, - description: 'A singular shop', - ), - new OA\Response( - response: 404, - description: 'No shop with specified name found.', - ), - ] - )] - public function show(Request $request): Response - { - ['shop' => $shop] = Validator::validate( - [ - 'shop' => $request->shop, - ], - [ - 'shop' => 'required|string|min:1|max:255', - ] - ); - - $shop = $this->cleanQueryName($shop); - - try { - $shop = Shop::query() - ->where('name_raw', 'LIKE', sprintf('%%%s%%%%', $shop)) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $shop)], 404); - } - - return $this->getResponse($shop); - } - - #[OA\Get( - path: '/api/shops/position/{position}', - tags: ['In-Game', 'Shops'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'shop_includes', - description: 'Available Shop includes', - collectionFormat: 'csv', - enum: [ - 'items', - ] - ), - allowReserved: true - ), - new OA\Parameter( - name: 'position', - in: 'path', - required: true, - schema: new OA\Schema( - schema: 'shop_position', - description: 'Shop position', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Shops in that position', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/shop') - ) - ), - new OA\Response( - response: 404, - description: 'No shop with specified position found.', - ), - ] - )] - public function showPosition(Request $request): Response - { - ['position' => $position] = Validator::validate( - [ - 'position' => $request->position, - ], - [ - 'position' => 'required|string|min:1|max:255', - ] - ); - - $position = $this->cleanQueryName($position); - $positions = Shop::query() - ->where('position', 'LIKE', sprintf('%%%s%%%%', $position)) - ->get(); - - if ($positions->isEmpty()) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $position)], 404); - } - - return $this->getResponse($positions); - } - - #[OA\Get( - path: '/api/shops/name/{name}', - tags: ['In-Game', 'Shops'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'shop_includes', - description: 'Available Shop includes', - collectionFormat: 'csv', - enum: [ - 'items', - ] - ), - allowReserved: true - ), - new OA\Parameter( - name: 'position', - in: 'path', - required: true, - schema: new OA\Schema( - schema: 'name', - description: 'Shop Name', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/shop', - response: 200, - description: 'Shop matching that name' - ), - new OA\Response( - response: 404, - description: 'No shop with specified name found.', - ), - ] - )] - public function showName(Request $request): Response - { - ['name' => $name] = Validator::validate( - [ - 'name' => $request->name, - ], - [ - 'name' => 'required|string|min:1|max:255', - ] - ); - - $name = $this->cleanQueryName($name); - $positions = Shop::query() - ->where('name', 'LIKE', sprintf('%%%s%%%%', $name)) - ->get(); - - if ($positions->isEmpty()) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $name)], 404); - } - - return $this->getResponse($positions); - } - - #[OA\Get( - path: '/api/shops/{position}/{name}', - tags: ['In-Game', 'Shops'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - schema: 'shop_includes', - description: 'Available Shop includes', - collectionFormat: 'csv', - enum: [ - 'items', - ] - ), - allowReserved: true - ), - new OA\Parameter( - name: 'position', - in: 'path', - required: true, - schema: new OA\Schema( - schema: 'name', - description: 'Shop Name', - type: 'string', - ), - ), - new OA\Parameter( - name: 'name', - in: 'path', - required: true, - schema: new OA\Schema( - schema: 'name', - description: 'Shop Name', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - ref: '#/components/schemas/shop', - response: 200, - description: 'Shop matching that name' - ), - new OA\Response( - response: 404, - description: 'No shop with specified name found.', - ), - ] - )] - public function showShopAtPosition(Request $request): Response - { - ['position' => $position, 'name' => $name] = Validator::validate( - [ - 'position' => $request->position, - 'name' => $request->name, - ], - [ - 'position' => 'required|string|min:1|max:255', - 'name' => 'required|string|min:1|max:255', - ] - ); - - $position = $this->cleanQueryName($position); - $name = $this->cleanQueryName($name); - - try { - $shop = Shop::query() - ->where('position', $position) - ->where('name', $name) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $position)], 404); - } - - return $this->getResponse($shop); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/WeaponPersonal/AttachmentController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/WeaponPersonal/AttachmentController.php deleted file mode 100644 index 61eb5a3e0..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/WeaponPersonal/AttachmentController.php +++ /dev/null @@ -1,57 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(Attachment::query() - ->where('version', config(self::SC_DATA_KEY))); - } - - public function show(Request $request): Response - { - ['attachment' => $attachment] = Validator::validate( - [ - 'attachment' => $request->attachment, - ], - [ - 'attachment' => 'required|string|min:1|max:255', - ] - ); - - $attachment = $this->cleanQueryName($attachment); - - try { - $attachment = Attachment::query() - ->whereHas('item', function (Builder $query) use ($attachment) { - return $query->where('name', 'LIKE', $attachment.'%') - ->orWhere('uuid', $attachment); - }) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $attachment)], 404); - } - - return $this->getResponse($attachment); - } -} diff --git a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/WeaponPersonal/WeaponPersonalController.php b/app/Http/Controllers/Api/V1/StarCitizenUnpacked/WeaponPersonal/WeaponPersonalController.php deleted file mode 100644 index 9d8a7ec60..000000000 --- a/app/Http/Controllers/Api/V1/StarCitizenUnpacked/WeaponPersonal/WeaponPersonalController.php +++ /dev/null @@ -1,60 +0,0 @@ -transformer = $transformer; - - parent::__construct($request); - } - - public function index(): Response - { - return $this->getResponse(WeaponPersonal::query() - ->where('version', config(self::SC_DATA_KEY))); - } - - public function show(Request $request): Response - { - ['weapon' => $weapon] = Validator::validate( - [ - 'weapon' => $request->weapon, - ], - [ - 'weapon' => 'required|string|min:1|max:255', - ] - ); - - $weapon = $this->cleanQueryName($weapon); - - try { - $weapon = WeaponPersonal::query() - ->whereHas('item', function (Builder $query) use ($weapon) { - return $query->where('name', $weapon) - ->orWhere('uuid', $weapon); - }) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - return new Response(['code' => 404, 'message' => sprintf(static::NOT_FOUND_STRING, $weapon)], 404); - } - - return $this->getResponse($weapon); - } -} diff --git a/app/Http/Controllers/Api/V2/AbstractApiV2Controller.php b/app/Http/Controllers/Api/V2/AbstractApiV2Controller.php deleted file mode 100644 index 4b11cdc53..000000000 --- a/app/Http/Controllers/Api/V2/AbstractApiV2Controller.php +++ /dev/null @@ -1,227 +0,0 @@ -request = $request; - - $this->processRequestParams(); - - ApiRouteCalled::dispatch([ - 'url' => $request->fullUrl(), - 'user-agent' => $request->userAgent() ?? 'Star Citizen Wiki API', - 'forwarded-for' => $request->header('X-Forwarded-For', '127.0.0.1'), - ]); - } - - /** - * Processes all possible Request Parameters - */ - protected function processRequestParams(): void - { - $this->processLimit(); - $this->processLocale(); - } - - /** - * Processes the 'limit' Request-Parameter - */ - private function processLimit(): void - { - if ($this->request->input(self::LIMIT) !== null) { - $itemLimit = (int) $this->request->get(self::LIMIT); - - if ($itemLimit > 500) { - $this->limit = 500; - } elseif ($itemLimit >= 0) { - $this->limit = $itemLimit; - } - } - } - - /** - * Processes the 'locale' Request-Parameter - */ - private function processLocale(): void - { - if ($this->request->has(self::LOCALE) && $this->request->get(self::LOCALE, null) !== null) { - $this->setLocale($this->request->get(self::LOCALE)); - } - } - - /** - * Set the Locale - */ - protected function setLocale(string $localeCode): void - { - if (in_array($localeCode, config('language.codes'), true)) { - $this->localeCode = $localeCode; - app()->setLocale(substr($localeCode, 0, 2)); - } - } - - /** - * Cleans the name for query use - */ - protected function cleanQueryName(string $name): string - { - return str_replace('_', ' ', urldecode($name)); - } - - protected function getAllowedIncludes(array $includes) - { - return collect($includes)->map(function ($include) { - if (is_array($include)) { - [$to, $from] = $include; - - return AllowedInclude::relationship($to, $from); - } - - return AllowedInclude::relationship($include); - })->flatten()->toArray(); - } -} diff --git a/app/Http/Controllers/Api/V2/Rsi/CommLink/CommLinkController.php b/app/Http/Controllers/Api/V2/Rsi/CommLink/CommLinkController.php deleted file mode 100644 index 6e16648f8..000000000 --- a/app/Http/Controllers/Api/V2/Rsi/CommLink/CommLinkController.php +++ /dev/null @@ -1,137 +0,0 @@ -allowedIncludes(CommLinkResource::validIncludes()) - ->allowedFilters([ - AllowedFilter::exact('category', 'category.name'), - AllowedFilter::exact('series', 'series.name'), - AllowedFilter::exact('channel', 'channel.name'), - ]) - ->orderByDesc('cig_id') - ->paginate($this->limit) - ->appends(request()->query()); - - return CommLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/comm-links/{id}', - tags: ['Comm-Links', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/comm_link_includes_v2'), - new OA\Parameter( - name: 'id', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Comm-Link ID, starting from 12663', - type: 'integer', - format: 'int64', - minimum: 12663 - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A singular Comm-Link', - content: new OA\JsonContent(ref: '#/components/schemas/comm_link_v2') - ), - new OA\Response( - response: 404, - description: 'No Comm-Link with specified ID found.', - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['comm_link' => $commLink] = Validator::validate( - [ - 'comm_link' => $request->id, - ], - [ - 'comm_link' => 'required|int|min:12663', - ] - ); - - try { - $commLink = QueryBuilder::for(CommLink::class) - ->where('cig_id', $commLink) - ->allowedIncludes(CommLinkResource::validIncludes()) - ->firstOrFail(); - $commLink->append(['prev', 'next']); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Comm-Link with specified ID found.'); - } - - $resource = new CommLinkResource($commLink); - $resource->addMetadata([ - 'prev_id' => optional($commLink->prev)->cig_id ?? -1, - 'next_id' => optional($commLink->next)->cig_id ?? -1, - ]); - - return $resource; - } -} diff --git a/app/Http/Controllers/Api/V2/Rsi/CommLink/CommLinkSearchController.php b/app/Http/Controllers/Api/V2/Rsi/CommLink/CommLinkSearchController.php deleted file mode 100644 index 555fedbcd..000000000 --- a/app/Http/Controllers/Api/V2/Rsi/CommLink/CommLinkSearchController.php +++ /dev/null @@ -1,375 +0,0 @@ -validate((new CommLinkSearchRequest)->rules()); - - $query = $request->get('keyword') ?? $request->get('query'); - - $commLinks = QueryBuilder::for(CommLink::class) - ->where('title', 'LIKE', sprintf('%%%s%%', $query)) - ->orWhere('cig_id', 'LIKE', "%{$query}%") - ->limit(100) - ->allowedIncludes(CommLinkResource::validIncludes()) - ->allowedFilters([ - AllowedFilter::exact('category', 'category.name'), - AllowedFilter::exact('series', 'series.name'), - AllowedFilter::exact('channel', 'channel.name'), - ]) - ->get(); - - return CommLinkResource::collection($commLinks); - } - - #[OA\Post( - path: '/api/v2/comm-links/reverse-image-link-search', - requestBody: new OA\RequestBody( - description: 'Url to an image hosted on (media.)robertsspaceindustries.com', - required: true, - content: [ - 'url' => new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - type: 'object', - ), - example: '{"url": "https://robertsspaceindustries.com/i/cc75a45005a236c6e015dfc2782a2f55ed1e84a2/ADdPNihJzmPbNuTnFsH1DqUeqBRpXdSXVVtgJTyDDgscGKrzJuoFjResiiucPBBDeyrBscqRyZz4qxNsSbWvqUwdG/alien-week-2022-front.webp"}', - ), - ] - ), - tags: ['Comm-Links', 'RSI-Website', 'Search'], - responses: [ - new OA\Response( - response: 200, - description: 'List of Comm-Links that use that image', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link_link_v2') - ) - ), - new OA\Response( - response: 404, - description: 'No Comm-Link found.', - ), - ], - )] - public function reverseImageLinkSearch(Request $request): AnonymousResourceCollection - { - $request->validate((new ReverseImageLinkSearchRequest)->rules()); - - $image = Image::query(); - - $dir = $this->getDirHashFromImageUrl($request->get('url', '')); - if ($dir === 'i') { - $path = parse_url( - ImageParser::cleanImgSource($request->get('url')), - PHP_URL_PATH - ); - $parts = explode('/', $path); - array_pop($parts); - $path = implode('/', $parts); - - $image->where('src', 'LIKE', $path.'%'); - } else { - $image->where('dir', $dir); - } - - /** @var Image $image */ - $image = $image->firstOr( - ['*'], - function () { - return []; - } - ); - - return CommLinkResource::collection(optional($image)->commLinks); - } - - #[OA\Post( - path: '/api/v2/comm-links/reverse-image-search', - requestBody: new OA\RequestBody( - required: true, - content: [ - 'image' => new OA\MediaType( - mediaType: 'application/octet-stream', - schema: new OA\Schema( - description: 'The image to reverse-search', - type: 'string', - format: 'binary', - ), - ), - ] - ), - tags: ['Comm-Links', 'RSI-Website', 'Search'], - parameters: [ - new OA\Parameter( - name: 'similarity', - in: 'query', - required: true, - schema: new OA\Schema( - type: 'integer', - maximum: 100, - minimum: 1, - ) - ), - new OA\Parameter( - name: 'method', - in: 'query', - required: true, - schema: new OA\Schema( - description: 'Available Comm-Link includes', - type: 'array', - items: new OA\Items( - type: 'string', - default: 'perceptual', - enum: [ - 'perceptual', - 'difference', - 'average', - ] - ), - ), - explode: false, - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of Comm-Links that use that image', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link_link_v2') - ) - ), - new OA\Response( - response: 404, - description: 'No Comm-Link found.', - ), - ], - )] - public function reverseImageSearch(Request $request): AnonymousResourceCollection - { - $this->checkExtensionsLoaded(); - - $request->validate((new ReverseImageSearchRequest)->rules()); - - /** @var PDQHash $hash */ - [$hash, $quality] = PDQHasher::computeHashAndQualityFromFilename( - $request->file('image')->get(), - true, - true - ); - - $pdqHash = $hash->to64BitStrings(); - - $hashData = [ - 'perceptual_hash' => (new ImageHash(new PerceptualHash2))->hash($request->file('image'))->toHex(), - 'pdq_hash1' => $pdqHash[0], - 'pdq_hash2' => $pdqHash[1], - 'pdq_hash3' => $pdqHash[2], - 'pdq_hash4' => $pdqHash[3], - ]; - - $data = $this->getResultImages($hashData, (int) $request->get('similarity')); - - return ImageHashResource::collection($data); - } - - public function similarSearch(Request $request) - { - ['image' => $image, 'similarity' => $similarity] = Validator::validate( - [ - 'image' => $request->image, - 'similarity' => $request->similarity, - ], - [ - 'image' => 'required|int|exists:comm_link_images,id', - 'similarity' => 'nullable|int|min:1|max:100', - ] - ); - - /** @var Image $image */ - $image = Image::query()->find($image); - - return ImageHashResource::collection($image->similarImages($similarity ?? 50, 50)); - } - - private function getResultImages(array $hashData, int $similarity = 50) - { - return $this->getHashesFromDatabase($hashData) - ->map( - function (object $data) { - $id = $data->comm_link_image_id; - - $image = Image::query()->find($id); - - if ($data->pdq_distance === null) { - $image->similarity = round((1 - ($data->p_distance / 64)) * 100); - $image->similarity_method = __('Basierend auf Merkmalen des Inhalts'); - } else { - $image->similarity = round((1 - ($data->pdq_distance / 256)) * 100); - $image->similarity_method = ''; //PDQ - } - - $image->pdq_distance = $data->pdq_distance ?? $image->p_distance; - - return $image; - } - ) - ->filter() - ->sortByDesc('similarity') - ->filter(fn (object $image) => $image->similarity >= $similarity); - } - - /** - * Returns the RSI directory hash of an image url - * - * @param string $url The RSI Media URl - * @return string The directory hash of the image - */ - private function getDirHashFromImageUrl(string $url): string - { - return ImageParser::getDirHash( - parse_url( - ImageParser::cleanImgSource($url), - PHP_URL_PATH - ) - ); - } - - /** - * Checks if either GD or Imagick is loaded - * - * @throws HttpException - */ - private function checkExtensionsLoaded(): void - { - if (! extension_loaded('gd') && ! extension_loaded('imagick')) { - app('Log')::error('Required extension "GD" or "Imagick" not available.'); - - throw new HttpException(501, 'Required extension "GD" or "Imagick" not available.'); - } - } - - /** - * Return hashes based on database connection type - * - * - * @return Builder[]|Collection|\Illuminate\Support\Collection - */ - private function getHashesFromDatabase(array $hashData) - { - // Since SQLITE does not support the BIT_COUNT operation we only search for exact hash matches - if (config('database.default') === 'sqlite') { - return $this->getHashesFromSQLiteStore($hashData['perceptual_hash']); - } - - return $this->getHashesFromSQLStore($hashData); - } - - /** - * Get the image hashes that equal the provided hash - * - * @param string $hash The image hash - * @return Builder[]|Collection - */ - private function getHashesFromSQLiteStore(string $hash) - { - return ImageHashModel::query() - ->where('perceptual_hash', $hash) - ->get('comm_link_image_id'); - } - - /** - * Get the image hashes matching the provided hash method and hamming distance - * - * @param array $hashes Image hash split in the middle and hex decoded - */ - private function getHashesFromSQLStore(array $hashes): \Illuminate\Support\Collection - { - return ImageHashModel::query() - ->with('image') - ->select('comm_link_image_hashes.comm_link_image_id') - ->selectRaw( - <<<'SQL' -(BIT_COUNT(CONV(HEX(pdq_hash1), 16, 10) ^ CONV(?, 16, 10)) + -BIT_COUNT(CONV(HEX(pdq_hash2), 16, 10) ^ CONV(?, 16, 10)) + -BIT_COUNT(CONV(HEX(pdq_hash3), 16, 10) ^ CONV(?, 16, 10)) + -BIT_COUNT(CONV(HEX(pdq_hash4), 16, 10) ^ CONV(?, 16, 10))) as pdq_distance, -BIT_COUNT(CONV(HEX(perceptual_hash), 16, 10) ^ CONV(?, 16, 10)) AS p_distance -SQL, - [ - $hashes['pdq_hash1'], - $hashes['pdq_hash2'], - $hashes['pdq_hash3'], - $hashes['pdq_hash4'], - $hashes['perceptual_hash'], - ] - ) - ->join('comm_link_images', 'comm_link_image_hashes.comm_link_image_id', '=', 'comm_link_images.id') - ->join('comm_link_image_metadata', 'comm_link_image_metadata.comm_link_image_id', '=', 'comm_link_images.id') - ->orderBy('pdq_distance') - ->limit(50) - ->get(); - } -} diff --git a/app/Http/Controllers/Api/V2/Rsi/CommLink/ImageController.php b/app/Http/Controllers/Api/V2/Rsi/CommLink/ImageController.php deleted file mode 100644 index e3a699aa4..000000000 --- a/app/Http/Controllers/Api/V2/Rsi/CommLink/ImageController.php +++ /dev/null @@ -1,117 +0,0 @@ -allowedFilters([ - AllowedFilter::custom('tags', new ImageTagFilter), - ]) - ->orderByDesc('id') - ->paginate($this->limit) - ->appends(request()->query()); - - return ImageResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/comm-link-images/random', - tags: ['Comm-Links', 'RSI-Website', 'Images'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter(name: 'filter[tags]', in: 'query', schema: new OA\Schema(type: 'string')), - ], - responses: [ - new OA\Response( - response: 200, - description: 'Retrieve a random Comm-Link Image. Limit parameter sets the number of random images', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link_image_v2') - ) - ), - ] - )] - public function random(Request $request): AnonymousResourceCollection - { - $query = QueryBuilder::for(Image::class, $request) - ->allowedFilters([ - AllowedFilter::partial('tags', 'tags.name'), - ]) - ->whereRelation('metadata', 'size', '>=', 250 * 1024) - ->inRandomOrder() - ->limit($request->has('limit') ? $this->limit : 1) - ->get(); - - return ImageResource::collection($query); - } - - #[OA\Post( - path: '/api/v2/comm-link-images/search', - tags: ['Comm-Links', 'RSI-Website', 'Images', 'Search'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter(name: 'filter[tags]', in: 'query', schema: new OA\Schema(type: 'string')), - ], - responses: [ - new OA\Response( - response: 200, - description: 'Search for a Comm-Link Image by its filename.', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link_image_v2') - ) - ), - ] - )] - public function search(ImageSearchRequest $request): AnonymousResourceCollection - { - $query = QueryBuilder::for(Image::class, $request) - ->allowedFilters([ - AllowedFilter::partial('tags', 'tags.name'), - ]) - ->whereNull('base_image_id') - ->whereRaw('LOWER(src) LIKE ?', [sprintf('%%%s%%', strtolower($request->get('query')))]) - ->whereRelation('metadata', 'size', '>', 0) - ->limit($this->limit) - ->orderByDesc('created_at') - ->get(); - - return ImageResource::collection($query); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/Char/ArmorController.php b/app/Http/Controllers/Api/V2/SC/Char/ArmorController.php deleted file mode 100644 index 88c0c6f4b..000000000 --- a/app/Http/Controllers/Api/V2/SC/Char/ArmorController.php +++ /dev/null @@ -1,137 +0,0 @@ -allowedFilters([ - AllowedFilter::partial('type'), - AllowedFilter::custom('variants', new ItemVariantsFilter), - ]) - ->paginate($this->limit) - ->appends(request()->query()); - - return ItemLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/armor/{armor}', - tags: ['Clothing', 'In-Game', 'Items'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter(ref: '#/components/parameters/clothing_includes_v2'), - new OA\Parameter( - name: 'armor', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Armor name of UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'An Armor Item', - content: new OA\JsonContent(ref: '#/components/schemas/clothing_item_v2') - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['clothing' => $identifier] = Validator::validate( - [ - 'clothing' => $request->clothing, - ], - [ - 'clothing' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $identifier = QueryBuilder::for(Armor::class, $request) - ->where('uuid', $identifier) - ->orWhere('name', $identifier) - ->orderByDesc('version') - ->allowedIncludes(ClothingResource::validIncludes()) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Armor with specified UUID or Name found.'); - } - - return new ItemResource($identifier); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/Char/ClothesController.php b/app/Http/Controllers/Api/V2/SC/Char/ClothesController.php deleted file mode 100644 index f9be1c5d1..000000000 --- a/app/Http/Controllers/Api/V2/SC/Char/ClothesController.php +++ /dev/null @@ -1,110 +0,0 @@ -where('type', 'LIKE', 'Char_Clothing%') - ->allowedFilters([ - AllowedFilter::partial('type'), - AllowedFilter::custom('variants', new ItemVariantsFilter), - ]) - ->paginate($this->limit) - ->appends(request()->query()); - - return ItemLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/clothes/{clothing}', - tags: ['Clothing', 'In-Game', 'Items'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter(ref: '#/components/parameters/clothing_includes_v2'), - new OA\Parameter( - name: 'clothing', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Clothing name or UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A Clothing Item', - content: new OA\JsonContent(ref: '#/components/schemas/clothing_item_v2') - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['clothing' => $identifier] = Validator::validate( - [ - 'clothing' => $request->clothing, - ], - [ - 'clothing' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $identifier = QueryBuilder::for(Clothes::class, $request) - ->where('type', 'LIKE', 'Char_Clothing%') - ->where('uuid', $identifier) - ->orWhere('name', $identifier) - ->orderByDesc('version') - ->allowedIncludes(ClothingResource::validIncludes()) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Clothing with specified UUID or Name found.'); - } - - return new ItemResource($identifier); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/Char/PersonalWeapon/PersonalWeaponController.php b/app/Http/Controllers/Api/V2/SC/Char/PersonalWeapon/PersonalWeaponController.php deleted file mode 100644 index 4884b7b8d..000000000 --- a/app/Http/Controllers/Api/V2/SC/Char/PersonalWeapon/PersonalWeaponController.php +++ /dev/null @@ -1,136 +0,0 @@ -allowedFilters([ - AllowedFilter::callback('type', static function (Builder $query, $value) { - $query->whereRelation('descriptionData', 'name', 'Item Type') - ->whereRelation('descriptionData', 'value', $value); - }), - AllowedFilter::callback('class', static function (Builder $query, $value) { - $query->whereRelation('descriptionData', 'name', 'Class') - ->whereRelation('descriptionData', 'value', $value); - }), - AllowedFilter::custom('variants', new ItemVariantsFilter), - ]) - ->paginate($this->limit) - ->appends(request()->query()); - - return ItemLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/weapons/{weapon}', - tags: ['In-Game', 'Items', 'Weapons'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - description: 'Available Weapon includes', - type: 'array', - items: new OA\Items( - type: 'string', - enum: [ - 'variants', - 'ports', - 'shops', - 'shops.items', - ] - ), - ), - explode: false, - allowReserved: true - ), - new OA\Parameter( - name: 'weapon', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Weapon name of UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A Personal Weapon', - content: new OA\JsonContent(ref: '#/components/schemas/personal_weapon_item_v2') - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['weapon' => $identifier] = Validator::validate( - [ - 'weapon' => $request->weapon, - ], - [ - 'weapon' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $identifier = QueryBuilder::for(PersonalWeapon::class, $request) - ->where('uuid', $identifier) - ->orWhere('name', $identifier) - ->orderByDesc('version') - ->allowedIncludes(PersonalWeaponResource::validIncludes()) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Weapon with specified UUID or Name found.'); - } - - return new ItemResource($identifier); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/Char/PersonalWeapon/WeaponAttachmentController.php b/app/Http/Controllers/Api/V2/SC/Char/PersonalWeapon/WeaponAttachmentController.php deleted file mode 100644 index 9e61330f0..000000000 --- a/app/Http/Controllers/Api/V2/SC/Char/PersonalWeapon/WeaponAttachmentController.php +++ /dev/null @@ -1,101 +0,0 @@ -allowedIncludes(ItemResource::validIncludes()) - ->paginate($this->limit) - ->appends(request()->query()); - - return ItemLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/weapon-attachments/{attachment}', - tags: ['In-Game', 'Items', 'Weapons'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter(ref: '#/components/parameters/commodity_includes_v2'), - new OA\Parameter( - name: 'attachment', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Attachment name of UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'An Attachment Item', - content: new OA\JsonContent(ref: '#/components/schemas/personal_weapon_attachment_item_v2') - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['attachment' => $identifier] = Validator::validate( - [ - 'attachment' => $request->attachment, - ], - [ - 'attachment' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $identifier = QueryBuilder::for(Attachment::class, $request) - ->where('uuid', $identifier) - ->orWhere('name', 'LIKE', sprintf('%%%s%%', $identifier)) - ->orderByDesc('version') - ->allowedIncludes(ItemResource::validIncludes()) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Attachment with specified UUID or Name found.'); - } - - return new ItemResource($identifier); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/FactionController.php b/app/Http/Controllers/Api/V2/SC/FactionController.php deleted file mode 100644 index 5678d7cc6..000000000 --- a/app/Http/Controllers/Api/V2/SC/FactionController.php +++ /dev/null @@ -1,95 +0,0 @@ -paginate($this->limit) - ->appends(request()->query()); - - return FactionLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/factions/{faction}', - tags: ['In-Game', 'Factions'], - parameters: [ - new OA\Parameter( - name: 'faction', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Faction UUID or name', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A Faction and its relations', - content: new OA\JsonContent(ref: '#/components/schemas/faction_v2') - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['faction' => $identifier] = Validator::validate( - [ - 'faction' => $request->faction, - ], - [ - 'faction' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $faction = QueryBuilder::for(Faction::class, $request) - ->where('uuid', $identifier) - ->orWhere('name', 'LIKE', sprintf('%%%s%%', $identifier)) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Faction with specified UUID or Name found.'); - } - - return new FactionResource($faction); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/FoodController.php b/app/Http/Controllers/Api/V2/SC/FoodController.php deleted file mode 100644 index c14da3de0..000000000 --- a/app/Http/Controllers/Api/V2/SC/FoodController.php +++ /dev/null @@ -1,105 +0,0 @@ -whereIn('type', ['Bottle', 'Food', 'Drink']) - ->paginate($this->limit) - ->appends(request()->query()); - - return ItemLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/food/{food}', - tags: ['In-Game', 'Items', 'Consumables'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter(ref: '#/components/parameters/commodity_includes_v2'), - new OA\Parameter( - name: 'food', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Food name or UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A Food Item', - content: new OA\JsonContent(ref: '#/components/schemas/food_item_v2') - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['food' => $identifier] = Validator::validate( - [ - 'food' => $request->food, - ], - [ - 'food' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $identifier = QueryBuilder::for(Item::class, $request) - ->whereIn('type', ['Bottle', 'Food', 'Drink']) - ->where(function (Builder $query) use ($identifier) { - $query->where('uuid', $identifier) - ->orWhere('name', $identifier); - }) - ->orderByDesc('version') - ->allowedIncludes(FoodResource::validIncludes()) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Food with specified UUID or Name found.'); - } - - return new ItemResource($identifier); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/ItemController.php b/app/Http/Controllers/Api/V2/SC/ItemController.php deleted file mode 100644 index 222659654..000000000 --- a/app/Http/Controllers/Api/V2/SC/ItemController.php +++ /dev/null @@ -1,223 +0,0 @@ -allowedFilters([ - 'type', - 'sub_type', - AllowedFilter::exact('manufacturer', 'manufacturer.name'), - AllowedFilter::custom('variants', new ItemVariantsFilter), - ]) - ->allowedIncludes(array_merge( - ItemResource::validIncludes(), - [AllowedInclude::custom('related_items', new IncludedPassthrough)] - )) - ->paginate($this->limit) - ->appends(request()->query()); - - return ItemLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/items/{item}', - tags: ['In-Game', 'Items'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter(ref: '#/components/parameters/commodity_includes_v2'), - new OA\Parameter( - name: 'item', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Item name or UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'An Item', - content: new OA\JsonContent(ref: '#/components/schemas/item_v2') - ), - ] - )] - public function show(Request $request) - { - ['item' => $identifier] = Validator::validate( - [ - 'item' => $request->item, - ], - [ - 'item' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $item = QueryBuilder::for(Item::class, $request) - ->where(function (Builder $query) use ($identifier) { - $query->where('uuid', $identifier) - ->orWhere('name', $identifier); - }) - ->allowedIncludes(array_merge( - ItemResource::validIncludes(), - [AllowedInclude::custom('related_items', new IncludedPassthrough)] - )) - ->with([ - 'dimensions', - 'manufacturer', - 'translations', - 'container', - 'ports', - 'durabilityData', - 'descriptionData', - 'defaultTags', - 'requiredTags', - 'heatData', - 'powerData', - 'distortionData', - 'durabilityData', - 'interactions', - ]) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Item with specified UUID or Name found.'); - } - - if ($item->type === 'NOITEM_Vehicle') { - return redirect(sprintf('/api/v2/vehicles/%s', $item->uuid)); - } - - return new ItemResource($item); - } - - #[OA\Post( - path: '/api/v2/items/search', - requestBody: new OA\RequestBody( - description: 'Item Name or (sub)type', - required: true, - content: [ - new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - type: 'object', - ), - example: '{"query": "Arrow"}', - ), - ] - ), - tags: ['In-Game', 'Items', 'Search'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter(ref: '#/components/parameters/commodity_includes_v2'), - new OA\Parameter(name: 'filter[variants]', in: 'query', schema: new OA\Schema(type: 'boolean')), - new OA\Parameter(name: 'filter[type]', in: 'query', schema: new OA\Schema(type: 'string')), - new OA\Parameter(name: 'filter[sub_type]', in: 'query', schema: new OA\Schema(type: 'string')), - new OA\Parameter(name: 'filter[manufacturer]', in: 'query', schema: new OA\Schema(type: 'string')), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A List of matching Items', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/item_link_v2') - ) - ), - ] - )] - public function search(ItemSearchRequest $request): JsonResource - { - $request->validate([ - 'query' => 'required|string|min:1|max:255', - 'shop' => 'nullable|uuid', - ]); - - $toSearch = $this->cleanQueryName($request->get('query')); - - $items = QueryBuilder::for(Item::class) - ->allowedFilters([ - 'type', - 'sub_type', - AllowedFilter::exact('manufacturer', 'manufacturer.name'), - AllowedFilter::custom('variants', new ItemVariantsFilter), - ]) - ->allowedIncludes(array_merge( - ['shops.items'], - [AllowedInclude::custom('related_items', new IncludedPassthrough)] - )); - - if ($request->has('shop') && $request->get('shop') !== null) { - $items->whereRelation('shops', 'uuid', $request->get('shop')); - } - - $items = $items - ->where(function (Builder $query) use ($toSearch) { - $query->where('name', 'like', "%{$toSearch}%") - ->orWhere('uuid', $toSearch) - ->orWhere('type', $toSearch) - ->orWhere('sub_type', $toSearch); - }) - ->paginate($this->limit) - ->appends(request()->query()); - - if ($items->count() === 0) { - throw new NotFoundHttpException(sprintf(static::NOT_FOUND_STRING, $toSearch)); - } - - return ItemLinkResource::collection($items); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/ManufacturerController.php b/app/Http/Controllers/Api/V2/SC/ManufacturerController.php deleted file mode 100644 index 6746e0caa..000000000 --- a/app/Http/Controllers/Api/V2/SC/ManufacturerController.php +++ /dev/null @@ -1,145 +0,0 @@ -select(['name']) - ->selectRaw('MAX(`code`) as code') - ->groupBy('name') - ->orderBy('name') - ->paginate($this->limit) - ->appends(request()->query()); - - return ManufacturerLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/manufacturers/{manufacturer}', - tags: ['In-Game', 'Manufacturers'], - parameters: [ - new OA\Parameter( - name: 'manufacturer', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Manufacturer name, uuid, or code', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A Manufacturer and its products', - content: new OA\JsonContent(ref: '#/components/schemas/manufacturer_v2') - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['manufacturer' => $identifier] = Validator::validate( - [ - 'manufacturer' => $request->manufacturer, - ], - [ - 'manufacturer' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $shop = QueryBuilder::for(Manufacturer::class, $request) - ->where('uuid', $identifier) - ->orWhere('name', 'LIKE', sprintf('%%%s%%', $identifier)) - ->orWhere('code', 'LIKE', sprintf('%%%s%%', $identifier)) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Manufacturer with specified UUID or Name found.'); - } - - return new ManufacturerResource($shop); - } - - #[OA\Post( - path: '/api/v2/manufacturers/search', - tags: ['In-Game', 'Manufacturers', 'Search'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A List of matching Manufacturers', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/manufacturer_link_v2') - ) - ), - ] - )] - public function search(ItemSearchRequest $request): JsonResource - { - $rules = (new ItemSearchRequest)->rules(); - $request->validate($rules); - - $query = $this->cleanQueryName($request->get('query')); - - $manufacturers = QueryBuilder::for(Manufacturer::class) - ->where('name', 'like', "%{$query}%") - ->orWhere('uuid', $query) - ->orWhere('name', 'LIKE', sprintf('%%%s%%', $query)) - ->orWhere('code', 'LIKE', sprintf('%%%s%%', $query)) - ->groupBy('name') - ->paginate($this->limit) - ->appends(request()->query()); - - if ($manufacturers->count() === 0) { - throw new NotFoundHttpException(sprintf(static::NOT_FOUND_STRING, $query)); - } - - return ManufacturerLinkResource::collection($manufacturers); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/MissionController.php b/app/Http/Controllers/Api/V2/SC/MissionController.php deleted file mode 100644 index e8ce2436d..000000000 --- a/app/Http/Controllers/Api/V2/SC/MissionController.php +++ /dev/null @@ -1,102 +0,0 @@ -allowedFilters([ - AllowedFilter::partial('type', 'type.name'), - AllowedFilter::partial('name', 'title'), - AllowedFilter::partial('giver', 'mission_giver'), - ]) - ->paginate($this->limit) - ->appends(request()->query()); - - return MissionLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/missions/{mission}', - tags: ['In-Game', 'Missions'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'mission', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Mission UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A Mission', - content: new OA\JsonContent(ref: '#/components/schemas/mission_v2') - ), - ] - )] - public function show(Request $request): JsonResource - { - ['mission' => $identifier] = Validator::validate( - [ - 'mission' => $request->mission, - ], - [ - 'mission' => 'required|uuid', - ] - ); - - $model = QueryBuilder::for(Mission::class, $request) - ->where('uuid', $identifier) - ->with([ - 'giver', - 'type', - 'deadline', - 'reward', - 'requiredMissions', - 'associatedMissions', - ]) - ->firstOrFail(); - - return new MissionResource($model); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/MissionGiverController.php b/app/Http/Controllers/Api/V2/SC/MissionGiverController.php deleted file mode 100644 index 799633d40..000000000 --- a/app/Http/Controllers/Api/V2/SC/MissionGiverController.php +++ /dev/null @@ -1,88 +0,0 @@ -paginate($this->limit) - ->appends(request()->query()); - - return GiverLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/mission-givers/{giver}', - tags: ['In-Game', 'Missions'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'giver', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Mission Giver UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A Mission Giver', - content: new OA\JsonContent(ref: '#/components/schemas/mission_giver_v2') - ), - ] - )] - public function show(Request $request): JsonResource - { - ['giver' => $identifier] = Validator::validate( - [ - 'giver' => $request->giver, - ], - [ - 'giver' => 'required|uuid', - ] - ); - - $model = QueryBuilder::for(Giver::class, $request) - ->where('uuid', $identifier) - ->with([ - 'missions', - ]) - ->firstOrFail(); - - return new GiverResource($model); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/ShopController.php b/app/Http/Controllers/Api/V2/SC/ShopController.php deleted file mode 100644 index 55816aa22..000000000 --- a/app/Http/Controllers/Api/V2/SC/ShopController.php +++ /dev/null @@ -1,110 +0,0 @@ -withCount('items') - ->paginate($this->limit) - ->appends(request()->query()); - - return ShopLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/shops/{shop}', - tags: ['In-Game', 'Shops'], - parameters: [ - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - description: 'Available Commodity Item includes', - enum: [ - 'items', - ] - ), - allowReserved: true - ), - new OA\Parameter( - name: 'shop', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Shop name or UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'An Item', - content: new OA\JsonContent(ref: '#/components/schemas/shop_v2') - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['shop' => $identifier] = Validator::validate( - [ - 'shop' => $request->shop, - ], - [ - 'shop' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $shop = QueryBuilder::for(Shop::class, $request) - ->with('items') - ->where('uuid', $identifier) - ->orWhere('name', 'LIKE', sprintf('%%%s%%', $identifier)) - ->orWhere('name_raw', 'LIKE', sprintf('%%%s%%', $identifier)) - ->allowedIncludes(ShopResource::validIncludes()) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Item with specified UUID or Name found.'); - } - - return new ShopResource($shop); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/Vehicle/VehicleController.php b/app/Http/Controllers/Api/V2/SC/Vehicle/VehicleController.php deleted file mode 100644 index 711f1f402..000000000 --- a/app/Http/Controllers/Api/V2/SC/Vehicle/VehicleController.php +++ /dev/null @@ -1,236 +0,0 @@ -withoutEagerLoads() - ->with(['manufacturer']) - ->orderBy('name') - ->allowedFilters([ - AllowedFilter::partial('manufacturer', 'manufacturer.name'), - AllowedFilter::exact('chassis_id', 'chassis_id'), - ]) - ->paginate($this->limit) - ->appends(request()->query()); - - return VehicleLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/vehicles/{name}', - tags: ['Vehicles', 'RSI-Website', 'In-Game'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'filter[hardpoints]', - description: 'Filter hardpoint types, prefix with "!" to remove these hardpoints.', - in: 'query', - schema: new OA\Schema(type: 'string') - ), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - description: 'Available Vehicle includes', - type: 'array', - items: new OA\Items( - type: 'string', - enum: [ - 'components', - 'hardpoints', - 'shops', - ] - ), - ), - explode: false, - allowReserved: true - ), - new OA\Parameter( - name: 'name', - in: 'path', - required: true, - schema: new OA\Schema( - description: '(Partial) Vehicle name', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A singular vehicle', - content: new OA\JsonContent( - oneOf: [ - new OA\Schema(ref: '#/components/schemas/sc_vehicle_v2'), - new OA\Schema(ref: '#/components/schemas/vehicle_v2'), - ], - ) - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['vehicle' => $identifier] = Validator::validate( - [ - 'vehicle' => $request->vehicle, - ], - [ - 'vehicle' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - $underscored = str_replace(' ', '_', $identifier); - - try { - $vehicleModel = QueryBuilder::for(UnpackedVehicle::class) - ->where('name', $identifier) - ->orWhere('class_name', $underscored) - ->orWhere('class_name', 'LIKE', "%_$underscored") - ->orWhere('class_name', $identifier) - ->orWhere('item_uuid', $identifier) - ->with([ - 'armor', - 'flightController', - 'quantumDrives', - 'shields', - 'thrusters', - 'partsWithoutParent', - ]) - ->first(); - - if ($vehicleModel === null) { - $vehicleModel = QueryBuilder::for(Vehicle::class, $request) - ->where('name', $identifier) - ->orWhere('slug', $identifier) - ->orWhereRelation('sc', 'item_uuid', $identifier) - ->firstOrFail(); - - return new VehicleResource($vehicleModel); - } - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Vehicle with specified name found.'.$request->vehicle); - } - - return new \App\Http\Resources\SC\Vehicle\VehicleResource($vehicleModel); - } - - #[OA\Post( - path: '/api/v2/vehicles/search', - requestBody: new OA\RequestBody( - description: 'Vehicle (partial) name or slug', - required: true, - content: [ - new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - type: 'object', - ), - example: '{"query": "Merchant"}', - ), - ] - ), - tags: ['Vehicles', 'RSI-Website', 'In-Game', 'Search'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter(name: 'filter[manufacturer]', in: 'query', schema: new OA\Schema(type: 'string')), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of vehicles matching the query', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/vehicle_link_v2') - ) - ), - new OA\Response( - response: 404, - description: 'No vehicle(s) found.', - ), - ], - )] - public function search(Request $request): AnonymousResourceCollection - { - $rules = (new VehicleSearchRequest)->rules(); - - $request->validate($rules); - - $identifier = $this->cleanQueryName($request->get('query')); - $underscored = str_replace(' ', '_', $identifier); - - $queryBuilder = QueryBuilder::for(UnpackedVehicle::class) - ->where(function (Builder $query) use ($identifier, $underscored) { - $query->Where('class_name', 'LIKE', "%{$underscored}") - ->orWhere('class_name', $identifier) - ->orWhere('item_uuid', $identifier) - ->orWhere('name', 'LIKE', "%{$identifier}%"); - }) - ->allowedFilters([ - AllowedFilter::partial('manufacturer', 'manufacturer.name'), - ]) - ->paginate($this->limit) - ->appends(request()->query()); - - if ($queryBuilder->count() === 0) { - $queryBuilder = QueryBuilder::for(Vehicle::class, $request) - ->where('name', 'LIKE', "%{$identifier}%") - ->orWhere('slug', $identifier) - ->orWhereRelation('sc', 'item_uuid', $identifier) - ->paginate($this->limit) - ->appends(request()->query()); - } - - if ($queryBuilder->count() === 0) { - throw new NotFoundHttpException(sprintf(static::NOT_FOUND_STRING, $identifier)); - } - - return VehicleLinkResource::collection($queryBuilder); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/Vehicle/VehicleItemController.php b/app/Http/Controllers/Api/V2/SC/Vehicle/VehicleItemController.php deleted file mode 100644 index b5dff0293..000000000 --- a/app/Http/Controllers/Api/V2/SC/Vehicle/VehicleItemController.php +++ /dev/null @@ -1,124 +0,0 @@ -allowedFilters([ - AllowedFilter::callback('type', static function (Builder $query, $value) { - $query->whereRelation('descriptionData', 'name', 'Item Type') - ->whereRelation('descriptionData', 'value', $value); - }), - AllowedFilter::callback('grade', static function (Builder $query, $value) { - $query->whereRelation('descriptionData', 'name', 'Grade') - ->whereRelation('descriptionData', 'value', $value); - }), - AllowedFilter::callback('class', static function (Builder $query, $value) { - $query->whereRelation('descriptionData', 'name', 'Class') - ->whereRelation('descriptionData', 'value', $value); - }), - ]) - ->allowedIncludes(['shops', 'shops.items']) - ->paginate($this->limit) - ->appends(request()->query()); - - return VehicleItemLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/vehicle-items/{item}', - tags: ['Vehicles', 'In-Game', 'Items'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter(ref: '#/components/parameters/commodity_includes_v2'), - new OA\Parameter( - name: 'item', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Item name or UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A Vehicle Item', - content: new OA\JsonContent(ref: '#/components/schemas/vehicle_weapon_v2') - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['item' => $identifier] = Validator::validate( - [ - 'item' => $request->item, - ], - [ - 'item' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $identifier = QueryBuilder::for(VehicleItem::class, $request) - ->with([ - 'powerData', - 'distortionData', - 'heatData', - ]) - ->where('uuid', $identifier) - ->orWhere('name', $identifier) - ->orderByDesc('version') - ->allowedIncludes(ItemResource::validIncludes()) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Item with specified UUID or Name found.'); - } - - return new ItemResource($identifier); - } -} diff --git a/app/Http/Controllers/Api/V2/SC/Vehicle/VehicleWeaponController.php b/app/Http/Controllers/Api/V2/SC/Vehicle/VehicleWeaponController.php deleted file mode 100644 index eed65b27f..000000000 --- a/app/Http/Controllers/Api/V2/SC/Vehicle/VehicleWeaponController.php +++ /dev/null @@ -1,113 +0,0 @@ -where('type', 'WeaponGun') - ->allowedIncludes(VehicleWeaponResource::validIncludes()) - ->allowedFilters([ - AllowedFilter::callback('type', static function (Builder $query, $value) { - $query->whereRelation('descriptionData', 'name', 'Item Type') - ->whereRelation('descriptionData', 'value', $value); - }), - ]) - ->paginate($this->limit) - ->appends(request()->query()); - - return VehicleItemLinkResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/vehicle-weapons/{weapon}', - tags: ['Vehicles', 'In-Game', 'Weapons'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter(ref: '#/components/parameters/commodity_includes_v2'), - new OA\Parameter( - name: 'weapon', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Item name or UUID', - type: 'string', - ), - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A Vehicle Weapon', - content: new OA\JsonContent(ref: '#/components/schemas/vehicle_weapon_v2') - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['weapon' => $identifier] = Validator::validate( - [ - 'weapon' => $request->weapon, - ], - [ - 'weapon' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $identifier = QueryBuilder::for(Item::class, $request) - ->where('type', 'WeaponGun') - ->where(function (Builder $query) use ($identifier) { - $query->where('uuid', $identifier) - ->orWhere('name', 'LIKE', sprintf('%%%s%%', $identifier)); - }) - ->orderByDesc('version') - ->allowedIncludes(VehicleWeaponResource::validIncludes()) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Weapon with specified UUID or Name found.'); - } - - return new ItemResource($identifier); - } -} diff --git a/app/Http/Controllers/Api/V2/StarCitizen/GalactapediaController.php b/app/Http/Controllers/Api/V2/StarCitizen/GalactapediaController.php deleted file mode 100644 index 074326a40..000000000 --- a/app/Http/Controllers/Api/V2/StarCitizen/GalactapediaController.php +++ /dev/null @@ -1,194 +0,0 @@ -allowedFilters([ - AllowedFilter::exact('category', 'category.name'), - AllowedFilter::exact('categoryId', 'category.cig_id'), - - AllowedFilter::exact('tag', 'tag.name'), - AllowedFilter::exact('tagId', 'tag.cig_id'), - - AllowedFilter::exact('property', 'property.name'), - AllowedFilter::exact('template', 'template.template'), - ]) - ->orderByDesc('id') - ->paginate($this->limit) - ->appends(request()->query()); - - return ArticleResource::collection($query); - } - - #[OA\Get( - path: '/api/v2/galactapedia/{id}', - tags: ['Galactapedia', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter( - name: 'id', - in: 'path', - required: true, - schema: new OA\Schema( - description: 'Galactapedia Article ID', - type: 'string', - ), - ), - new OA\Parameter( - name: 'include', - in: 'query', - schema: new OA\Schema( - description: 'Available Galactapedia includes', - type: 'array', - items: new OA\Items( - type: 'string', - enum: [ - 'translations', - 'tags', - 'categories', - 'related_articles', - 'properties', - ] - ), - ), - explode: false, - allowReserved: true - ), - ], - responses: [ - new OA\Response( - response: 200, - description: 'A singular Article', - content: new OA\JsonContent(ref: '#/components/schemas/galactapedia_article_v2') - ), - new OA\Response( - response: 404, - description: 'No Article with specified ID found.', - ), - ] - )] - public function show(Request $request): AbstractBaseResource - { - ['article' => $identifier] = Validator::validate( - [ - 'article' => $request->article, - ], - [ - 'article' => 'required|string|min:10|max:12', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - - try { - $model = QueryBuilder::for(Article::class, $request) - ->where('cig_id', $identifier) - ->with(ArticleResource::validIncludes()) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Article with specified ID found.'); - } - - return new ArticleResource($model); - } - - #[OA\Post( - path: '/api/v2/galactapedia/search', - requestBody: new OA\RequestBody( - description: 'Article (partial) title, template or slug', - required: true, - content: [ - new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - type: 'object', - ), - example: '{"query": "Banu"}', - ), - ] - ), - tags: ['Galactapedia', 'RSI-Website', 'Search'], - responses: [ - new OA\Response( - response: 200, - description: 'List of articles matching the query', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/galactapedia_article_v2') - ) - ), - new OA\Response( - response: 404, - description: 'No Article found.', - ), - ], - )] - public function search(Request $request): AnonymousResourceCollection - { - $rules = (new GalactapediaSearchRequest)->rules(); - $request->validate($rules); - - $query = $this->cleanQueryName($request->get('query')); - - $queryBuilder = QueryBuilder::for(Article::class, $request) - ->where('title', 'like', "%{$query}%") - ->orWhere('slug', 'like', "%{$query}%") - ->orWhere('cig_id', $query) - ->orWhereHas('templates', function (Builder $builder) use ($query) { - return $builder->where('template', 'like', "%{$query}%"); - }) - ->paginate($this->limit) - ->appends(request()->query()); - - if ($queryBuilder->count() === 0) { - throw new NotFoundHttpException('No Article(s) for specified query found.'); - } - - return ArticleResource::collection($queryBuilder); - } -} diff --git a/app/Http/Controllers/Api/V2/StarCitizen/Starmap/CelestialObjectController.php b/app/Http/Controllers/Api/V2/StarCitizen/Starmap/CelestialObjectController.php deleted file mode 100644 index 394b51db0..000000000 --- a/app/Http/Controllers/Api/V2/StarCitizen/Starmap/CelestialObjectController.php +++ /dev/null @@ -1,79 +0,0 @@ -allowedIncludes([]) - ->paginate($this->limit) - ->appends(request()->query()); - - return CelestialObjectResource::collection($query); - } - - public function show(Request $request): AbstractBaseResource - { - ['code' => $code] = Validator::validate( - [ - 'code' => $request->code, - ], - [ - 'code' => 'required|string|min:1|max:255', - ] - ); - - $code = mb_strtoupper(urldecode($code)); - - try { - /** @var CelestialObject $starsystem */ - $starsystem = QueryBuilder::for(CelestialObject::class, $request) - ->where('code', $code) - ->orWhere('cig_id', $code) - ->orWhere('name', 'LIKE', "%$code%") - ->allowedIncludes(CelestialObjectResource::validIncludes()) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Celestial Object with specified Code or Name found.'); - } - - return new CelestialObjectResource($starsystem); - } - - public function search(Request $request): AnonymousResourceCollection - { - $rules = (new StarsystemRequest)->rules(); - $request->validate($rules); - - $query = $this->cleanQueryName($request->get('query')); - - $objects = QueryBuilder::for(CelestialObject::class) - ->where('code', $query) - ->orWhere('cig_id', $query) - ->orWhere('name', 'LIKE', "%$query%") - ->paginate($this->limit) - ->appends(request()->query()); - - if ($objects->count() === 0) { - throw new NotFoundHttpException(sprintf(static::NOT_FOUND_STRING, $query)); - } - - return CelestialObjectResource::collection($objects); - } -} diff --git a/app/Http/Controllers/Api/V2/StarCitizen/Starmap/StarsystemController.php b/app/Http/Controllers/Api/V2/StarCitizen/Starmap/StarsystemController.php deleted file mode 100644 index b0ba8fff6..000000000 --- a/app/Http/Controllers/Api/V2/StarCitizen/Starmap/StarsystemController.php +++ /dev/null @@ -1,78 +0,0 @@ -allowedIncludes([]) - ->paginate($this->limit) - ->appends(request()->query()); - - return StarsystemResource::collection($query); - } - - public function show(Request $request): StarsystemResource - { - ['code' => $code] = Validator::validate( - [ - 'code' => $request->code, - ], - [ - 'code' => 'required|string|min:1|max:255', - ] - ); - - $code = mb_strtoupper(urldecode($code)); - - try { - /** @var Starsystem $starsystem */ - $starsystem = QueryBuilder::for(Starsystem::class, $request) - ->where('code', $code) - ->orWhere('cig_id', $code) - ->orWhere('name', 'LIKE', "%$code%") - ->allowedIncludes(StarsystemResource::validIncludes()) - ->firstOrFail(); - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Starsystem with specified Code or Name found.'); - } - - return new StarsystemResource($starsystem); - } - - public function search(Request $request): AnonymousResourceCollection - { - $rules = (new StarsystemRequest)->rules(); - $request->validate($rules); - - $query = $this->cleanQueryName($request->get('query')); - - $starsystems = QueryBuilder::for(Starsystem::class) - ->where('code', $query) - ->orWhere('cig_id', $query) - ->orWhere('name', 'LIKE', "%$query%") - ->paginate($this->limit) - ->appends(request()->query()); - - if ($starsystems->count() === 0) { - throw new NotFoundHttpException(sprintf(static::NOT_FOUND_STRING, $query)); - } - - return StarsystemResource::collection($starsystems); - } -} diff --git a/app/Http/Controllers/Api/V2/StarCitizen/StatController.php b/app/Http/Controllers/Api/V2/StarCitizen/StatController.php deleted file mode 100644 index 0cf505419..000000000 --- a/app/Http/Controllers/Api/V2/StarCitizen/StatController.php +++ /dev/null @@ -1,62 +0,0 @@ -orderByDesc('created_at')->first(); - - return new StatResource($stat); - } - - #[OA\Get( - path: '/api/v2/stats', - tags: ['Stats', 'RSI-Website'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of stats', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/stat_v2') - ) - ), - ] - )] - public function index(Request $request): AnonymousResourceCollection - { - $query = QueryBuilder::for(Stat::class, $request) - ->orderByDesc('created_at') - ->paginate($this->limit) - ->appends(request()->query()); - - return StatResource::collection($query); - } -} diff --git a/app/Http/Controllers/Api/V3/SC/Vehicle/VehicleController.php b/app/Http/Controllers/Api/V3/SC/Vehicle/VehicleController.php deleted file mode 100644 index 5b002a26d..000000000 --- a/app/Http/Controllers/Api/V3/SC/Vehicle/VehicleController.php +++ /dev/null @@ -1,188 +0,0 @@ - $identifier] = Validator::validate( - [ - 'vehicle' => $request->vehicle, - ], - [ - 'vehicle' => 'required|string|min:1|max:255', - ] - ); - - $identifier = $this->cleanQueryName($identifier); - $underscored = str_replace(' ', '_', $identifier); - - try { - $vehicleModel = QueryBuilder::for(UnpackedVehicle::class) - ->where('name', $identifier) - ->orWhere('class_name', $underscored) - ->orWhere('class_name', 'LIKE', "%_$underscored") - ->orWhere('class_name', $identifier) - ->orWhere('item_uuid', $identifier) - ->with([ - 'armor', - 'flightController', - 'quantumDrives', - 'shields', - 'thrusters', - 'partsWithoutParent', - ]) - ->first(); - - if ($vehicleModel === null) { - $vehicleModel = QueryBuilder::for(Vehicle::class, $request) - ->where('name', $identifier) - ->orWhere('slug', $identifier) - ->orWhereRelation('sc', 'item_uuid', $identifier) - ->firstOrFail(); - - return new VehicleResource($vehicleModel); - } - } catch (ModelNotFoundException $e) { - throw new NotFoundHttpException('No Vehicle with specified name found.'.$request->vehicle); - } - - return new VehicleResourceV3($vehicleModel); - } - - #[OA\Post( - path: '/api/v3/vehicles/search', - requestBody: new OA\RequestBody( - description: 'Vehicle (partial) name or slug', - required: true, - content: [ - new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - type: 'object', - ), - example: '{"query": "Merchant"}', - ), - ] - ), - tags: ['Vehicles', 'RSI-Website', 'In-Game', 'Search'], - parameters: [ - new OA\Parameter(ref: '#/components/parameters/page'), - new OA\Parameter(ref: '#/components/parameters/limit'), - new OA\Parameter(ref: '#/components/parameters/locale'), - new OA\Parameter(name: 'filter[manufacturer]', in: 'query', schema: new OA\Schema(type: 'string')), - ], - responses: [ - new OA\Response( - response: 200, - description: 'List of vehicles matching the query', - content: new OA\JsonContent( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/vehicle_link_v2') - ) - ), - new OA\Response( - response: 404, - description: 'No vehicle(s) found.', - ), - ], - )] - public function search(Request $request): AnonymousResourceCollection - { - return parent::search($request); - } -} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index fe6c02fe2..7a5861578 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -4,25 +4,21 @@ namespace App\Http\Controllers; -use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Foundation\Bus\DispatchesJobs; -use Illuminate\Foundation\Validation\ValidatesRequests; -use Illuminate\Routing\Controller as BaseController; +use OpenApi\Attributes as OA; -/** - * Class Controller - */ -class Controller extends BaseController +#[OA\Info( + version: '3.0.0', + title: 'Star Citizen API', + contact: new OA\Contact(email: 'foxftw@star-citizen.wiki'), +)] +#[OA\Server(url: 'https://api.star-citizen.wiki')] +abstract class Controller { - use AuthorizesRequests; - use DispatchesJobs; - use ValidatesRequests; - /** - * Controller constructor. + * Clean the name for query use. */ - public function __construct() + protected function cleanQueryName(string $name): string { - // Base Constructor + return str_replace('_', ' ', urldecode($name)); } } diff --git a/app/Http/Controllers/GameVersionSelectionController.php b/app/Http/Controllers/GameVersionSelectionController.php new file mode 100644 index 000000000..58332d4f1 --- /dev/null +++ b/app/Http/Controllers/GameVersionSelectionController.php @@ -0,0 +1,47 @@ +string('version')->toString(); + + $gameVersion = GameVersion::query() + ->whereRaw('LOWER(code) = ?', [strtolower($versionCode)]) + ->firstOrFail(); + + $request->session()->put('game_version_code', $gameVersion->code); + + $redirectUrl = $this->resolveRedirectUrl($request->input('redirect')); + + return redirect()->to(url()->query($redirectUrl, ['version' => $gameVersion->code])); + } + + protected function resolveRedirectUrl(?string $redirect): string + { + $redirect = $redirect ?: url()->previous(); + + $appHost = parse_url(url('/'), PHP_URL_HOST); + $redirectHost = parse_url($redirect, PHP_URL_HOST); + + if ($redirectHost === null) { + $redirect = '/'.ltrim($redirect, '/'); + + return url($redirect); + } + + if ($redirectHost === $appHost) { + return $redirect; + } + + return url('/'); + } +} diff --git a/app/Http/Controllers/Web/Account/AccountController.php b/app/Http/Controllers/Web/Account/AccountController.php deleted file mode 100644 index 32185c71a..000000000 --- a/app/Http/Controllers/Web/Account/AccountController.php +++ /dev/null @@ -1,79 +0,0 @@ -middleware('auth'); - } - - /** - * @return Factory|View - * - * @throws AuthorizationException - */ - public function index() - { - $this->authorize('web.account.view'); - - return view( - 'web.account.index', - [ - 'user' => Auth::user(), - ] - ); - } - - /** - * @return RedirectResponse - * - * @throws AuthorizationException - */ - public function update(Request $request) - { - $this->authorize('web.account.update'); - - Validator::validate($request->all(), [ - 'language' => 'required|in:en,de', - ]); - - Auth::user()->settings()->updateOrCreate( - [ - 'user_id' => Auth::id(), - ], - [ - 'receive_comm_link_notifications' => $request->has('receive_comm_link_notifications'), - 'receive_api_notifications' => $request->has('api_notifications'), - 'language' => $request->get('language'), - ] - ); - - return redirect()->route('web.account.index')->withMessages( - [ - 'success' => [ - __('crud.updated', ['type' => __('Einstellungen')]), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Auth/LoginController.php b/app/Http/Controllers/Web/Auth/LoginController.php deleted file mode 100644 index 3429ede5e..000000000 --- a/app/Http/Controllers/Web/Auth/LoginController.php +++ /dev/null @@ -1,131 +0,0 @@ -middleware('guest', ['except' => 'logout']); - $this->authRepository = $authRepository; - } - - /** - * Get the path that we should redirect once logged out. - * Adaptable to user needs. - * - * @return string - */ - public function logoutToPath() - { - return '/'; - } - - /** - * Get the login username to be used by the controller. - * - * @return string - */ - public function username() - { - return 'username'; - } - - /** - * @return Factory|View - */ - public function showLoginForm() - { - return view('web.auth.login'); - } - - /** - * Redirect the user to the GitHub authentication page. - * - * @return Response - */ - public function redirectToProvider() - { - return $this->authRepository->startAuth(); - } - - /** - * Obtain the user information from the Provider. - */ - public function handleProviderCallback(Request $request): RedirectResponse - { - $user = $this->authRepository->getUserFromProvider($request); - - $authUser = $this->authRepository->getOrCreateLocalUser($user, 'mediawiki'); - - Auth::login($authUser); - - return $this->authenticated(); - } - - /** - * Redirect to Intended Route or Account. - */ - protected function authenticated(): RedirectResponse - { - return redirect()->intended($this->getRedirectTo()); - } - - public function getRedirectTo(): string - { - if (Auth::user()->isAdmin()) { - return '/dashboard'; - } - - return $this->redirectTo; - } - - /** - * Redirect to Login Form. - */ - protected function loggedOut(Request $request): RedirectResponse - { - return redirect()->route('web.auth.login'); - } -} diff --git a/app/Http/Controllers/Web/Changelog/ChangelogController.php b/app/Http/Controllers/Web/Changelog/ChangelogController.php deleted file mode 100644 index dde9fe21c..000000000 --- a/app/Http/Controllers/Web/Changelog/ChangelogController.php +++ /dev/null @@ -1,66 +0,0 @@ -middleware('auth'); - } - - /** - * @return Factory|View - * - * @throws AuthorizationException - */ - public function index(Request $request) - { - $this->authorize('web.changelogs.view'); - - $query = ModelChangelog::query() - //->where('changelog_type', '!=', ShopItem::class) - ->with('changelog') - ->orderByDesc('id'); - - if ($request->get('model') !== null) { - $query->where( - 'changelog_type', - $request->get('model'), - ); - } - - if ($request->get('type') !== null) { - $query->where( - 'type', - $request->get('type'), - ); - } - - return view( - 'web.changelog.index', - [ - 'changelogs' => $query->paginate($request->get('limit', 25)), - 'models' => ModelChangelog::query()->select('changelog_type')->distinct()->get(), - 'types' => ModelChangelog::query()->select('type')->distinct()->get(), - ] - ); - } -} diff --git a/app/Http/Controllers/Web/DashboardController.php b/app/Http/Controllers/Web/DashboardController.php deleted file mode 100644 index 8d0715cd6..000000000 --- a/app/Http/Controllers/Web/DashboardController.php +++ /dev/null @@ -1,153 +0,0 @@ -middleware('auth')->except('index'); - } - - /** - * Returns the Dashboard View. - */ - public function index(): View - { - $data = []; - if (Auth::user() !== null && Auth::user()->can('web.dashboard.view')) { - $data = [ - 'users' => $this->getUserStats(), - 'deepl' => $this->getDeeplStats(), - 'jobs' => $this->getQueueStats(), - ]; - } - - return view( - 'web.dashboard', - $data - ); - } - - /** - * User Stats - * New Registrations and Logins. - */ - private function getUserStats(): array - { - $today = Carbon::today()->toDateString(); - - return [ - 'overall' => User::query()->count(), - 'last' => User::query()->take(5)->orderBy('created_at', 'desc')->get(), - 'registrations' => [ - 'counts' => [ - 'last_hour' => User::query()->whereDate('created_at', '>', Carbon::now()->subHour())->count(), - 'today' => User::query()->whereDate('created_at', '=', $today)->get()->count(), - ], - ], - 'logins' => [ - 'counts' => [ - 'last_hour' => User::query()->whereDate('last_login', '>', Carbon::now()->subHour())->count(), - 'today' => User::query()->whereDate('last_login', '=', $today)->get()->count(), - ], - ], - ]; - } - - /** - * Deepl Usage Stats. - */ - private function getDeeplStats(): array - { - if (Cache::has(self::DEEPL_STATS_CACHE_KEY)) { - return Cache::get(self::DEEPL_STATS_CACHE_KEY); - } - - try { - if (empty(config('services.deepl.auth_key'))) { - throw new Exception; - } - $deeplUsage = DeepLyFacade::getUsage()->getResponse(); - } catch (Exception $e) { - $deeplUsage = [ - self::DEEPL_CHARACTER_COUNT => -1, - self::DEEPL_CHARACTER_LIMIT => -1, - ]; - } - - $width = ($deeplUsage[self::DEEPL_CHARACTER_COUNT] / $deeplUsage[self::DEEPL_CHARACTER_LIMIT]) * 100; - - $style = 'bg-success'; - if ($width >= 85) { - $style = 'bg-danger'; - } elseif ($width >= 75) { - $style = 'bg-warning'; - } elseif ($width >= 50) { - $style = 'bg-info'; - } - - $stats = [ - 'usage' => [ - 'limit' => $deeplUsage[self::DEEPL_CHARACTER_LIMIT] === -1 ? __( - 'Fehler bei der Datenabfrage' - ) : number_format( - $deeplUsage[self::DEEPL_CHARACTER_LIMIT], - 0, - ',', - '.' - ), - 'count' => number_format($deeplUsage[self::DEEPL_CHARACTER_COUNT], 0, ',', '.'), - ], - 'bar' => [ - 'width' => $width, - 'style' => $style, - ], - ]; - - Cache::put(self::DEEPL_STATS_CACHE_KEY, $stats, 60); - - return $stats; - } - - /** - * Simple Queue Stat Counts. - */ - private function getQueueStats(): array - { - $jobs = DB::table('jobs')->count(); - $jobsActive = DB::table('jobs')->whereNotNull('reserved_at')->count(); - $jobsFailed = DB::table('failed_jobs')->count(); - - return [ - 'all' => number_format($jobs, 0, ',', '.'), - 'active' => number_format($jobsActive, 0, ',', '.'), - 'failed' => number_format($jobsFailed, 0, ',', '.'), - ]; - } -} diff --git a/app/Http/Controllers/Web/Game/ItemController.php b/app/Http/Controllers/Web/Game/ItemController.php new file mode 100644 index 000000000..a05fdce16 --- /dev/null +++ b/app/Http/Controllers/Web/Game/ItemController.php @@ -0,0 +1,141 @@ +normalizeFilterParams($request->input('filter', [])); + $category = $endpointFilters['category'] ?? null; + $type = $endpointFilters['type'] ?? null; + $resolvedType = $type ?? $category; + + $initialTableData = $this->apiJsonRequest->request(route('items.index', [], false), $request); + $filterPayload = $this->apiJsonRequest->request(route('items.filters', [], false), $request); + + $filterOptions = Arr::get($filterPayload, 'filters', []); + + $tableConfig = $this->itemTableConfig->build($resolvedType); + + return view('items.index', [ + 'initialTableData' => $initialTableData, + 'initialHeaderFilter' => $filterOptions, + 'initialFilters' => $this->buildInitialFilters($endpointFilters), + 'pageTitle' => $tableConfig['title'], + 'tableColumns' => $tableConfig['columns'], + 'headerFilterOptionsMap' => $tableConfig['headerFilterOptionsMap'], + 'endpointRouteName' => 'items.index', + 'endpointFilters' => $endpointFilters, + ]); + } + + public function show(Request $request, string $item): View + { + $include = array_filter(array_map('trim', explode(',', (string) $request->query('include', '')))); + $include = array_values(array_unique(array_merge($include, ['related_items']))); + + $apiRequest = $request->duplicate(); + $apiRequest->query->set('include', implode(',', $include)); + + $payload = $this->apiJsonRequest->request(route('items.show', ['identifier' => $item], false), $apiRequest); + $itemData = Arr::get($payload, 'data', []); + + if ($itemData === []) { + abort(Response::HTTP_NOT_FOUND); + } + + return view('items.show', [ + 'item' => $itemData, + 'itemMeta' => Arr::get($payload, 'meta', []), + 'pageTitle' => Arr::get($itemData, 'name', 'Item'), + ]); + } + + private function buildInitialFilters(array $filters): array + { + if ($filters === []) { + return []; + } + + $initialFilters = []; + + foreach ($filters as $field => $value) { + if ($field === 'category') { + continue; + } + + $initialFilters[] = [ + 'field' => $field, + 'value' => $value, + ]; + } + + return $initialFilters; + } + + /** + * @return array + */ + private function normalizeFilterParams(mixed $filters): array + { + if (! is_array($filters) || $filters === []) { + return []; + } + + $normalized = []; + + foreach ($filters as $field => $value) { + if (! is_string($field) || $field === '') { + continue; + } + + $normalizedValue = $this->normalizeFilterValue($value); + + if ($normalizedValue === null) { + continue; + } + + $normalized[$field] = $normalizedValue; + } + + return $normalized; + } + + private function normalizeFilterValue(mixed $value): ?string + { + if (is_array($value)) { + $values = array_map(static fn (mixed $entry): string => trim((string) $entry), $value); + $values = array_values(array_filter($values, static fn (string $entry): bool => $entry !== '')); + + if ($values === []) { + return null; + } + + return implode(',', $values); + } + + if ($value === null) { + return null; + } + + $normalized = trim((string) $value); + + return $normalized === '' ? null : $normalized; + } +} diff --git a/app/Http/Controllers/Web/Game/VehicleController.php b/app/Http/Controllers/Web/Game/VehicleController.php new file mode 100644 index 000000000..ffdc72def --- /dev/null +++ b/app/Http/Controllers/Web/Game/VehicleController.php @@ -0,0 +1,52 @@ +apiJsonRequest->request(route('vehicles.index', [], false), $request); + $filterPayload = $this->apiJsonRequest->request(route('vehicles.filters', [], false), $request); + + $allowedFilterValues = Arr::get($filterPayload, 'filters', []); + + return view('vehicles.index', [ + 'initialTableData' => $initialTableData, + 'initialHeaderFilter' => $allowedFilterValues, + ]); + } + + public function show(Request $request, string $item): View + { + $include = array_filter(array_map('trim', explode(',', (string) $request->query('include', '')))); + $include = array_values(array_unique(array_merge($include, ['shipMatrixVehicle']))); + + $apiRequest = $request->duplicate(); + $apiRequest->query->set('include', implode(',', $include)); + + $payload = $this->apiJsonRequest->request(route('vehicles.show', ['vehicle' => $item], false), $apiRequest); + $vehicleData = Arr::get($payload, 'data', []); + + if ($vehicleData === []) { + abort(Response::HTTP_NOT_FOUND); + } + + return view('vehicles.show', [ + 'vehicle' => $vehicleData, + 'vehicleMeta' => Arr::get($payload, 'meta', []), + 'pageTitle' => Arr::get($vehicleData, 'name', 'Vehicle'), + ]); + } +} diff --git a/app/Http/Controllers/Web/Job/JobController.php b/app/Http/Controllers/Web/Job/JobController.php deleted file mode 100644 index 4668b0810..000000000 --- a/app/Http/Controllers/Web/Job/JobController.php +++ /dev/null @@ -1,72 +0,0 @@ -middleware('auth'); - } - - /** - * View failed jobs - * - * - * @throws AuthorizationException - */ - public function viewFailed(): View - { - $this->authorize('web.jobs.view_failed'); - - return view('web.jobs.failed_index') - ->with( - [ - 'failed' => DB::table('failed_jobs') - ->select([ - 'id', - 'connection', - 'queue', - 'payload', - 'exception', - 'failed_at', - ]) - ->orderByDesc('id') - ->get(), - ] - ); - } - - /** - * Truncate the failed job table - * - * - * @throws AuthorizationException - */ - public function truncate(): RedirectResponse - { - $this->authorize('web.jobs.truncate'); - - DB::table('failed_jobs')->truncate(); - - return redirect()->route('web.dashboard')->withMessages( - [ - 'success' => [ - __('Jobs gelöscht'), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Job/Rsi/CommLink/JobController.php b/app/Http/Controllers/Web/Job/Rsi/CommLink/JobController.php deleted file mode 100644 index efd1b322b..000000000 --- a/app/Http/Controllers/Web/Job/Rsi/CommLink/JobController.php +++ /dev/null @@ -1,112 +0,0 @@ -middleware('auth'); - } - - /** - * @throws AuthorizationException - */ - public function startCommLinkTranslationJob(): RedirectResponse - { - $this->authorize('web.jobs.start_translation'); - - Artisan::call('comm-links:translate', ['modifiedTime' => -1]); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Übersetzung gestartet'), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - public function startCommLinkImageDownloadJob(): RedirectResponse - { - $this->authorize('web.jobs.start_image_download'); - - Artisan::call('comm-links:download-images'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Download gestartet'), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - public function startCommLinkDownloadJob(Request $request): RedirectResponse - { - $this->authorize('web.jobs.start_download'); - - $data = $request->validate( - [ - 'ids' => 'required|string|min:5', - ] - ); - - $ids = collect(explode(',', $data['ids']))->map( - static function ($id) { - return trim($id); - } - )->filter( - static function ($id) { - return is_numeric($id); - } - )->map( - static function ($id) { - return (int) $id; - } - )->filter( - static function (int $id) { - return $id >= 12663; - } - ); - - Artisan::call( - 'comm-links:download', - [ - 'id' => $ids->toArray(), - '--import' => true, - ] - ); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Comm-Link Download gestartet'), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Job/SC/JobController.php b/app/Http/Controllers/Web/Job/SC/JobController.php deleted file mode 100644 index eee47fb5d..000000000 --- a/app/Http/Controllers/Web/Job/SC/JobController.php +++ /dev/null @@ -1,114 +0,0 @@ -middleware('auth'); - } - - /** - * @throws AuthorizationException - */ - public function startItemImportJob(): RedirectResponse - { - $this->authorize('web.jobs.sc-import'); - - Artisan::call('sc:import-items'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Import gestartet'), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - public function startVehicleImportJob(): RedirectResponse - { - $this->authorize('web.jobs.sc-import'); - - Artisan::call('sc:import-vehicles'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Import gestartet'), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - public function startShopImportJob(): RedirectResponse - { - $this->authorize('web.jobs.sc-import'); - - Artisan::call('sc:import-shops'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Import gestartet'), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - public function startImageUploadJob(): RedirectResponse - { - $this->authorize('web.jobs.sc-import'); - - Artisan::call('sc:upload-images'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Bild Upload gestartet'), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - public function startTranslateJob(): RedirectResponse - { - $this->authorize('web.jobs.sc-import'); - - Artisan::call('sc:translate'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Übersetzung gestartet'), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Job/StarCitizen/Galactapedia/JobController.php b/app/Http/Controllers/Web/Job/StarCitizen/Galactapedia/JobController.php deleted file mode 100644 index 4e5f934df..000000000 --- a/app/Http/Controllers/Web/Job/StarCitizen/Galactapedia/JobController.php +++ /dev/null @@ -1,97 +0,0 @@ -middleware('auth'); - } - - /** - * @throws AuthorizationException - */ - public function startImportGalactapediaCategoriesJob(): RedirectResponse - { - $this->authorize('web.jobs.import_galactapedia_job'); - - Artisan::call('galactapedia:import-categories'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Galactapedia Kategorieimport gestartet'), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - public function startImportGalactapediaArticlesJob(): RedirectResponse - { - $this->authorize('web.jobs.import_galactapedia_job'); - - Artisan::call('galactapedia:import-articles'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Galactapedia Artikelimport gestartet'), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - public function startImportGalactapediaArticlePropertiesJob(): RedirectResponse - { - $this->authorize('web.jobs.import_galactapedia_job'); - - Artisan::call('galactapedia:import-properties'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Galactapedia Import der Artikeleigenschaften gestartet'), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - public function startCreateWikiPagesJob(): RedirectResponse - { - $this->authorize('web.jobs.import_galactapedia_job'); - - Artisan::call('galactapedia:create-wiki-pages'); - Artisan::call('galactapedia:upload-wiki-images'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Galactapedia Wiki Seiten werden erstellt'), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Job/StarCitizen/Vehicle/JobController.php b/app/Http/Controllers/Web/Job/StarCitizen/Vehicle/JobController.php deleted file mode 100644 index 7a1c89f28..000000000 --- a/app/Http/Controllers/Web/Job/StarCitizen/Vehicle/JobController.php +++ /dev/null @@ -1,78 +0,0 @@ -middleware('auth'); - } - - /** - * @throws AuthorizationException - */ - public function startDownloadShipMatrixJob(): RedirectResponse - { - $this->authorize('web.jobs.start_ship_matrix_download'); - - Artisan::call('ship-matrix:download --import'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Download gestartet'), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - public function startMsrpImportJob(): RedirectResponse - { - $this->authorize('web.jobs.start_msrp_import'); - - Artisan::call('vehicles:import-msrp'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Import gestartet'), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - public function startLoanerImportJob(): RedirectResponse - { - $this->authorize('web.jobs.start_msrp_import'); - - Artisan::call('vehicles:import-loaner'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Import gestartet'), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Job/Wiki/CommLink/JobController.php b/app/Http/Controllers/Web/Job/Wiki/CommLink/JobController.php deleted file mode 100644 index c19987d15..000000000 --- a/app/Http/Controllers/Web/Job/Wiki/CommLink/JobController.php +++ /dev/null @@ -1,67 +0,0 @@ -middleware('auth'); - } - - /** - * @throws AuthorizationException - */ - public function startCommLinkWikiPageCreationJob(): RedirectResponse - { - $this->authorize('web.jobs.start_wiki_page_creation'); - - Artisan::call('comm-links:create-wiki-pages'); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Seitenerstellung gestartet'), - ], - ] - ); - } - - /** - * Update Comm-Link Proofread Status - * - * - * @throws AuthorizationException - */ - public function startCommLinkProofReadStatusUpdateJob(): RedirectResponse - { - $this->authorize('web.jobs.start_proofread_update'); - - $this->dispatch(new UpdateCommLinkProofReadStatus); - - return redirect()->route(self::DASHBOARD_ROUTE)->withMessages( - [ - 'success' => [ - __('Update gestartet'), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Rsi/CommLink/Category/CategoryController.php b/app/Http/Controllers/Web/Rsi/CommLink/Category/CategoryController.php deleted file mode 100644 index 6809174c0..000000000 --- a/app/Http/Controllers/Web/Rsi/CommLink/Category/CategoryController.php +++ /dev/null @@ -1,64 +0,0 @@ -middleware('auth'); - } - - /** - * All Categories - * - * - * @throws AuthorizationException - */ - public function index(): View - { - $this->authorize('web.rsi.comm-links.view'); - - return view( - 'web.rsi.comm_links.categories.index', - [ - 'categories' => Category::query()->orderBy('name')->get(), - ] - ); - } - - /** - * Get All Comm-Links in a given Category - * - * - * - * @throws AuthorizationException - */ - public function show(Category $category): View - { - $this->authorize('web.rsi.comm-links.view'); - - return view( - 'web.rsi.comm_links.index', - [ - 'commLinks' => $category->commLinks() - ->orderByDesc('cig_id') - ->paginate(20), - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Rsi/CommLink/Channel/ChannelController.php b/app/Http/Controllers/Web/Rsi/CommLink/Channel/ChannelController.php deleted file mode 100644 index c4dc37d0e..000000000 --- a/app/Http/Controllers/Web/Rsi/CommLink/Channel/ChannelController.php +++ /dev/null @@ -1,64 +0,0 @@ -middleware('auth'); - } - - /** - * All Channels - * - * - * @throws AuthorizationException - */ - public function index(): View - { - $this->authorize('web.rsi.comm-links.view'); - - return view( - 'web.rsi.comm_links.channels.index', - [ - 'channels' => Channel::query()->orderBy('name')->get(), - ] - ); - } - - /** - * Get all Comm-Links of a given Channel - * - * - * - * @throws AuthorizationException - */ - public function show(Channel $channel): View - { - $this->authorize('web.rsi.comm-links.view'); - - $links = $channel->commLinks()->orderByDesc('cig_id')->paginate(20); - - return view( - 'web.rsi.comm_links.index', - [ - 'commLinks' => $links, - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Rsi/CommLink/CommLinkController.php b/app/Http/Controllers/Web/Rsi/CommLink/CommLinkController.php deleted file mode 100644 index a4bcb8920..000000000 --- a/app/Http/Controllers/Web/Rsi/CommLink/CommLinkController.php +++ /dev/null @@ -1,229 +0,0 @@ -middleware('auth')->except(['index', 'show']); - } - - /** - * Display a listing of the resource. - */ - public function index(): View - { - $links = CommLink::query()->orderByDesc('cig_id')->paginate(250); - - return view( - 'web.rsi.comm_links.index', - [ - 'commLinks' => $links, - ] - ); - } - - /** - * Display the specified resource. - */ - public function show(int $commLinkId): View - { - $commLink = CommLink::query() - ->with( - [ - 'images', - 'links', - 'translations', - 'translationChangelogs', - 'images.commLinks', - ] - ) - ->where('cig_id', $commLinkId) - ->firstOrFail(); - - /** @var Collection $changelogs */ - $changelogs = $commLink->changelogs; - - $changelogs = $changelogs->merge($commLink->translationChangelogs); - - return view( - 'web.rsi.comm_links.show', - [ - 'commLink' => $commLink, - 'changelogs' => $this->diffTranslations($changelogs, $commLink), - 'prev' => $commLink->getPrevAttribute(), - 'next' => $commLink->getNextAttribute(), - ] - ); - } - - /** - * Show the form for editing the specified resource. - * - * - * - * @throws AuthorizationException - */ - public function edit(CommLink $commLink): View - { - $this->middleware('auth'); - $this->authorize(self::COMM_LINK_PERMISSION); - - $versions = $this->getCommLinkVersions($commLink->cig_id); - $versionData = $this->processCommLinkVersions($versions, $commLink->file); - - return view( - 'web.rsi.comm_links.edit', - [ - 'commLink' => $commLink, - 'versions' => $versionData, - 'channels' => Channel::query()->orderBy('name')->get(), - 'categories' => Category::query()->orderBy('name')->get(), - 'series' => Series::query()->orderBy('name')->get(), - ] - ); - } - - /** - * Returns all Comm-Link Version Files in a Comm-Link Folder. - */ - private function getCommLinkVersions(int $commLinkCigId): array - { - $versions = Storage::disk('comm_links')->files($commLinkCigId); - $versions = array_map( - static function ($value) { - $file = preg_split('#(?<=)[/\\\]#', $value)[1]; - - return str_replace('.html', '', $file); - }, - $versions - ); - rsort($versions); - - return $versions; - } - - /** - * Parses Comm-Link Version Names to a Human readable String, creates Data array to use in views. - */ - private function processCommLinkVersions(array $versions, string $currentVersion): array - { - $versionData = []; - collect($versions)->each( - static function ($version) use (&$versionData, $currentVersion) { - $output = Carbon::createFromFormat('Y-m-d_His', $version)->format('d.m.Y H:i'); - - if ("{$version}.html" === $currentVersion) { - $output = sprintf('%s: %s', __('Aktuell'), $output); - } - - $versionData[] = [ - 'output' => $output, - 'file_clean' => $version, - 'file' => "{$version}.html", - ]; - } - ); - - return $versionData; - } - - /** - * Update the specified resource in storage. - * - * - * - * @throws AuthorizationException - */ - public function update(CommLinkRequest $request, CommLink $commLink): RedirectResponse - { - $this->middleware('auth'); - $this->authorize(self::COMM_LINK_PERMISSION); - - $data = $request->validated(); - - $commLink->update( - [ - 'title' => $data['title'], - 'url' => $data['url'], - 'created_at' => $data['created_at'], - 'channel_id' => $data['channel'], - 'category_id' => $data['category'], - 'series_id' => $data['series'], - ] - ); - - $message = __('crud.updated', ['type' => __('Comm-Link')]); - - return redirect()->route('web.rsi.comm-links.show', $commLink->getRouteKey())->withMessages( - [ - 'success' => [ - $message, - ], - ] - ); - } - - /** - * Preview a Comm-Link Version. - * - * - * - * @throws AuthorizationException - * @throws FileNotFoundException - */ - public function preview(CommLink $commLink, string $version): View - { - $this->authorize('web.rsi.comm-links.preview'); - - $content = Storage::disk('comm_links')->get("{$commLink->cig_id}/{$version}.html"); - if ($content === null) { - throw new FileNotFoundException; - } - - $crawler = new Crawler; - $crawler->addHtmlContent($content); - - $contentParser = new Content($crawler); - - return view( - 'web.rsi.comm_links.preview', - [ - 'commLink' => $commLink, - 'version' => $version, - 'preview' => $contentParser->getContent(), - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Rsi/CommLink/CommLinkSearchController.php b/app/Http/Controllers/Web/Rsi/CommLink/CommLinkSearchController.php deleted file mode 100644 index 5ad485e02..000000000 --- a/app/Http/Controllers/Web/Rsi/CommLink/CommLinkSearchController.php +++ /dev/null @@ -1,151 +0,0 @@ -with('apiToken', optional(Auth::user())->api_token); - } - - /** - * @return View|RedirectResponse - */ - public function searchByTitle(CommLinkSearchRequest $request) - { - $data = $request->validated(); - - $query = $data['keyword']; - $links = CommLink::query()->where('title', 'LIKE', "%{$query}%")->get(); - - if ($links->isEmpty()) { - return redirect()->route('web.rsi.comm-links.search')->withMessages( - [ - 'warning' => [ - 'Keine Comm-Links gefunden.', - ], - ] - ); - } - - return view( - 'web.rsi.comm_links.index', - [ - 'commLinks' => $links, - ] - ); - } - - /** - * Reverse searches a comm-link by an image url - * - * - * @return Application|Factory|View - */ - public function reverseImageLinkSearchPost(ReverseImageLinkSearchRequest $request) - { - $controller = new \App\Http\Controllers\Api\V2\Rsi\CommLink\CommLinkSearchController($request); - - return $this->handleSearchResult( - $controller->reverseImageLinkSearch($request), - 'web.rsi.comm_links.index', - 'commLinks' - ); - } - - /** - * Reverse searches a comm-link by an actual image file - * - * - * @return Application|Factory|View - */ - public function reverseImageSearchPost(ReverseImageSearchRequest $request) - { - $controller = new \App\Http\Controllers\Api\V2\Rsi\CommLink\CommLinkSearchController($request); - - return $this->handleSearchResult( - $controller->reverseImageSearch($request), - 'web.rsi.comm_links.images.index' - ); - } - - /** - * Search for comm-links based on the relevance of the input - * - * - * @return Application|Factory|View - */ - public function textSearchPost(CommLinkSearchRequest $request) - { - $this->middleware('auth'); - $data = $request->validated(); - - $data = $data['query']; - - $links = CommLink::query() - ->whereRelation('translations', function ($query) use ($data) { - $query->whereFullText('translation', $data); - }) - ->limit(50) - ->get(); - - if ($links->isEmpty()) { - return $this->handleSearchResult($links, 'web.rsi.comm_links.index'); - } - - return view( - 'web.rsi.comm_links.index', - [ - 'commLinks' => $links, - 'relevanceSorted' => true, - ] - ); - } - - /** - * Things to do after a search request was done - * - * @return Application|Factory|View - */ - private function handleSearchResult($results, string $view, string $key = 'images') - { - if ($results->isEmpty()) { - return redirect()->route('web.rsi.comm-links.search')->withMessages( - [ - 'warning' => [ - 'Keine Comm-Links gefunden.', - ], - ] - ); - } - - return view( - $view, - [ - $key => $results, - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Rsi/CommLink/Image/ImageController.php b/app/Http/Controllers/Web/Rsi/CommLink/Image/ImageController.php deleted file mode 100644 index f3f729aea..000000000 --- a/app/Http/Controllers/Web/Rsi/CommLink/Image/ImageController.php +++ /dev/null @@ -1,205 +0,0 @@ -middleware('auth')->only('upload'); - } - - /** - * All downloaded Images, excluding those that could not be found - * - * - * @return Factory|View - */ - public function index(Request $request) - { - $query = Image::query() - ->where('dir', 'NOT LIKE', 'NOT_FOUND') - ->whereNull('base_image_id'); - - $mimes = array_filter($request->get('mime', [])); - $tags = array_filter($request->get('tag', [])); - - if (! empty($mimes)) { - $query->whereHas( - 'metadata', - function (Builder $query) use ($mimes) { - return $query->whereIn('mime', $mimes); - } - ); - } else { - $query->whereHas('metadata'); - } - - if (! empty($tags)) { - $query->whereRelation( - 'tags', - function (Builder $query) use ($tags) { - return $query->whereIn('name', $tags)->orWhereIn('name_en', $tags); - } - ); - } - - if (! Auth::check()) { - $query->whereHas('commLinks'); - } - - return view( - 'web.rsi.comm_links.images.index', - [ - 'images' => $query - ->orderByDesc('id') - ->groupBy('src') - ->paginate(50), - 'mimes' => ImageMetadata::query()->groupBy('mime')->get('mime'), - 'selectedMimes' => $mimes, - 'tags' => Tag::query()->get(), - 'selectedTags' => $tags, - ] - ); - } - - /** - * @return Factory|View - */ - public function indexByTag(Request $request) - { - $tag = Tag::query()->where('name', $request->tag)->orWhere('name_en', $request->tag)->first(); - - return view( - 'web.rsi.comm_links.images.index', - [ - 'images' => optional($tag)->images ?? [], - ] - ); - } - - /** - * @return Factory|View - */ - public function show(Image $image) - { - if (! Auth::check() && $image->commLinks()->count() === 0) { - throw new ModelNotFoundException; - } - - return view( - 'web.rsi.comm_links.images.show', - [ - 'image' => $image, - ] - ); - } - - /** - * @throws GuzzleException - * @throws AuthorizationException|JsonException - */ - public function upload(ImageUploadRequest $request): string - { - $this->authorize('web.rsi.comm-links.view'); - - $params = $request->validated(); - - return (new UploadWikiImage)->uploadCommLinkImage($params); - } - - /** - * Retrieve similar images based on a hash - */ - public function similarImages(Request $request): View - { - $controller = new CommLinkSearchController($request); - - return view( - 'web.rsi.comm_links.images.index', - [ - 'images' => $controller->similarSearch($request), - ] - ); - } - - /** - * View for editing the tags of an image - */ - public function editTags(Image $image): View - { - return view( - 'web.rsi.comm_links.images.edit-tags', - [ - 'tags' => Tag::query()->orderBy('name')->get(), - 'image' => $image, - 'image_tags' => $image->tags->map(fn (Tag $tag) => $tag->name), - ] - ); - } - - /** - * Save tags to an image - */ - public function saveTags(Image $image, AddImageTagsRequest $request): RedirectResponse - { - $data = $request->validated(); - - $ids = collect($data['tags'])->map(function (string $datum) { - if (str_starts_with($datum, 'id:')) { - return (int) str_replace('id:', '', $datum); - } - - return Tag::query()->firstOrCreate(['name' => $datum])->id; - }); - - $image->tags()->sync($ids); - - return redirect()->back(); - } - - /** - * Search for images by filename - */ - public function search(ImageSearchRequest $request): View - { - $request->query->set('limit', 250); - $controller = new \App\Http\Controllers\Api\V2\Rsi\CommLink\ImageController($request); - - return view( - 'web.rsi.comm_links.images.index', - [ - 'images' => $controller->search($request), - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Rsi/CommLink/Image/TagController.php b/app/Http/Controllers/Web/Rsi/CommLink/Image/TagController.php deleted file mode 100644 index 04bb0ad46..000000000 --- a/app/Http/Controllers/Web/Rsi/CommLink/Image/TagController.php +++ /dev/null @@ -1,108 +0,0 @@ -middleware('auth'); - } - - /** - * @return Factory|View - * - * @throws AuthorizationException - */ - public function index(Request $request) - { - $this->authorize('web.rsi.comm-links.view'); - - return view( - 'web.rsi.comm_links.tags.index', - [ - 'tags' => Tag::query()->orderByDesc('images_count')->paginate(250), - ] - ); - } - - /** - * @return Factory|View - * - * @throws AuthorizationException - */ - public function edit(Tag $tag) - { - $this->authorize('web.rsi.comm-links.view'); - - return view( - 'web.rsi.comm_links.tags.edit', - [ - 'tag' => $tag, - ] - ); - } - - /** - * @return RedirectResponse - * - * @throws AuthorizationException - */ - public function update(Tag $tag, TagUpdateRequest $request) - { - $this->authorize('web.rsi.comm-links.view'); - $data = $request->validated(); - - $tag->update([ - //'name' => $data['name'], - 'name_en' => $data['name_en'], - ]); - - return redirect()->route('web.rsi.comm-links.image-tags.index')->withMessages( - [ - 'success' => [ - __('Tag aktualisiert'), - ], - ] - ); - } - - /** - * Creates a new image tag - * - * @return mixed - */ - public function post(NewImageTagRequest $request) - { - Tag::query()->create([ - 'name' => $request->name, - ]); - - return redirect()->route('web.rsi.comm-links.image-tags.index')->withMessages( - [ - 'success' => [ - __('Tag erstellt'), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Rsi/CommLink/Series/SeriesController.php b/app/Http/Controllers/Web/Rsi/CommLink/Series/SeriesController.php deleted file mode 100644 index 17ba3cb68..000000000 --- a/app/Http/Controllers/Web/Rsi/CommLink/Series/SeriesController.php +++ /dev/null @@ -1,64 +0,0 @@ -middleware('auth'); - } - - /** - * All Series - * - * - * @throws AuthorizationException - */ - public function index(): View - { - $this->authorize('web.rsi.comm-links.view'); - - return view( - 'web.rsi.comm_links.series.index', - [ - 'series' => Series::query()->orderBy('name')->get(), - ] - ); - } - - /** - * Get all Comm-Links in a given Series - * - * - * - * @throws AuthorizationException - */ - public function show(Series $series): View - { - $this->authorize('web.rsi.comm-links.view'); - - return view( - 'web.rsi.comm_links.index', - [ - 'commLinks' => $series->commLinks() - ->orderByDesc('cig_id') - ->paginate(20), - ] - ); - } -} diff --git a/app/Http/Controllers/Web/Rsi/CommLinkController.php b/app/Http/Controllers/Web/Rsi/CommLinkController.php new file mode 100644 index 000000000..56a0e2c14 --- /dev/null +++ b/app/Http/Controllers/Web/Rsi/CommLinkController.php @@ -0,0 +1,252 @@ +query('search'); + $searchQuery = trim((string) $request->query('query', '')); + $searchUrl = trim((string) $request->query('url', '')); + $initialFilters = []; + $searchCommLinks = []; + + $apiRequest = $request->duplicate(); + + if ($searchType === 'title' && $searchQuery !== '') { + $filters = []; + + if (ctype_digit($searchQuery)) { + $filters['id'] = $searchQuery; + $initialFilters[] = ['field' => 'id', 'value' => $searchQuery]; + } else { + $filters['title'] = $searchQuery; + $initialFilters[] = ['field' => 'title', 'value' => $searchQuery]; + } + + $apiRequest->query->set('filter', array_merge( + (array) $apiRequest->query->get('filter', ''), + $filters + )); + } + + if ($searchType === 'media-url' && $searchUrl !== '') { + $searchCommLinks = $this->commLinksForMediaUrl($searchUrl); + } + + $initialTableData = $this->apiJsonRequest->request(route('comm-links.index', [], false), $apiRequest); + $filterPayload = $this->apiJsonRequest->request(route('comm-links.filters', [], false), $apiRequest); + + $allowedFilterValues = Arr::get($filterPayload, 'filters', []); + + return view('comm-links.index', [ + 'initialTableData' => $initialTableData, + 'initialHeaderFilter' => $allowedFilterValues, + 'initialFilters' => $initialFilters, + 'searchType' => $searchType, + 'searchQuery' => $searchQuery, + 'searchUrl' => $searchUrl, + 'searchCommLinks' => $searchCommLinks, + ]); + } + + public function show(Request $request, int $id): View + { + $include = array_filter(array_map('trim', explode(',', (string) $request->query('include', '')))); + $include = array_values(array_unique(array_merge($include, ['images', 'links']))); + + $apiRequest = $request->duplicate(); + $apiRequest->query->set('include', implode(',', $include)); + + $payload = $this->apiJsonRequest->request(route('comm-links.show', ['id' => $id], false), $apiRequest); + $commLinkData = Arr::get($payload, 'data', []); + + if ($commLinkData === []) { + abort(Response::HTTP_NOT_FOUND); + } + + return view('comm-links.show', [ + 'commLink' => $commLinkData, + 'commLinkMeta' => Arr::get($payload, 'meta', []), + 'pageTitle' => Arr::get($commLinkData, 'title', 'Comm-Link'), + ]); + } + + public function imagesIndex(Request $request): View + { + $payload = $this->apiJsonRequest->request(route('comm-link-images.index', [], false), $request); + $imageData = Arr::get($payload, 'data', []); + $pagination = Arr::get($payload, 'meta', []); + $paginationLinks = Arr::get($payload, 'links', []); + + return view('comm-links.images.index', [ + 'images' => $imageData, + 'pagination' => $pagination, + 'paginationLinks' => $paginationLinks, + 'pageTitle' => 'Comm-Link Images', + 'searchType' => null, + 'searchQuery' => null, + ]); + } + + public function searchImagesByName(Request $request): View + { + $searchQuery = trim((string) $request->input('query', '')); + + $results = $searchQuery === '' + ? collect() + : Image::query() + ->with(['commLinks', 'tags']) + ->whereNull('base_image_id') + ->whereRaw('LOWER(src) LIKE ?', [sprintf('%%%s%%', strtolower($searchQuery))]) + ->whereRelation('metadata', 'size', '>', 0) + ->limit(100) + ->orderByDesc('created_at') + ->get(); + + $images = ImageResource::collection($results)->resolve(); + + return view('comm-links.images.index', [ + 'images' => $images, + 'pagination' => $this->paginationSummary(count($images)), + 'paginationLinks' => [], + 'pageTitle' => 'Comm-Link Images', + 'searchType' => 'media-name', + 'searchQuery' => $searchQuery, + ]); + } + + public function reverseImageSearch(Request $request, PdqHasher $hasher): View + { + $validated = $request->validate([ + 'image' => ['required', 'image', 'max:5120'], + 'similarity' => ['nullable', 'integer', 'min:1', 'max:100'], + ]); + + $file = $request->file('image'); + $contents = $file ? file_get_contents($file->getPathname()) : false; + + if ($contents === false) { + throw new HttpException(422, 'Unable to read uploaded image.'); + } + + try { + $hashResult = $hasher->hashContents($contents); + } catch (RuntimeException $exception) { + throw new HttpException(422, $exception->getMessage(), $exception); + } + + $similarity = (int) ($validated['similarity'] ?? 75); + $matches = ImageHashModel::similarImagesForHash($hashResult->toBitString(), $similarity); + + $matches->loadMissing(['commLinks', 'tags']); + + $images = ImageHashResource::collection($matches)->resolve(); + + return view('comm-links.images.index', [ + 'images' => $images, + 'pagination' => $this->paginationSummary(count($images)), + 'paginationLinks' => [], + 'pageTitle' => 'Comm-Link Images', + 'searchType' => 'reverse-image', + 'searchQuery' => '', + ]); + } + + public function showImage(Request $request, int $image): View + { + $payload = $this->apiJsonRequest->request(route('comm-link-images.show', ['image' => $image], false), $request); + $imageData = Arr::get($payload, 'data', []); + + if ($imageData === []) { + abort(Response::HTTP_NOT_FOUND); + } + + return view('comm-links.images.show', [ + 'image' => $imageData, + 'pageTitle' => sprintf('Comm-Link Image %s', Arr::get($imageData, 'id', '')), + ]); + } + + public function search(): View + { + return view('comm-links.search'); + } + + /** + * @return array + */ + private function commLinksForMediaUrl(string $url): array + { + $image = $this->imageForMediaUrl($url); + + if ($image === null) { + return []; + } + + return $image->commLinks() + ->orderByDesc('cig_id') + ->get() + ->map(fn ($commLink) => [ + 'id' => $commLink->cig_id, + 'title' => $commLink->title, + 'url' => route('web.comm-links.show', $commLink->cig_id), + ]) + ->values() + ->all(); + } + + private function imageForMediaUrl(string $url): ?Image + { + $path = parse_url(ImageParser::cleanImgSource($url), PHP_URL_PATH); + + if (! is_string($path) || $path === '') { + return null; + } + + $dir = ImageParser::getDirHash($path); + $query = Image::query(); + + if ($dir === 'i') { + $parts = explode('/', $path); + array_pop($parts); + $prefix = implode('/', $parts); + + return $query->where('src', 'LIKE', $prefix.'%')->first(); + } + + return $query->where('dir', $dir)->first(); + } + + /** + * @return array{current_page: int, last_page: int, total: int} + */ + private function paginationSummary(int $total): array + { + return [ + 'current_page' => 1, + 'last_page' => 1, + 'total' => $total, + ]; + } +} diff --git a/app/Http/Controllers/Web/Rsi/Stat/StatController.php b/app/Http/Controllers/Web/Rsi/Stat/StatController.php deleted file mode 100644 index 28600acc4..000000000 --- a/app/Http/Controllers/Web/Rsi/Stat/StatController.php +++ /dev/null @@ -1,62 +0,0 @@ -get('skip', 100); - - if (! is_numeric($every) || $every < 0) { - $every = 100; - } - - if ($request->has('from') && ! $request->has('skip')) { - $every = 0; - } - - $every = (int) $every; - - if ($every === 0 || config('database.default') === 'sqlite') { - $data = Stat::query(); - } else { - $data = Stat::query()->whereRaw('id mod '.$every.' = 0'); - } - - $from = null; - - if ($request->has('from')) { - $from = $request->get('from'); - try { - $from = Carbon::parse($from); - $every = -1; - $data->where('created_at', '>=', $from->format('Y-m-d')); - } catch (InvalidFormatException $e) { - // - } - } - - $data = $data->get(); - - return view( - 'web.rsi.stats.index', - [ - 'labels' => $data->pluck('created_at')->toJson(), - 'funds' => $data->pluck('funds')->toJson(), - 'fans' => $data->pluck('fans')->toJson(), - 'active' => $every, - 'from' => optional($from)->format('Y-m-d'), - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizen/Galactapedia/GalactapediaController.php b/app/Http/Controllers/Web/StarCitizen/Galactapedia/GalactapediaController.php deleted file mode 100644 index bbd472f30..000000000 --- a/app/Http/Controllers/Web/StarCitizen/Galactapedia/GalactapediaController.php +++ /dev/null @@ -1,55 +0,0 @@ - Article::query()->orderByDesc('id')->paginate(250), - ] - ); //->with('apiToken', optional(Auth::user())->api_token); - } - - /** - * Shows a singular article - */ - public function show(string $article): View - { - $article = Article::query()->where('cig_id', $article)->firstOrFail(); - - /** @var Collection $changelogs */ - $changelogs = $article->changelogs; - - $changelogs = $changelogs->merge($article->translationChangelogs); - - return view( - 'web.starcitizen.galactapedia.show', - [ - 'article' => $article, - 'wikitext' => (new CreateGalactapediaWikiPage($article, '')) - ->getFormattedText( - Article::normalizeContent($article->german()->translation ?? $article->english()->translation), - null - ), - 'changelogs' => $this->diffTranslations($changelogs, $article), - 'prev' => $article->getPrevAttribute(), - 'next' => $article->getNextAttribute(), - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizen/GalactapediaController.php b/app/Http/Controllers/Web/StarCitizen/GalactapediaController.php new file mode 100644 index 000000000..73a8082e5 --- /dev/null +++ b/app/Http/Controllers/Web/StarCitizen/GalactapediaController.php @@ -0,0 +1,52 @@ +apiJsonRequest->request(route('galactapedia.index', [], false), $request); + $filterPayload = $this->apiJsonRequest->request(route('galactapedia.filters', [], false), $request); + + $allowedFilterValues = Arr::get($filterPayload, 'filters', []); + + return view('galactapedia.index', [ + 'initialTableData' => $initialTableData, + 'initialHeaderFilter' => $allowedFilterValues, + ]); + } + + public function show(Request $request, string $article): View + { + $include = array_filter(array_map('trim', explode(',', (string) $request->query('include', '')))); + $include = array_values(array_unique(array_merge($include, ['categories', 'tags', 'properties', 'related']))); + + $apiRequest = $request->duplicate(); + $apiRequest->query->set('include', implode(',', $include)); + + $payload = $this->apiJsonRequest->request(route('galactapedia.show', ['article' => $article], false), $apiRequest); + $articleData = Arr::get($payload, 'data', []); + + if ($articleData === []) { + abort(Response::HTTP_NOT_FOUND); + } + + return view('galactapedia.show', [ + 'article' => $articleData, + 'articleMeta' => Arr::get($payload, 'meta', []), + 'pageTitle' => Arr::get($articleData, 'title', 'Galactapedia'), + ]); + } +} diff --git a/app/Http/Controllers/Web/StarCitizen/Manufacturer/ManufacturerController.php b/app/Http/Controllers/Web/StarCitizen/Manufacturer/ManufacturerController.php deleted file mode 100644 index fb213b5e3..000000000 --- a/app/Http/Controllers/Web/StarCitizen/Manufacturer/ManufacturerController.php +++ /dev/null @@ -1,112 +0,0 @@ -middleware('auth')->except('index'); - } - - public function index(): View - { - return view( - 'web.starcitizen.manufacturers.index', - [ - 'manufacturers' => Manufacturer::all(), - 'manufacturers_ingame' => \App\Models\SC\Manufacturer::query()->groupBy('name')->orderBy('name')->get(), - ] - ); - } - - /** - * Display the specified resource. - * - * - * - * @throws AuthorizationException - */ - public function edit(string $manufacturer): View - { - $this->authorize(self::MANUFACTURER_PERMISSION); - - $manufacturer = Manufacturer::query() - ->where('name', $manufacturer) - ->orWhere('name_short', $manufacturer) - ->firstOrFail(); - - return view( - 'web.starcitizen.manufacturers.edit', - [ - 'manufacturer' => $manufacturer, - 'updateRoute' => self::MANUFACTURER_PERMISSION, - 'changelogs' => $manufacturer->changelogs - ->merge($manufacturer->translationChangelogs) - ->sortByDesc('created_at'), - ] - ); - } - - /** - * Update the specified resource in storage. - * - * - * - * @throws AuthorizationException - */ - public function update(ManufacturerTranslationRequest $request, string $manufacturer): RedirectResponse - { - $this->authorize(self::MANUFACTURER_PERMISSION); - - $data = $request->validated(); - $manufacturer = Manufacturer::query() - ->where('name', $manufacturer) - ->orWhere('name_short', $manufacturer) - ->firstOrFail(); - - $localeCodes = Language::all('locale_code')->keyBy('locale_code'); - - foreach ($localeCodes as $localeCode => $model) { - $manufacturer->translations()->updateOrCreate( - [ - 'locale_code' => $localeCode, - ], - [ - 'description' => $data["description_{$localeCode}"], - 'known_for' => $data["known_for_{$localeCode}"], - ] - ); - } - - return redirect()->route( - 'web.starcitizen.manufacturers.edit', - $manufacturer->getRouteKey() - )->withMessages( - [ - 'success' => [ - __('crud.updated', ['type' => __('Hersteller')]), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizen/ProductionNote/ProductionNoteController.php b/app/Http/Controllers/Web/StarCitizen/ProductionNote/ProductionNoteController.php deleted file mode 100644 index 1a0cdd037..000000000 --- a/app/Http/Controllers/Web/StarCitizen/ProductionNote/ProductionNoteController.php +++ /dev/null @@ -1,97 +0,0 @@ -middleware('auth'); - } - - /** - * Display a listing of the resource. - * - * - * @throws AuthorizationException - */ - public function index(): View - { - $this->authorize('web.translations.view'); - - return view( - 'web.starcitizen.production_notes.index', - [ - 'translations' => ProductionNote::all(), - 'languages' => Language::all(), - 'editRoute' => 'web.starcitizen.production-notes.edit', - ] - ); - } - - /** - * Display the specified resource. - * - * - * - * @throws AuthorizationException - */ - public function edit(ProductionNote $note): View - { - $this->authorize('web.translations.update'); - - return view( - 'web.starcitizen.production_notes.edit', - [ - 'translation' => $note, - 'updateRoute' => 'web.starcitizen.production-notes.update', - ] - ); - } - - /** - * Update the specified resource in storage. - * - * - * - * @throws AuthorizationException - */ - public function update(TranslationRequest $request, ProductionNote $note): RedirectResponse - { - $this->authorize('web.translations.update'); - - $data = $request->validated(); - - foreach ($data as $localeCode => $translation) { - $note->translations()->updateOrCreate( - ['locale_code' => $localeCode], - ['translation' => $translation] - ); - } - - return redirect()->route('web.starcitizen.production-notes.index')->withMessages( - [ - 'success' => [ - __('crud.updated', ['type' => __('Produktionsnotiz')]), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizen/ProductionStatus/ProductionStatusController.php b/app/Http/Controllers/Web/StarCitizen/ProductionStatus/ProductionStatusController.php deleted file mode 100644 index 11b983238..000000000 --- a/app/Http/Controllers/Web/StarCitizen/ProductionStatus/ProductionStatusController.php +++ /dev/null @@ -1,96 +0,0 @@ -middleware('auth'); - } - - /** - * Display a listing of the resource. - * - * - * @throws AuthorizationException - */ - public function index(): View - { - $this->authorize('web.translations.view'); - - return view( - 'web.starcitizen.production_statuses.index', - [ - 'translations' => ProductionStatus::all(), - 'languages' => Language::all(), - 'editRoute' => 'web.starcitizen.production-statuses.edit', - ] - ); - } - - /** - * Display the specified resource. - * - * - * - * @throws AuthorizationException - */ - public function edit(ProductionStatus $productionStatus): View - { - $this->authorize('web.translations.update'); - - return view( - 'web.starcitizen.production_statuses.edit', - [ - 'translation' => $productionStatus, - 'updateRoute' => 'web.starcitizen.production-statuses.update', - ] - ); - } - - /** - * Update the specified resource in storage. - * - * - * - * @throws AuthorizationException - */ - public function update(TranslationRequest $request, ProductionStatus $productionStatus): RedirectResponse - { - $this->authorize('web.translations.update'); - $data = $request->validated(); - - foreach ($data as $localeCode => $translation) { - $productionStatus->translations()->updateOrCreate( - ['locale_code' => $localeCode], - ['translation' => $translation] - ); - } - - return redirect()->route('web.starcitizen.production-statuses.index')->withMessages( - [ - 'success' => [ - __('crud.updated', ['type' => __('Produktionsstatus')]), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizen/ShipMatrixVehicleController.php b/app/Http/Controllers/Web/StarCitizen/ShipMatrixVehicleController.php new file mode 100644 index 000000000..d2bc2429d --- /dev/null +++ b/app/Http/Controllers/Web/StarCitizen/ShipMatrixVehicleController.php @@ -0,0 +1,29 @@ +apiJsonRequest->request(route('shipmatrix.vehicles.index', [], false), $request); + $filterPayload = $this->apiJsonRequest->request(route('shipmatrix.vehicles.filters', [], false), $request); + + $allowedFilterValues = Arr::get($filterPayload, 'filters', []); + + return view('ship-matrix.index', [ + 'initialTableData' => $initialTableData, + 'initialHeaderFilter' => $allowedFilterValues, + ]); + } +} diff --git a/app/Http/Controllers/Web/StarCitizen/Starmap/CelestialObject/CelestialObjectController.php b/app/Http/Controllers/Web/StarCitizen/Starmap/CelestialObject/CelestialObjectController.php deleted file mode 100644 index ee627443d..000000000 --- a/app/Http/Controllers/Web/StarCitizen/Starmap/CelestialObject/CelestialObjectController.php +++ /dev/null @@ -1,25 +0,0 @@ - CelestialObject::query()->with('starsystem')->orderBy('code')->get(), - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizen/Starmap/CelestialObjectController.php b/app/Http/Controllers/Web/StarCitizen/Starmap/CelestialObjectController.php new file mode 100644 index 000000000..cf2a9669e --- /dev/null +++ b/app/Http/Controllers/Web/StarCitizen/Starmap/CelestialObjectController.php @@ -0,0 +1,34 @@ +apiJsonRequest->request( + route('celestial-objects.index', ['include' => 'starsystem'], false), + $request + ); + + return view('starmap.celestial-objects.index', [ + 'initialTableData' => $initialTableData, + 'initialHeaderFilter' => [], + ]); + } + + public function show(string $id): Response + { + return response('', Response::HTTP_NO_CONTENT); + } +} diff --git a/app/Http/Controllers/Web/StarCitizen/Starmap/Starsystem/StarsystemController.php b/app/Http/Controllers/Web/StarCitizen/Starmap/Starsystem/StarsystemController.php deleted file mode 100644 index 644174ee9..000000000 --- a/app/Http/Controllers/Web/StarCitizen/Starmap/Starsystem/StarsystemController.php +++ /dev/null @@ -1,35 +0,0 @@ - Starsystem::query()->orderBy('code')->get(), - ] - ); - } - - public function show(Starsystem $starsystem): View - { - return view( - 'web.starcitizen.starmap.starsystems.show', - [ - 'system' => $starsystem, - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizen/Starmap/StarsystemController.php b/app/Http/Controllers/Web/StarCitizen/Starmap/StarsystemController.php new file mode 100644 index 000000000..32a6ba4af --- /dev/null +++ b/app/Http/Controllers/Web/StarCitizen/Starmap/StarsystemController.php @@ -0,0 +1,35 @@ +apiJsonRequest->request(route('starsystems.index', [], false), $request); + $filterPayload = $this->apiJsonRequest->request(route('starsystems.filters', [], false), $request); + + $allowedFilterValues = Arr::get($filterPayload, 'filters', []); + + return view('starmap.systems.index', [ + 'initialTableData' => $initialTableData, + 'initialHeaderFilter' => $allowedFilterValues, + ]); + } + + public function show(string $id): Response + { + return response('', Response::HTTP_NO_CONTENT); + } +} diff --git a/app/Http/Controllers/Web/StarCitizen/StatController.php b/app/Http/Controllers/Web/StarCitizen/StatController.php new file mode 100644 index 000000000..a540eac14 --- /dev/null +++ b/app/Http/Controllers/Web/StarCitizen/StatController.php @@ -0,0 +1,25 @@ +apiJsonRequest->request(route('stats.latest', [], false), $request); + + return view('stats.index', [ + 'latestStats' => Arr::get($latestStats, 'data', []), + ]); + } +} diff --git a/app/Http/Controllers/Web/StarCitizen/Vehicle/Focus/FocusController.php b/app/Http/Controllers/Web/StarCitizen/Vehicle/Focus/FocusController.php deleted file mode 100644 index cb03a06fa..000000000 --- a/app/Http/Controllers/Web/StarCitizen/Vehicle/Focus/FocusController.php +++ /dev/null @@ -1,97 +0,0 @@ -middleware('auth'); - } - - /** - * Display a listing of the resource. - * - * - * @throws AuthorizationException - */ - public function index(): View - { - $this->authorize('web.translations.view'); - - return view( - 'web.starcitizen.vehicles.foci.index', - [ - 'translations' => Focus::all(), - 'languages' => Language::all(), - 'editRoute' => 'web.starcitizen.vehicles.foci.edit', - ] - ); - } - - /** - * Display the specified resource. - * - * - * - * @throws AuthorizationException - */ - public function edit(Focus $focus): View - { - $this->authorize('web.translations.update'); - - return view( - 'web.starcitizen.vehicles.foci.edit', - [ - 'translation' => $focus, - 'updateRoute' => 'web.starcitizen.vehicles.foci.update', - ] - ); - } - - /** - * Update the specified resource in storage. - * - * - * - * @throws AuthorizationException - */ - public function update(TranslationRequest $request, Focus $focus): RedirectResponse - { - $this->authorize('web.translations.update'); - - $data = $request->validated(); - - foreach ($data as $localeCode => $translation) { - $focus->translations()->updateOrCreate( - ['locale_code' => $localeCode], - ['translation' => $translation] - ); - } - - return redirect()->route('web.starcitizen.vehicles.foci.index')->withMessages( - [ - 'success' => [ - __('crud.updated', ['type' => __('Fahrzeugfokus')]), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizen/Vehicle/GroundVehicle/GroundVehicleController.php b/app/Http/Controllers/Web/StarCitizen/Vehicle/GroundVehicle/GroundVehicleController.php deleted file mode 100644 index 8510886e4..000000000 --- a/app/Http/Controllers/Web/StarCitizen/Vehicle/GroundVehicle/GroundVehicleController.php +++ /dev/null @@ -1,99 +0,0 @@ -middleware('auth')->except(['index']); - } - - public function index(): View - { - return view( - 'web.starcitizen.vehicles.ground_vehicles.index', - [ - 'groundVehicles' => GroundVehicle::all(), - ] - ); - } - - /** - * Display Ship data, edit Translations - * - * - * - * @throws AuthorizationException - */ - public function edit(GroundVehicle $groundVehicle): View - { - $this->authorize('web.starcitizen.vehicles.update'); - - $groundVehicle->load('components'); - - /** @var Collection $changelogs */ - $changelogs = $groundVehicle->changelogs; - - $changelogs = $changelogs->merge($groundVehicle->translationChangelogs); - - $changelogs = $changelogs->sortByDesc('created_at'); - - return view( - 'web.starcitizen.vehicles.ground_vehicles.edit', - [ - 'groundVehicle' => $groundVehicle, - 'componentGroups' => $groundVehicle->componentsByClass(), - 'changelogs' => $changelogs, - ] - ); - } - - /** - * Update the specified resource in storage. - * - * - * - * @throws AuthorizationException - */ - public function update(TranslationRequest $request, GroundVehicle $groundVehicle): RedirectResponse - { - $this->authorize('web.starcitizen.vehicles.update'); - $data = $request->validated(); - - foreach ($data as $localeCode => $translation) { - $groundVehicle->translations()->updateOrCreate( - ['locale_code' => $localeCode], - ['translation' => $translation] - ); - } - - return redirect()->route( - 'web.starcitizen.vehicles.ground-vehicles.edit', - $groundVehicle->getRouteKey() - )->withMessages( - [ - 'success' => [ - __('crud.updated', ['type' => __('Fahrzeug')]), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizen/Vehicle/Ship/ShipController.php b/app/Http/Controllers/Web/StarCitizen/Vehicle/Ship/ShipController.php deleted file mode 100644 index 82bea32bf..000000000 --- a/app/Http/Controllers/Web/StarCitizen/Vehicle/Ship/ShipController.php +++ /dev/null @@ -1,95 +0,0 @@ -middleware('auth')->except(['index']); - } - - public function index(): View - { - return view( - 'web.starcitizen.vehicles.ships.index', - [ - 'ships' => Ship::all(), - ] - ); - } - - /** - * Display Ship data, edit Translations - * - * - * - * @throws AuthorizationException - */ - public function edit(Ship $ship): View - { - $this->authorize('web.starcitizen.vehicles.update'); - - /** @var Collection $changelogs */ - $changelogs = $ship->changelogs; - - $changelogs = $changelogs->merge($ship->translationChangelogs); - - $changelogs = $changelogs->sortByDesc('created_at'); - - return view( - 'web.starcitizen.vehicles.ships.edit', - [ - 'ship' => $ship, - 'componentGroups' => $ship->componentsByClass(), - 'changelogs' => $changelogs, - ] - ); - } - - /** - * Update the specified resource in storage. - * - * - * - * @throws AuthorizationException - */ - public function update(TranslationRequest $request, Ship $ship): RedirectResponse - { - $this->authorize('web.starcitizen.vehicles.update'); - - $data = $request->validated(); - - foreach ($data as $localeCode => $translation) { - $ship->translations()->updateOrCreate( - ['locale_code' => $localeCode], - ['translation' => $translation] - ); - } - - return redirect()->route('web.starcitizen.vehicles.ships.edit', $ship->getRouteKey())->withMessages( - [ - 'success' => [ - __('crud.updated', ['type' => __('Schiff')]), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizen/Vehicle/Size/SizeController.php b/app/Http/Controllers/Web/StarCitizen/Vehicle/Size/SizeController.php deleted file mode 100644 index 25156d6b2..000000000 --- a/app/Http/Controllers/Web/StarCitizen/Vehicle/Size/SizeController.php +++ /dev/null @@ -1,97 +0,0 @@ -middleware('auth'); - } - - /** - * Display a listing of the resource. - * - * - * @throws AuthorizationException - */ - public function index(): View - { - $this->authorize('web.translations.view'); - - return view( - 'web.starcitizen.vehicles.sizes.index', - [ - 'translations' => Size::all(), - 'languages' => Language::all(), - 'editRoute' => 'web.starcitizen.vehicles.sizes.edit', - ] - ); - } - - /** - * Display the specified resource. - * - * - * - * @throws AuthorizationException - */ - public function edit(Size $size): View - { - $this->authorize('web.translations.update'); - - return view( - 'web.starcitizen.vehicles.sizes.edit', - [ - 'translation' => $size, - 'updateRoute' => 'web.starcitizen.vehicles.sizes.update', - ] - ); - } - - /** - * Update the specified resource in storage. - * - * - * - * @throws AuthorizationException - */ - public function update(TranslationRequest $request, Size $size): RedirectResponse - { - $this->authorize('web.translations.update'); - - $data = $request->validated(); - - foreach ($data as $localeCode => $translation) { - $size->translations()->updateOrCreate( - ['locale_code' => $localeCode], - ['translation' => $translation] - ); - } - - return redirect()->route('web.starcitizen.vehicles.sizes.index')->withMessages( - [ - 'success' => [ - __('crud.updated', ['type' => __('Fahrzeuggröße')]), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizen/Vehicle/Type/TypeController.php b/app/Http/Controllers/Web/StarCitizen/Vehicle/Type/TypeController.php deleted file mode 100644 index e5ac6e0bb..000000000 --- a/app/Http/Controllers/Web/StarCitizen/Vehicle/Type/TypeController.php +++ /dev/null @@ -1,97 +0,0 @@ -middleware('auth'); - } - - /** - * Display a listing of the resource. - * - * - * @throws AuthorizationException - */ - public function index(): View - { - $this->authorize('web.translations.view'); - - return view( - 'web.starcitizen.vehicles.types.index', - [ - 'translations' => Type::all(), - 'languages' => Language::all(), - 'editRoute' => 'web.starcitizen.vehicles.types.edit', - ] - ); - } - - /** - * Display the specified resource. - * - * - * - * @throws AuthorizationException - */ - public function edit(Type $type): View - { - $this->authorize('web.translations.update'); - - return view( - 'web.starcitizen.vehicles.types.edit', - [ - 'translation' => $type, - 'updateRoute' => 'web.starcitizen.vehicles.types.update', - ] - ); - } - - /** - * Update the specified resource in storage. - * - * - * - * @throws AuthorizationException - */ - public function update(TranslationRequest $request, Type $type): RedirectResponse - { - $this->authorize('web.translations.update'); - - $data = $request->validated(); - - foreach ($data as $localeCode => $translation) { - $type->translations()->updateOrCreate( - ['locale_code' => $localeCode], - ['translation' => $translation] - ); - } - - return redirect()->route('web.starcitizen.vehicles.types.index')->withMessages( - [ - 'success' => [ - __('crud.updated', ['type' => __('Fahrzeugtyp')]), - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/StarCitizenUnpacked/Item/ItemController.php b/app/Http/Controllers/Web/StarCitizenUnpacked/Item/ItemController.php deleted file mode 100644 index 01c2d71e1..000000000 --- a/app/Http/Controllers/Web/StarCitizenUnpacked/Item/ItemController.php +++ /dev/null @@ -1,31 +0,0 @@ - Item::query()->where('uuid', $item)->firstOrFail(), - ] - ); - } - - public function index(): View - { - return view('web.starcitizenunpacked.item.index')->with('apiToken', optional(Auth::user())->api_token); - } -} diff --git a/app/Http/Controllers/Web/StarCitizenUnpacked/VehicleController.php b/app/Http/Controllers/Web/StarCitizenUnpacked/VehicleController.php deleted file mode 100644 index 396936898..000000000 --- a/app/Http/Controllers/Web/StarCitizenUnpacked/VehicleController.php +++ /dev/null @@ -1,19 +0,0 @@ - Vehicle::query()->orderBy('is_ship')->get(), - ]); - } -} diff --git a/app/Http/Controllers/Web/Transcript/TranscriptController.php b/app/Http/Controllers/Web/Transcript/TranscriptController.php deleted file mode 100644 index 11ff393bb..000000000 --- a/app/Http/Controllers/Web/Transcript/TranscriptController.php +++ /dev/null @@ -1,117 +0,0 @@ -middleware('auth'); - } - - /** - * Display a listing of the resource. - * - * - * @throws AuthorizationException - */ - public function index(): View - { - $this->authorize('web.transcripts.index'); - - $transcripts = Transcript::query()->orderBy('id')->paginate(500); - - return view( - 'web.transcripts.index', - [ - 'transcripts' => $transcripts, - ] - ); - } - - /** - * Display the specified resource. - * - * - * @return Factory|View - * - * @throws AuthorizationException - */ - public function show(Transcript $transcript) - { - $this->authorize('web.transcripts.view'); - - return view( - 'web.transcripts.show', - [ - 'transcript' => $transcript, - 'prev' => $transcript->getPrevAttribute(), - 'next' => $transcript->getNextAttribute(), - ] - ); - } - - /** - * Show the form for editing the specified resource. - * - * - * - * @throws AuthorizationException - */ - public function edit(Transcript $transcript): View - { - $this->authorize(self::TRANSCRIPT_UPDATE_PERMISSION); - - return view( - 'web.transcripts.edit', - [ - 'transcript' => $transcript, - 'prev' => $transcript->getPrevAttribute(), - 'next' => $transcript->getNextAttribute(), - ] - ); - } - - /** - * Update the specified resource in storage. - * - * - * - * @throws AuthorizationException - */ - public function update(TranscriptUpdateRequest $request, Transcript $transcript): RedirectResponse - { - $this->authorize(self::TRANSCRIPT_UPDATE_PERMISSION); - - $validated = $request->validated(); - $transcript->title = trim($validated['title']); - $transcript->playlist_name = trim($validated['playlist_name']); - $transcript->save(); - - $message = __('crud.updated', ['type' => __('Transkript')]); - - return redirect()->route('web.transcripts.edit', $transcript->getRouteKey())->withMessages( - [ - 'success' => [ - $message, - ], - ] - ); - } -} diff --git a/app/Http/Controllers/Web/User/UserController.php b/app/Http/Controllers/Web/User/UserController.php deleted file mode 100644 index fda9fb50f..000000000 --- a/app/Http/Controllers/Web/User/UserController.php +++ /dev/null @@ -1,135 +0,0 @@ -middleware('auth'); - } - - /** - * View all Admins - * - * - * @throws AuthorizationException - */ - public function index(): View - { - $this->authorize('web.users.view'); - - return view( - 'web.users.index', - [ - 'users' => User::query()->withCount('changelogs')->get(), - ] - ); - } - - /** - * Edit Admin - * - * - * - * @throws AuthorizationException - */ - public function edit(User $user): View - { - $this->authorize('web.users.update'); - - return view( - 'web.users.edit', - [ - 'user' => $user, - ] - ); - } - - /** - * Update (Block/Restore) Admin - * - * - * - * @throws AuthorizationException - */ - public function update(Request $request, User $user): RedirectResponse - { - $this->authorize('web.users.update'); - - if ($request->has('block')) { - return $this->block($user); - } - - $data = $request->validate( - [ - self::API_TOKEN => "required|min:60|max:60|string|unique:users,api_token,{$user->id}", - 'language' => 'required|in:en,de', - ] - ); - - $user->update( - [ - self::API_TOKEN => $data[self::API_TOKEN], - ] - ); - - if ($request->has('no_api_throttle')) { - $user->settings()->updateOrCreate( - [ - 'user_id' => $user->id, - ], - [ - 'no_api_throttle' => true, - 'language' => $request->get('language'), - ] - ); - } - - return redirect(route('web.users.edit', $user->getRouteKey()))->withMessages( - [ - 'success' => [ - __('crud.updated', ['type' => __('Benutzer')]), - ], - ] - ); - } - - /** - * @throws AuthorizationException - */ - private function block(User $user): RedirectResponse - { - $this->authorize('web.users.delete'); - - $user->sessions()->delete(); - $user->blocked = true; - $user->save(); - - return redirect(route('web.users.edit', $user->getRouteKey()))->withMessages( - [ - 'warning' => [ - __('crud.blocked', ['type' => __('Benutzer')]), - ], - ] - ); - } -} diff --git a/app/Http/Filters/DateFilter.php b/app/Http/Filters/DateFilter.php new file mode 100644 index 000000000..a9a3d11a4 --- /dev/null +++ b/app/Http/Filters/DateFilter.php @@ -0,0 +1,57 @@ +whereYear($this->column, (int) $value); + + return; + } + + if (preg_match('/^\d{4}-\d{1,2}$/', $value) === 1) { + try { + $start = Carbon::createFromFormat('Y-m', $value)->startOfMonth(); + $end = (clone $start)->endOfMonth(); + + $query->whereBetween($this->column, [$start, $end]); + } catch (Exception $e) { + } + + return; + } + + if (preg_match('/^\d{4}-\d{1,2}-\d{1,2}$/', $value) === 1) { + try { + $date = Carbon::createFromFormat('Y-m-d', $value); + $query->whereDate($this->column, $date->toDateString()); + } catch (Exception $e) { + } + } + } +} diff --git a/app/Http/Filters/ImageTagFilter.php b/app/Http/Filters/ImageTagFilter.php deleted file mode 100644 index 23f17c0ad..000000000 --- a/app/Http/Filters/ImageTagFilter.php +++ /dev/null @@ -1,20 +0,0 @@ -whereRelation('tags', 'name', $value) - ->orWhereRelation('tags', 'name_en', $value); - } -} diff --git a/app/Http/Filters/ItemVariantsFilter.php b/app/Http/Filters/ItemVariantsFilter.php index 86f3c73f0..63fe7f84d 100644 --- a/app/Http/Filters/ItemVariantsFilter.php +++ b/app/Http/Filters/ItemVariantsFilter.php @@ -1,5 +1,7 @@ whereNull('base_id'); diff --git a/app/Http/Filters/SortByRelation.php b/app/Http/Filters/SortByRelation.php new file mode 100644 index 000000000..c1826973d --- /dev/null +++ b/app/Http/Filters/SortByRelation.php @@ -0,0 +1,14 @@ +orderByPowerJoins($property, $descending ? 'desc' : 'asc'); + } +} diff --git a/app/Http/Includes/PassthroughInclude.php b/app/Http/Includes/PassthroughInclude.php new file mode 100644 index 000000000..6a8e1c432 --- /dev/null +++ b/app/Http/Includes/PassthroughInclude.php @@ -0,0 +1,24 @@ + [ - EncryptCookies::class, - AddQueuedCookiesToResponse::class, - StartSession::class, - ShareErrorsFromSession::class, - VerifyCsrfToken::class, - SubstituteBindings::class, - CheckUserState::class, - SetPreferredLocale::class, - ], - - 'api' => [ - ForceJsonResponse::class, - 'bindings', - 'check_user_state', - ], - - 'api.v2' => [ - ForceJsonResponse::class, - 'bindings', - 'check_user_state', - FormatApiResponse::class, - ], - ]; - - /** - * The application's route middleware. - * - * These middleware may be assigned to groups or used individually. - * - * @var array - */ - protected $middlewareAliases = [ - 'auth' => Authenticate::class, - 'auth.basic' => AuthenticateWithBasicAuth::class, - 'bindings' => SubstituteBindings::class, - 'can' => Authorize::class, - 'check_user_state' => CheckUserState::class, - 'guest' => RedirectIfAuthenticated::class, - 'throttle' => ThrottleRequests::class, - ]; -} diff --git a/app/Http/Middleware/Api/Game/ResolveGameVersion.php b/app/Http/Middleware/Api/Game/ResolveGameVersion.php new file mode 100644 index 000000000..b7db0897b --- /dev/null +++ b/app/Http/Middleware/Api/Game/ResolveGameVersion.php @@ -0,0 +1,32 @@ +query('version'); + + // Resolve version once and cache in request attributes + $gameVersion = GameVersion::resolveRequestedOrDefault($versionCode); + $request->attributes->set('game_version', $gameVersion); + $request->attributes->set('game_version_code', $versionCode); + + return $next($request); + } +} diff --git a/app/Http/Middleware/CheckUserState.php b/app/Http/Middleware/CheckUserState.php deleted file mode 100644 index 285f6cb87..000000000 --- a/app/Http/Middleware/CheckUserState.php +++ /dev/null @@ -1,44 +0,0 @@ -user(); - - if ($user !== null && $user->blocked) { - app('Log')::notice( - 'Request from blacklisted User', - [ - 'user_id' => $user->id, - 'request_url' => $request->getUri(), - ] - ); - - Auth::logout(); - - abort(403, __('Benutzer ist gesperrt')); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php deleted file mode 100644 index 79827687a..000000000 --- a/app/Http/Middleware/EncryptCookies.php +++ /dev/null @@ -1,20 +0,0 @@ -headers->set('Accept', 'application/json'); - - return $next($request); - } -} diff --git a/app/Http/Middleware/FormatApiResponse.php b/app/Http/Middleware/FormatApiResponse.php deleted file mode 100644 index 844d8542c..000000000 --- a/app/Http/Middleware/FormatApiResponse.php +++ /dev/null @@ -1,28 +0,0 @@ -setEncodingOptions(JSON_PRETTY_PRINT); - } - - return $response; - } -} diff --git a/app/Http/Middleware/MigrateLimitParameter.php b/app/Http/Middleware/MigrateLimitParameter.php new file mode 100644 index 000000000..8625c25a5 --- /dev/null +++ b/app/Http/Middleware/MigrateLimitParameter.php @@ -0,0 +1,83 @@ +query; + $queryValues = $query->all(); + $page = $queryValues['page'] ?? null; + $queryString = (string) $request->server->get('QUERY_STRING', ''); + + $pageParams = []; + + if (is_numeric($page)) { + $pageNumber = max(1, (int) $page); + $pageParams['number'] = $pageNumber; + } + + if (is_array($page)) { + $pageParams = $page; + + if (isset($pageParams['number']) && is_numeric($pageParams['number'])) { + $pageParams['number'] = max(1, (int) $pageParams['number']); + } + + if (isset($pageParams['size']) && is_numeric($pageParams['size'])) { + $pageParams['size'] = max(1, (int) $pageParams['size']); + } + } + + if ($queryString !== '') { + $pageSize = $this->queryValue($queryString, 'page[size]'); + if ($pageSize !== null && is_numeric($pageSize)) { + $pageParams['size'] ??= max(1, (int) $pageSize); + } + + $pageNumber = $this->queryValue($queryString, 'page[number]'); + if ($pageNumber !== null && is_numeric($pageNumber)) { + $pageParams['number'] ??= max(1, (int) $pageNumber); + } + } + + if ($query->has('limit')) { + $limit = $query->get('limit'); + + if (is_numeric($limit)) { + $limitInt = max(1, (int) $limit); + + $pageParams['size'] ??= $limitInt; + } + + $query->remove('limit'); + } + + if (! empty($pageParams)) { + $query->set('page', $pageParams); + } + + return $next($request); + } + + private function queryValue(string $queryString, string $key): ?string + { + $pattern = '/(?:^|&)'.preg_quote($key, '/').'=([^&]*)/'; + + if (preg_match($pattern, $queryString, $matches) !== 1) { + return null; + } + + return urldecode($matches[1]); + } +} diff --git a/app/Http/Middleware/PersistSelectedGameVersion.php b/app/Http/Middleware/PersistSelectedGameVersion.php new file mode 100644 index 000000000..bbd6631a7 --- /dev/null +++ b/app/Http/Middleware/PersistSelectedGameVersion.php @@ -0,0 +1,74 @@ +hasSession()) { + $requestedCode = $this->normalizeCode($request->query('version')); + $sessionCode = $this->normalizeCode($request->session()->get('game_version_code')); + $defaultCode = null; + + if ($requestedCode !== null) { + $resolvedCode = $this->resolveVersionCode($requestedCode); + $defaultCode = $this->resolveDefaultVersionCode(); + + if ($resolvedCode !== null && $resolvedCode !== $defaultCode) { + $request->session()->put('game_version_code', $resolvedCode); + } else { + $request->session()->forget('game_version_code'); + } + } elseif ($sessionCode !== null) { + $resolvedSessionCode = $this->resolveVersionCode($sessionCode); + + if ($resolvedSessionCode === null) { + $request->session()->forget('game_version_code'); + } else { + $defaultCode = $defaultCode ?? $this->resolveDefaultVersionCode(); + + if ($resolvedSessionCode === $defaultCode) { + $request->session()->forget('game_version_code'); + } + } + } + } + + return $next($request); + } + + protected function resolveVersionCode(string $code): ?string + { + return GameVersion::query() + ->whereRaw('LOWER(code) = ?', [strtolower($code)]) + ->value('code'); + } + + protected function resolveDefaultVersionCode(): ?string + { + return GameVersion::query() + ->orderByDesc('is_default') + ->orderByDesc('released_at') + ->orderBy('code') + ->value('code'); + } + + protected function normalizeCode(mixed $code): ?string + { + if (! is_string($code)) { + return null; + } + + $code = trim($code); + + return $code === '' ? null : $code; + } +} diff --git a/app/Http/Middleware/SetPreferredLocale.php b/app/Http/Middleware/SetPreferredLocale.php deleted file mode 100644 index e4195924d..000000000 --- a/app/Http/Middleware/SetPreferredLocale.php +++ /dev/null @@ -1,33 +0,0 @@ -getPreferredLanguage(['de', 'en']); - - if (Auth::user()) { - $language = optional(Auth::user()->settings)->language ?? $language; - } - - app()->setLocale($language); - - return $next($request); - } -} diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php deleted file mode 100644 index 6da6d4a5c..000000000 --- a/app/Http/Middleware/TrimStrings.php +++ /dev/null @@ -1,20 +0,0 @@ -check()) { - return redirect()->intended(route('web.account.index')); - } - - return $next($request); - } -} diff --git a/app/Http/Requests/AbstractSearchRequest.php b/app/Http/Requests/AbstractSearchRequest.php deleted file mode 100644 index e49243a56..000000000 --- a/app/Http/Requests/AbstractSearchRequest.php +++ /dev/null @@ -1,28 +0,0 @@ - 'required|string|min:1|max:255', - ]; - } -} diff --git a/app/Http/Requests/Admin/UpdateTranslationRequest.php b/app/Http/Requests/Admin/UpdateTranslationRequest.php new file mode 100644 index 000000000..145d5a435 --- /dev/null +++ b/app/Http/Requests/Admin/UpdateTranslationRequest.php @@ -0,0 +1,43 @@ + ['required', 'array'], + 'translations.'.Language::ENGLISH => ['nullable', 'string'], + 'translations.'.Language::GERMAN => ['nullable', 'string'], + 'translations.'.Language::CHINESE => ['nullable', 'string'], + ]; + } + + /** + * Get custom error messages for validator errors. + */ + public function messages(): array + { + return [ + 'translations.required' => 'Translations data is required.', + 'translations.array' => 'Translations must be an array.', + ]; + } +} diff --git a/app/Http/Requests/Api/Game/SearchRequest.php b/app/Http/Requests/Api/Game/SearchRequest.php new file mode 100644 index 000000000..080197b0b --- /dev/null +++ b/app/Http/Requests/Api/Game/SearchRequest.php @@ -0,0 +1,45 @@ +input('query'); + + if (! is_string($query)) { + return; + } + + $this->merge([ + 'query' => trim(str_replace('_', ' ', urldecode($query))), + ]); + } + + public function authorize(): bool + { + return true; + } + + public function rules(): array + { + return [ + 'query' => ['required', 'string', 'min:1', 'max:255'], + ]; + } + + public function messages(): array + { + return [ + 'query.required' => 'A search query is required.', + 'query.string' => 'The search query must be a string.', + 'query.min' => 'The search query must be at least :min character.', + 'query.max' => 'The search query may not be greater than :max characters.', + ]; + } +} diff --git a/app/Http/Requests/Rsi/CommLink/CommLinkRequest.php b/app/Http/Requests/Rsi/CommLink/CommLinkRequest.php deleted file mode 100644 index ca4c3aed2..000000000 --- a/app/Http/Requests/Rsi/CommLink/CommLinkRequest.php +++ /dev/null @@ -1,38 +0,0 @@ -can('web.rsi.comm-links.update'); - } - - /** - * Get the validation rules that apply to the request. - */ - public function rules(): array - { - return [ - 'title' => 'required|string|min:1|max:255', - 'url' => 'nullable|string|min:15|max:255', - 'created_at' => 'required|date', - 'channel' => 'required|string|exists:comm_link_channels,id', - 'series' => 'required|string|exists:comm_link_series,id', - 'category' => 'required|string|exists:comm_link_categories,id', - //'version' => 'required|string|regex:/\d{4}\-\d{2}\-\d{2}\_\d{6}\.html/', - ]; - } -} diff --git a/app/Http/Requests/Rsi/CommLink/CommLinkSearchRequest.php b/app/Http/Requests/Rsi/CommLink/CommLinkSearchRequest.php index d8cf6bed2..4279c3530 100644 --- a/app/Http/Requests/Rsi/CommLink/CommLinkSearchRequest.php +++ b/app/Http/Requests/Rsi/CommLink/CommLinkSearchRequest.php @@ -4,9 +4,9 @@ namespace App\Http\Requests\Rsi\CommLink; -use App\Http\Requests\AbstractSearchRequest; +use App\Http\Requests\Api\Game\SearchRequest; -class CommLinkSearchRequest extends AbstractSearchRequest +class CommLinkSearchRequest extends SearchRequest { /** * Determine if the user is authorized to make this request. diff --git a/app/Http/Requests/Rsi/CommLink/Image/AddImageTagsRequest.php b/app/Http/Requests/Rsi/CommLink/Image/AddImageTagsRequest.php deleted file mode 100644 index 1f4d167f6..000000000 --- a/app/Http/Requests/Rsi/CommLink/Image/AddImageTagsRequest.php +++ /dev/null @@ -1,33 +0,0 @@ -getHighestPermissionLevel() >= UserGroup::MITARBEITER; - } - - /** - * Get the validation rules that apply to the request. - */ - public function rules(): array - { - return [ - 'tags' => [ - 'nullable', - 'array', - ], - ]; - } -} diff --git a/app/Http/Requests/Rsi/CommLink/Image/ImageSearchRequest.php b/app/Http/Requests/Rsi/CommLink/Image/ImageSearchRequest.php deleted file mode 100644 index b7c647e3c..000000000 --- a/app/Http/Requests/Rsi/CommLink/Image/ImageSearchRequest.php +++ /dev/null @@ -1,9 +0,0 @@ -getHighestPermissionLevel() >= UserGroup::USER; - } - - /** - * Get the validation rules that apply to the request. - */ - public function rules(): array - { - return [ - 'filename' => [ - 'required', - 'string', - 'min:3', - 'max:255', - ], - 'image' => [ - 'required', - 'exists:comm_link_images,id', - ], - 'description' => [ - 'required', - 'string', - 'min:10', - 'max:255', - ], - 'categories' => [ - 'required', - 'string', - 'min:3', - 'max:255', - ], - ]; - } -} diff --git a/app/Http/Requests/Rsi/CommLink/Image/NewImageTagRequest.php b/app/Http/Requests/Rsi/CommLink/Image/NewImageTagRequest.php deleted file mode 100644 index e97a92105..000000000 --- a/app/Http/Requests/Rsi/CommLink/Image/NewImageTagRequest.php +++ /dev/null @@ -1,36 +0,0 @@ -getHighestPermissionLevel() >= UserGroup::USER; - } - - /** - * Get the validation rules that apply to the request. - */ - public function rules(): array - { - return [ - 'name' => [ - 'required', - 'string', - 'min:2', - 'max:255', - 'unique:comm_link_image_tags', - ], - ]; - } -} diff --git a/app/Http/Requests/Rsi/CommLink/Image/TagUpdateRequest.php b/app/Http/Requests/Rsi/CommLink/Image/TagUpdateRequest.php deleted file mode 100644 index 8daf06131..000000000 --- a/app/Http/Requests/Rsi/CommLink/Image/TagUpdateRequest.php +++ /dev/null @@ -1,40 +0,0 @@ -|string> - */ - public function rules(): array - { - return [ - 'name' => [ - 'required', - 'string', - 'min:3', - 'max:255', - 'exists:comm_link_image_tags', - ], - 'name_en' => [ - 'required', - 'string', - 'min:3', - 'max:255', - ], - ]; - } -} diff --git a/app/Http/Requests/Rsi/CommLink/ReverseImageSearchRequest.php b/app/Http/Requests/Rsi/CommLink/ReverseImageSearchRequest.php index 2449fe6ad..371033cbf 100644 --- a/app/Http/Requests/Rsi/CommLink/ReverseImageSearchRequest.php +++ b/app/Http/Requests/Rsi/CommLink/ReverseImageSearchRequest.php @@ -5,29 +5,43 @@ namespace App\Http\Requests\Rsi\CommLink; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Http\UploadedFile; +use Symfony\Component\HttpKernel\Exception\HttpException; class ReverseImageSearchRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - * - * @return bool - */ - public function authorize() + public function authorize(): bool { return true; } - /** - * Get the validation rules that apply to the request. - * - * @return array - */ - public function rules() + public function rules(): array { return [ - 'image' => 'required|file|max:5120', // Limit to 5mb - 'similarity' => 'required|numeric|min:1|max:100', + 'image' => ['required', 'image', 'max:5120'], + 'similarity' => ['nullable', 'integer', 'min:1', 'max:100'], ]; } + + public function similarity(): int + { + return $this->integer('similarity', 75); + } + + public function imageContents(): string + { + $file = $this->file('image'); + + if (! $file instanceof UploadedFile) { + throw new HttpException(422, 'Image file is required.'); + } + + $contents = file_get_contents($file->getPathname()); + + if ($contents === false) { + throw new HttpException(422, 'Unable to read uploaded image.'); + } + + return $contents; + } } diff --git a/app/Http/Requests/Rsi/CommLink/SimilarSearchRequest.php b/app/Http/Requests/Rsi/CommLink/SimilarSearchRequest.php new file mode 100644 index 000000000..e6d066666 --- /dev/null +++ b/app/Http/Requests/Rsi/CommLink/SimilarSearchRequest.php @@ -0,0 +1,31 @@ +|string> + */ + public function rules(): array + { + return [ + 'image' => 'required|integer|exists:comm_link_images,id', + 'similarity' => 'nullable|integer|min:1|max:100', + ]; + } +} diff --git a/app/Http/Requests/SelectGameVersionRequest.php b/app/Http/Requests/SelectGameVersionRequest.php new file mode 100644 index 000000000..4e406c40f --- /dev/null +++ b/app/Http/Requests/SelectGameVersionRequest.php @@ -0,0 +1,32 @@ + ['required', 'string', 'max:255', Rule::exists('game_versions', 'code')], + 'redirect' => ['nullable', 'string', 'max:2048'], + ]; + } + + public function messages(): array + { + return [ + 'version.required' => 'Select a game version.', + 'version.exists' => 'Selected game version is invalid.', + ]; + } +} diff --git a/app/Http/Requests/StarCitizen/Galactapedia/GalactapediaSearchRequest.php b/app/Http/Requests/StarCitizen/Galactapedia/GalactapediaSearchRequest.php deleted file mode 100644 index 27e62e439..000000000 --- a/app/Http/Requests/StarCitizen/Galactapedia/GalactapediaSearchRequest.php +++ /dev/null @@ -1,9 +0,0 @@ -keyBy('locale_code'); - $rule = '|string|min:1'; - $rules = []; - - foreach ($localeCodes as $code => $language) { - if (config('language.english') === $language->locale_code) { - $rules["description_{$code}"] = 'required'.$rule; - $rules["known_for_{$code}"] = 'required'.$rule; - } else { - $rules["description_{$code}"] = 'present'.$rule; - $rules["known_for_{$code}"] = 'present'.$rule; - } - } - - return $rules; - } -} diff --git a/app/Http/Requests/StarCitizen/Starmap/StarsystemRequest.php b/app/Http/Requests/StarCitizen/Starmap/StarsystemRequest.php deleted file mode 100644 index f59d59781..000000000 --- a/app/Http/Requests/StarCitizen/Starmap/StarsystemRequest.php +++ /dev/null @@ -1,9 +0,0 @@ -keyBy('locale_code'); - $rule = '|string|min:1'; - $rules = []; - - foreach ($localeCodes as $code => $language) { - if (config('language.english') === $language->locale_code) { - $rules[$code] = 'required'.$rule; - } else { - $rules[$code] = 'present'.$rule; - } - } - - return $rules; - } -} diff --git a/app/Http/Requests/Transcript/TranscriptUpdateRequest.php b/app/Http/Requests/Transcript/TranscriptUpdateRequest.php deleted file mode 100644 index 05b2fb196..000000000 --- a/app/Http/Requests/Transcript/TranscriptUpdateRequest.php +++ /dev/null @@ -1,30 +0,0 @@ -can('web.transcripts.update'); - } - - /** - * Get the validation rules that apply to the request. - */ - public function rules(): array - { - return [ - 'title' => 'required|string|min:1|max:255', - 'playlist_name' => 'required|string|min:1|max:255', - ]; - } -} diff --git a/app/Http/Resources/AbstractBaseResource.php b/app/Http/Resources/AbstractBaseResource.php index 0df689249..55b9b8efd 100644 --- a/app/Http/Resources/AbstractBaseResource.php +++ b/app/Http/Resources/AbstractBaseResource.php @@ -1,5 +1,7 @@ additional['meta'] += $key; - } else { - $this->additional['meta'][$key] = $value; + $this->additional['meta'] = array_replace_recursive($this->additional['meta'], $key); + + return; } - } - protected function cleanType(): string - { - return str_replace('NOITEM_', '', ($this->type ?? '')); + if ( + isset($this->additional['meta'][$key]) && + is_array($this->additional['meta'][$key]) && + is_array($value) + ) { + $this->additional['meta'][$key] = array_replace_recursive($this->additional['meta'][$key], $value); + + return; + } + + $this->additional['meta'][$key] = $value; } } diff --git a/app/Http/Resources/AbstractTranslationResource.php b/app/Http/Resources/AbstractTranslationResource.php deleted file mode 100644 index a1c5a34b1..000000000 --- a/app/Http/Resources/AbstractTranslationResource.php +++ /dev/null @@ -1,86 +0,0 @@ -translationsCollection(); - - $locale = $request->get('locale'); - - if (! empty($locale)) { - return $this->getSingleTranslation($translations, $request->get('locale'), $translationKey); - } - - $translations = $translations->map( - function ($translation) use ($translationKey, $translations) { - if ($translation instanceof Language) { - return $this->getSingleTranslation($translations, Language::ENGLISH, $translationKey); - } - - return $this->getSingleTranslation($translation, $translationKey); - } - )->filter( - function ($translations) { - return ! empty($translations); - } - ); - - return $translations->isEmpty() ? null : $translations; - } - - private function getSingleTranslation($translations, string $locale, $translationKey = 'translation'): ?string - { - $translation = null; - - if ( - $translations instanceof ItemTranslation || - $translations instanceof Translation || - $translations instanceof GiverTranslation || - $translations instanceof CelestialObjectTranslation || - $translations instanceof StarsystemTranslation - ) { - return $translations[$translationKey]; - } - - if ($translations->has($locale) && ! $translations->get($locale) instanceof Language) { - $translation = $translations->get($locale)[$translationKey]; - } elseif ($translations->has(Language::ENGLISH)) { - $translation = $translations->get(Language::ENGLISH)[$translationKey]; - } - - return $translation; - } -} diff --git a/app/Http/Resources/FilterSchemas.php b/app/Http/Resources/FilterSchemas.php new file mode 100644 index 000000000..688e2fbdc --- /dev/null +++ b/app/Http/Resources/FilterSchemas.php @@ -0,0 +1,28 @@ +data, "stdItem.{$path}", $default); + } + + /** + * Extract a value from vehicle JSON payload. + * + * @param string $path Dot notation path + * @param mixed $default Default value if path not found + */ + protected function extractFromVehicleJson(VehicleData $vehicleData, string $path, mixed $default = null): mixed + { + return Arr::get($vehicleData->data, $path, $default); + } + + /** + * Normalize array handling to match v2 behavior. + * + * V2 behavior: Empty arrays are returned as empty arrays, not null. + * V3 behavior: Should match v2 - empty arrays remain empty arrays. + * Only return null if the value is actually absent from the JSON. + */ + protected function normalizeArray(mixed $value): ?array + { + // If explicitly null or not an array, return null + if ($value === null || ! is_array($value)) { + return null; + } + + // Empty arrays should remain as empty arrays to match v2 + return $value; + } + + /** + * Extract a specification from stdItem with type checking. + * Returns null if the specification doesn't exist or is empty. + * + * @param string $type Specification type (e.g., 'Shield', 'Weapon', 'PowerPlant') + */ + protected function extractSpecification(ItemData $itemData, string $type): ?array + { + $spec = Arr::get($itemData->data, "stdItem.{$type}"); + + if (! is_array($spec) || $spec === []) { + return null; + } + + return $spec; + } + + /** + * Extract entity tags from ItemData. + */ + protected function extractEntityTags(ItemData $itemData): array + { + $tags = Arr::get($itemData->data, 'entity_tags', []); + + return is_array($tags) ? $tags : []; + } + + /** + * Extract ports from stdItem. + */ + protected function extractPorts(ItemData $itemData): array + { + $ports = Arr::get($itemData->data, 'stdItem.Ports', []); + + return is_array($ports) ? $ports : []; + } + + /** + * Extract a numeric value from stdItem, ensuring it's the correct type. + */ + protected function extractNumeric(ItemData $itemData, string $path, ?float $default = null): ?float + { + $value = Arr::get($itemData->data, "stdItem.{$path}", $default); + + if ($value === null) { + return null; + } + + return is_numeric($value) ? (float) $value : $default; + } + + /** + * Extract an integer value from stdItem. + */ + protected function extractInteger(ItemData $itemData, string $path, ?int $default = null): ?int + { + $value = Arr::get($itemData->data, "stdItem.{$path}", $default); + + if ($value === null) { + return null; + } + + return is_numeric($value) ? (int) $value : $default; + } + + /** + * Extract a string value from stdItem. + */ + protected function extractString(ItemData $itemData, string $path, ?string $default = null): ?string + { + $value = Arr::get($itemData->data, "stdItem.{$path}", $default); + + if ($value === null) { + return null; + } + + return is_string($value) ? $value : $default; + } + + /** + * Extract a boolean value from stdItem. + */ + protected function extractBoolean(ItemData $itemData, string $path, ?bool $default = null): ?bool + { + $value = Arr::get($itemData->data, "stdItem.{$path}", $default); + + if ($value === null) { + return null; + } + + return is_bool($value) ? $value : $default; + } + + /** + * Extract raw XML data from ItemData. + * + * @param string $path Path within Raw structure + */ + protected function extractFromRaw(ItemData $itemData, string $path, mixed $default = null): mixed + { + return Arr::get($itemData->data, "Raw.{$path}", $default); + } + + /** + * Check if a path exists in stdItem. + */ + protected function hasInStdItem(ItemData $itemData, string $path): bool + { + return Arr::has($itemData->data, "stdItem.{$path}"); + } + + /** + * Extract an array and ensure it's properly typed, filtering out null values if needed. + * + * @param bool $filterNulls Whether to filter out null values + */ + protected function extractArray(ItemData $itemData, string $path, bool $filterNulls = false): array + { + $value = Arr::get($itemData->data, "stdItem.{$path}", []); + + if (! is_array($value)) { + return []; + } + + if ($filterNulls) { + return array_filter($value, static fn ($item) => $item !== null); + } + + return $value; + } +} diff --git a/app/Http/Resources/Game/Concerns/ResolvesGameVersion.php b/app/Http/Resources/Game/Concerns/ResolvesGameVersion.php new file mode 100644 index 000000000..266e6cbdc --- /dev/null +++ b/app/Http/Resources/Game/Concerns/ResolvesGameVersion.php @@ -0,0 +1,109 @@ +attributes->get('game_version'); + + if ($gameVersion instanceof GameVersion) { + return $gameVersion; + } + + $resolved = GameVersion::resolveRequestedOrDefault($this->gameVersionCode()); + request()->attributes->set('game_version', $resolved); + + return $resolved; + } + + /** + * Get the game version code from the request (may be null for default). + */ + protected function gameVersionCode(): ?string + { + $code = request()->attributes->get('game_version_code'); + + if ($code !== null) { + return $code; + } + + $code = request()->query('version'); + request()->attributes->set('game_version_code', $code); + + return $code; + } + + /** + * Load an Item with data for the current game version. + * + * This method ensures the item is loaded with version-specific ItemData + * matching the request's resolved game version. + * + * @param string $uuid The item UUID to load + * @return Item|null The item with loaded data, or null if not found + */ + protected function loadItemForVersion(string $uuid): ?Item + { + return Item::query() + ->where('uuid', $uuid) + ->withDataForVersion($this->gameVersionCode()) + ->first(); + } + + /** + * Load ItemData for the current game version by item UUID. + * + * @param string $uuid The item UUID to load + * @return ItemData|null The item data with required relations, or null if not found + */ + protected function loadItemDataForVersion(string $uuid): ?ItemData + { + return ItemData::query() + ->forRequestedOrDefaultVersion($this->gameVersionCode()) + ->whereHas('item', fn (Builder $query) => $query->where('uuid', $uuid)) + ->with(['item', 'manufacturer', 'gameVersion']) + ->first(); + } + + /** + * Load a Vehicle with data for the current game version. + * + * @param string $uuid The vehicle UUID to load + * @return Vehicle|null The vehicle with loaded data, or null if not found + */ + protected function loadVehicleForVersion(string $uuid): ?Vehicle + { + return Vehicle::query() + ->where('uuid', $uuid) + ->with([ + 'data' => fn (Builder $query) => $query->where('game_version_id', $this->gameVersion()->id), + ]) + ->first(); + } + + /** + * Apply version filtering to an Item query builder. + * + * Use this when you need to customize the query before execution. + */ + protected function scopeItemForVersion(Builder $query): Builder + { + return $query->withDataForVersion($this->gameVersionCode()); + } +} diff --git a/app/Http/Resources/Game/Item/ItemDescriptionDataResource.php b/app/Http/Resources/Game/Item/ItemDescriptionDataResource.php new file mode 100644 index 000000000..1911b5751 --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemDescriptionDataResource.php @@ -0,0 +1,32 @@ + $this->name, + 'value' => $this->value, + 'type' => $this->value, + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ItemDimensionResource.php b/app/Http/Resources/Game/Item/ItemDimensionResource.php new file mode 100644 index 000000000..0d05dde7b --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemDimensionResource.php @@ -0,0 +1,113 @@ + 0.5, + 'height' => 0.2, + 'length' => 1.1, + ], + nullable: true + ), + ], + type: 'object' +)] +class ItemDimensionResource extends AbstractBaseResource +{ + use ExtractsJsonData; + + public function toArray(Request $request): array + { + $dimensions = $this->extractFromStdItem($this->resource, 'InventoryOccupancy.Dimensions'); + $uiDimensions = $this->extractFromStdItem($this->resource, 'InventoryOccupancy.UIDimensions'); + + $sumDim = Arr::get($dimensions, 'Width', 0) + Arr::get($dimensions, 'Height', 0) + Arr::get($dimensions, 'Length', 0); + $sumTrueDim = Arr::get($uiDimensions, 'Width', 0) + Arr::get($uiDimensions, 'Height', 0) + Arr::get($uiDimensions, 'Length', 0); + + $dim = $uiDimensions && $sumDim !== $sumTrueDim ? $uiDimensions : $dimensions; + + return [ + 'width' => Arr::get($dim, 'Width'), + 'height' => Arr::get($dim, 'Height'), + 'length' => Arr::get($dim, 'Length'), + 'volume' => $this->extractFromStdItem($this->resource, 'InventoryOccupancy.Volume.SCU'), + 'volume_converted' => $this->extractFromStdItem($this->resource, 'InventoryOccupancy.Volume.SCUConverted'), + 'volume_converted_unit' => $this->extractFromStdItem($this->resource, 'InventoryOccupancy.Volume.Unit'), + $this->mergeWhen($uiDimensions && $sumDim !== $sumTrueDim, [ + 'true_dimension' => [ + 'width' => Arr::get($dimensions, 'Width'), + 'height' => Arr::get($dimensions, 'Height'), + 'length' => Arr::get($dimensions, 'Length'), + ], + ]), + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ItemDistortionResource.php b/app/Http/Resources/Game/Item/ItemDistortionResource.php new file mode 100644 index 000000000..3c3251426 --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemDistortionResource.php @@ -0,0 +1,47 @@ + round(Arr::get($this, 'DecayRate', 0), 2), + 'decay_delay' => Arr::get($this, 'DecayDelay'), + 'maximum' => Arr::get($this, 'Maximum'), + 'overload_ratio' => Arr::get($this, 'OverloadRatio'), + 'warning_ratio' => Arr::get($this, 'WarningRatio'), + 'recovery_ratio' => Arr::get($this, 'RecoveryRatio'), + 'recovery_time' => Arr::get($this, 'RecoveryTime'), + 'power_ratio_at_max_distortion' => Arr::get($this, 'PowerRatioAtMaxDistortion'), + 'power_change_only_at_max_distortion' => Arr::get($this, 'PowerChangeOnlyAtMaxDistortion'), + 'shutdown_time' => round(Arr::get($this, 'ShutdownTime', 0), 2), + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ItemDurabilityResource.php b/app/Http/Resources/Game/Item/ItemDurabilityResource.php new file mode 100644 index 000000000..7ba646442 --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemDurabilityResource.php @@ -0,0 +1,42 @@ + Arr::get($this, 'Health'), + 'max_lifetime' => Arr::get($this, 'Lifetime'), + 'lifetime' => Arr::get($this, 'Lifetime'), + 'repairable' => Arr::get($this, 'Repairable') === 1, + 'salvageable' => Arr::get($this, 'Salvageable') === 1, + 'resistance' => collect(Arr::get($this, 'Resistance', [])) + ->mapWithKeys(fn ($value, $key) => [ + strtolower($key) => Arr::get($value, 'Multiplier'), + ]) + ->toArray(), + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ItemEmissionResource.php b/app/Http/Resources/Game/Item/ItemEmissionResource.php new file mode 100644 index 000000000..f4c02530e --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemEmissionResource.php @@ -0,0 +1,37 @@ + Arr::get($this, 'Ir'), + 'em_min' => Arr::get($this, 'Em.Minimum'), + 'em_max' => Arr::get($this, 'Em.Maximum'), + 'em_decay' => Arr::get($this, 'Em.Decay'), + $this->mergeWhen(Arr::has($this, 'Em.PerSegment'), fn () => ['em_per_segment' => Arr::get($this, 'Em.PerSegment')]), + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ItemHeatConnectionResource.php b/app/Http/Resources/Game/Item/ItemHeatConnectionResource.php new file mode 100644 index 000000000..30f7fcce1 --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemHeatConnectionResource.php @@ -0,0 +1,75 @@ + Arr::get($this, 'TemperatureToIR'), + 'ir_temperature_threshold' => Arr::get($this, 'StartIRTemperature'), + 'overpower_heat' => Arr::get($this, 'OverpowerHeat'), + 'overclock_threshold_min' => Arr::get($this, 'OverclockThresholdMinHeat'), + 'overclock_threshold_max' => Arr::get($this, 'OverclockThresholdMaxHeat'), + 'overclock_threshold_min_heat' => Arr::get($this, 'OverclockThresholdMinHeat'), + 'overclock_threshold_max_heat' => Arr::get($this, 'OverclockThresholdMaxHeat'), + 'thermal_energy_base' => Arr::get($this, 'ThermalEnergyBase'), + 'thermal_energy_draw' => Arr::get($this, 'ThermalEnergyDraw'), + 'thermal_conductivity' => Arr::get($this, 'ThermalConductivity'), + 'specific_heat_capacity' => Arr::get($this, 'SpecificHeatCapacity'), + 'mass' => Arr::get($this, 'Mass'), + 'surface_area' => Arr::get($this, 'SurfaceArea'), + 'start_cooling_temperature' => Arr::get($this, 'StartCoolingTemperature'), + 'max_cooling_rate' => Arr::get($this, 'MaxCoolingRate'), + 'max_temperature' => Arr::get($this, 'MaxTemperature'), + 'min_temperature' => Arr::get($this, 'MinTemperature'), + 'overheat_temperature' => Arr::get($this, 'OverheatTemperature'), + 'recovery_temperature' => Arr::get($this, 'RecoveryTemperature'), + 'misfire_min_temperature' => Arr::get($this, 'MisfireMinTemperature'), + 'misfire_max_temperature' => Arr::get($this, 'MisfireMaxTemperature'), + 'ir_emission' => max( + 0, + ( + Arr::get($this, 'StartCoolingTemperature', 0) - + Arr::get($this, 'StartIRTemperature', 0) + ) * Arr::get($this, 'TemperatureToIR', 0) + ), + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ItemInventoryResource.php b/app/Http/Resources/Game/Item/ItemInventoryResource.php new file mode 100644 index 000000000..07d7ffdb8 --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemInventoryResource.php @@ -0,0 +1,64 @@ + Arr::get($this, 'UUID'), + 'width' => Arr::get($this, 'x'), + 'height' => Arr::get($this, 'z'), + 'length' => Arr::get($this, 'y'), + 'volume' => Arr::has($this, ['x', 'z', 'y']) + ? Arr::get($this, 'x') * Arr::get($this, 'z') * Arr::get($this, 'y') + : null, + 'scu' => Arr::get($this, 'SCU'), + 'scu_converted' => Arr::has($this, ['SCU', 'Unit']) + ? Arr::get($this, 'SCU') * (10 ** Arr::get($this, 'Unit')) + : null, + 'unit' => Arr::get($this, 'UnitName'), + 'open' => Arr::get($this, 'IsOpenContainer'), + 'external' => Arr::get($this, 'IsExternalContainer'), + 'closed' => Arr::get($this, 'IsClosedContainer'), + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ItemLinkResource.php b/app/Http/Resources/Game/Item/ItemLinkResource.php new file mode 100644 index 000000000..7147fc388 --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemLinkResource.php @@ -0,0 +1,73 @@ +resource; + $item = $itemData->item; + + return [ + 'uuid' => $item->uuid, + 'name' => $itemData->name, + 'class_name' => $itemData->class_name, + 'type' => $itemData->type, + 'sub_type' => $itemData->sub_type, + 'classification' => $itemData->classification, + 'is_base_variant' => $itemData->base_id === null, + 'manufacturer' => new ManufacturerLinkResource($itemData->manufacturer), + 'link' => route('items.show', ['identifier' => $item->uuid]), + $this->mergeWhen($itemData->base_id !== null && $itemData->relationLoaded('baseVariant'), fn () => [ + 'base_variant' => route('items.show', [ + 'identifier' => $itemData->baseVariant?->item?->uuid ?? '', + ]), + ]), + 'variants' => self::collection($this->whenLoaded('variants')), + 'shops' => [], + + 'updated_at' => $item->updated_at, + 'version' => $itemData->gameVersion->code, + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ItemPortResource.php b/app/Http/Resources/Game/Item/ItemPortResource.php new file mode 100644 index 000000000..140e497d0 --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemPortResource.php @@ -0,0 +1,107 @@ +routeIs('items.show')) { + $itemData = $this->loadItemDataForVersion(Arr::get($this, 'EquippedItem')); + } + + return [ + 'name' => Arr::get($this, 'PortName'), + 'display_name' => Arr::get($this, 'DisplayName'), + 'position' => Arr::get($this, 'Position', strtoupper(Arr::get($this, 'PortName', ''))), + 'size' => Arr::get($this, 'Size'), + 'sizes' => [ + 'min' => Arr::get($this, 'MinSize'), + 'max' => Arr::get($this, 'MaxSize'), + ], + 'compatible_types' => Arr::has($this, 'CompatibleTypes') && is_array(Arr::get($this, 'CompatibleTypes')) + ? ItemPortTypeResource::collection(Arr::get($this, 'CompatibleTypes')) + : null, + 'types' => Arr::get($this, 'Types', []), + 'tags' => Arr::get($this, 'Tags', []), + 'required_tags' => Arr::get($this, 'RequiredTags', []), + 'flags' => Arr::get($this, 'Flags', []), + 'uneditable' => Arr::get($this, 'Uneditable'), + 'equipped_item_uuid' => Arr::get($this, 'EquippedItem'), + $this->mergeWhen($itemData !== null, [ + 'equipped_item' => new ItemLinkResource($itemData), + ]), + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ItemPortTypeResource.php b/app/Http/Resources/Game/Item/ItemPortTypeResource.php new file mode 100644 index 000000000..982395e9b --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemPortTypeResource.php @@ -0,0 +1,35 @@ + Arr::get($this, 'Type'), + 'sub_types' => Arr::get($this, 'SubTypes', []), + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ItemPowerConnectionResource.php b/app/Http/Resources/Game/Item/ItemPowerConnectionResource.php new file mode 100644 index 000000000..a46a3e2e2 --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemPowerConnectionResource.php @@ -0,0 +1,51 @@ + Arr::get($this, 'PowerBase'), + 'power_draw' => Arr::get($this, 'PowerDraw'), + 'throttleable' => Arr::get($this, 'IsThrottleable'), + 'overclockable' => Arr::get($this, 'IsOverclockable'), + 'overclock_threshold_min' => Arr::get($this, 'OverclockThresholdMin'), + 'overclock_threshold_max' => Arr::get($this, 'OverclockThresholdMax'), + 'overpower_performance' => Arr::get($this, 'OverpowerPerformance'), + 'overclock_performance' => Arr::get($this, 'OverclockPerformance'), + 'power_to_em' => Arr::get($this, 'PowerToEM'), + 'decay_rate_em' => Arr::get($this, 'DecayRateOfEM'), + 'em_min' => Arr::get($this, 'PowerBase', 0) * Arr::get($this, 'PowerToEM', 0), + 'em_max' => Arr::get($this, 'PowerDraw', 0) * Arr::get($this, 'PowerToEM', 0), + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ItemResource.php b/app/Http/Resources/Game/Item/ItemResource.php new file mode 100644 index 000000000..af292591d --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemResource.php @@ -0,0 +1,941 @@ +uuid === null) { + return []; + } + + $this->addMetadata('deprecated_fields', [ + 'shops' => 'Shop data is not available in the source files anymore, there is currently no replacement.', + ]); + + $itemData = $this->data->first(); + + // Determine if 'related_items' has been requested via include + $includeParam = $request->query('include'); + $includeValues = []; + if (is_string($includeParam)) { + $includeValues = array_map('trim', explode(',', $includeParam)); + } elseif (is_array($includeParam)) { + $includeValues = $includeParam; + } + $includeRelated = in_array('related_items', $includeValues, true); + + $type = str_replace('NOITEM_', '', ($itemData->type ?? '')); + + return [ + 'uuid' => $this->uuid, + 'name' => $itemData->name, + 'class_name' => $itemData->class_name, + 'classification' => $itemData->classification, + 'description' => $this->getTranslation($this->resource, $request), + 'size' => $itemData->size, + 'mass' => $this->extractNumeric($itemData, 'Mass'), + 'is_base_variant' => $itemData->base_id === null, + $this->mergeWhen(str_starts_with($itemData->classification ?? '', 'Ship.'), [ + 'grade' => $this->formatGrade($itemData), + 'class' => $itemData->class, + ]), + 'description_data' => ItemDescriptionDataResource::collection($itemData->descriptionData), + 'manufacturer_description' => $itemData->getDescriptionDatum('Manufacturer'), + 'manufacturer' => new ManufacturerLinkResource($itemData->manufacturer), + 'type' => $type, + 'type_web_url' => $this->buildTypeWebUrl($type, $request), + 'sub_type' => $itemData->sub_type, + $this->mergeWhen(...$this->addAttachmentPosition($itemData)), + $this->mergeWhen($this->isTurret($itemData), $this->addTurretData($itemData)), + $this->mergeWhen(...$this->addSpecification($this->resource, $itemData)), + + 'dimension' => new ItemDimensionResource($itemData), + + $this->mergeWhen($this->hasInStdItem($itemData, 'InventoryContainer'), [ + 'inventory' => new ItemInventoryResource($this->extractFromStdItem($itemData, 'InventoryContainer')), + ]), + + 'tags' => $this->extractArray($itemData, 'Tags'), + 'required_tags' => $this->extractArray($itemData, 'RequiredTags'), + 'entity_tags' => $itemData->relationLoaded('entityTags') + ? $itemData->entityTags->pluck('uuid')->values()->toArray() + : [], + 'entity_tag_map' => $itemData->relationLoaded('entityTags') + ? $itemData->entityTags->map(fn ($tag) => [ + 'uuid' => $tag->uuid, + 'name' => $tag->name, + ])->values()->toArray() + : [], + 'interactions' => $this->extractArray($itemData, 'Interactions'), + 'ports' => ItemPortResource::collection($this->when($this->hasInStdItem($itemData, 'Ports'), $this->extractPorts($itemData))), + $this->mergeWhen($this->hasInStdItem($itemData, 'ResourceContainer'), [ + 'resource_container' => new ResourceContainerResource($this->extractFromStdItem($itemData, 'ResourceContainer')), + ]), + + $this->mergeWhen($this->hasInStdItem($itemData, 'RadiationResistance'), [ + 'radiation_resistance' => new RadiationResistanceResource($this->extractFromStdItem($itemData, 'RadiationResistance')), + ]), + $this->mergeWhen($this->hasInStdItem($itemData, 'TemperatureResistance'), [ + 'temperature_resistance' => new TemperatureResistanceResource($this->extractFromStdItem($itemData, 'TemperatureResistance')), + ]), + $this->mergeWhen($this->hasInStdItem($itemData, 'HeatConnection'), [ + 'heat' => new ItemHeatConnectionResource($this->extractFromStdItem($itemData, 'HeatConnection')), + ]), + $this->mergeWhen($this->hasInStdItem($itemData, 'Temperature'), [ + 'temperature' => new ItemTemperatureResource($this->extractFromStdItem($itemData, 'Temperature')), + ]), + $this->mergeWhen($this->hasInStdItem($itemData, 'PowerConnection'), [ + 'power' => new ItemPowerConnectionResource($this->extractFromStdItem($itemData, 'PowerConnection')), + ]), + $this->mergeWhen($this->hasInStdItem($itemData, 'Durability') && $this->extractFromStdItem($itemData, 'Durability.Health', 0) > 0, [ + 'durability' => new ItemDurabilityResource($this->extractFromStdItem($itemData, 'Durability')), + ]), + $this->mergeWhen($this->hasInStdItem($itemData, 'Distortion'), [ + 'distortion' => new ItemDistortionResource($this->extractFromStdItem($itemData, 'Distortion')), + ]), + $this->mergeWhen($this->hasInStdItem($itemData, 'ResourceNetwork'), [ + 'resource_network' => new ResourceNetworkResource($itemData), + 'emission' => new ItemEmissionResource($this->extractFromStdItem($itemData, 'Emission')), + ]), + + 'shops' => [], + $this->mergeWhen($itemData->base_id !== null && $itemData->relationLoaded('baseVariant'), [ + 'base_variant' => new ItemLinkResource($itemData->baseVariant), + ]), + 'variants' => $itemData->relationLoaded('variants') + ? ItemLinkResource::collection( + $itemData->variants + ->filter(fn ($variant) => $variant->item !== null) + ) + : [], + $this->mergeWhen($includeRelated, [ + 'related_items' => (new RelatedItemsBuilder($itemData->gameVersion->code))->build($this->resource), + ]), + 'web_url' => $this->buildWebUrl($request), + 'link' => route('items.show', ['identifier' => $this->uuid]), + 'updated_at' => $this->updated_at, + 'version' => $itemData->gameVersion->code, + ]; + } + + protected function addSpecification(Item $item, ItemData $itemData): array + { + $specifications = []; + $hasMatch = false; + + // FPS Clothing + if (str_starts_with($itemData->classification ?? '', 'FPS.Clothing.')) { + $hasMatch = true; + $specifications['clothing'] = static fn () => new ClothingResource($itemData); + + $this->addMetadata('deprecated_fields', [ + 'clothing' => [ + 'clothing_type' => 'Use type instead.', + 'temp_resistance_min' => 'Use temperature_resistance from root.', + 'temp_resistance_max' => 'Use temperature_resistance from root.', + ], + ]); + } + + // FPS Armor + if (str_starts_with($itemData->classification ?? '', 'FPS.Armor.')) { + $hasMatch = true; + $specifications['clothing'] = static fn () => new SuitArmorResource($itemData); + $specifications['suit_armor'] = static fn () => new SuitArmorResource($itemData); + } + + // Ship Armor + if (str_starts_with($itemData->classification ?? '', 'Ship.Armor.')) { + $hasMatch = true; + $specifications['armor'] = static fn () => new ArmorResource($itemData); + + $this->addMetadata('deprecated_fields', [ + 'armor' => [ + 'signal_infrared' => 'Use signal_multiplier.infrared instead.', + 'signal_electromagnetic' => 'Use signal_multiplier.electromagnetic instead.', + 'signal_cross_section' => 'Use signal_multiplier.cross_section instead.', + 'damage_physical' => 'Use damage_multiplier.physical instead.', + 'damage_energy' => 'Use damage_multiplier.energy instead.', + 'damage_distortion' => 'Use damage_multiplier.distortion instead.', + 'damage_thermal' => 'Use damage_multiplier.thermal instead.', + 'damage_biochemical' => 'Use damage_multiplier.biochemical instead.', + 'damage_stun' => 'Use damage_multiplier.stun instead.', + ], + ]); + } + + // Thrusters + if (str_starts_with($itemData->classification ?? '', 'Ship.MainThruster') || + str_starts_with($itemData->classification ?? '', 'Ship.ManneuverThruster')) { + $hasMatch = true; + $specifications['thruster'] = static fn () => new ThrusterResource($itemData); + } + + // Flight Controller + if ($itemData->type === 'FlightController') { + $hasMatch = true; + $specifications['flight_controller'] = static fn () => new FlightControllerResource($itemData); + } + + // Fuel Tanks + if (in_array($itemData->type, ['FuelTank', 'QuantumFuelTank', 'ExternalFuelTank'], true)) { + $hasMatch = true; + $specifications['fuel_tank'] = static fn () => new FuelTankResource($itemData); + } + + // Hacking Chip + if ($itemData->sub_type === 'Hacking') { + $hasMatch = true; + $specifications['hacking_chip'] = static fn () => new HackingChipResource($itemData); + } + + // Mining Laser + if ($itemData->type === 'WeaponMining') { + $hasMatch = true; + $specifications['mining_laser'] = static fn () => new MiningLaserResource($itemData); + } + + // Mining Module/Modifier + if ($itemData->type === 'MiningModifier') { + $hasMatch = true; + $specifications['mining_module'] = static fn () => new MiningModuleResource($itemData); + + $this->addMetadata('deprecated_fields', [ + 'mining_module' => 'Use mining_modifier instead.', + ]); + } + + // Mining Gadget + if ($itemData->type === 'Gadget' && $this->extractFromStdItem($itemData, 'MiningModule') !== null) { + $hasMatch = true; + $specifications['mining_gadget'] = static fn () => new MiningModuleResource($itemData); + } + + // Quantum Drive + if ($itemData->type === 'QuantumDrive') { + $hasMatch = true; + $specifications['quantum_drive'] = static fn () => new QuantumDriveResource($itemData); + } + + // Quantum Interdiction Generator + if ($itemData->type === 'QuantumInterdictionGenerator') { + $hasMatch = true; + $specifications['quantum_interdiction_generator'] = static fn () => new QuantumInterdictionGeneratorResource($itemData); + } + + // Self Destruct + if ($itemData->type === 'SelfDestruct') { + $hasMatch = true; + $specifications['self_destruct'] = static fn () => new SelfDestructResource($itemData); + } + + // Bombs + if ($itemData->type === 'Bomb') { + $hasMatch = true; + $specifications['bomb'] = static fn () => new BombResource($itemData); + + $this->addMetadata('deprecated_fields', [ + 'bomb' => [ + 'explosion_safety_distance' => 'Use explosion.safety_distance instead.', + 'explosion_radius_min' => 'Use explosion.radius_min instead.', + 'explosion_radius_max' => 'Use explosion.radius_max instead.', + 'damage' => 'Use damage_total instead.', + 'damages' => 'Use damage_map instead.', + ], + ]); + } + + // Missiles/Torpedoes + if ($itemData->type === 'Torpedo' || $itemData->type === 'Missile') { + $hasMatch = true; + $specifications['missile'] = static fn () => new MissileResource($itemData); + } + + // EMP + if ($itemData->type === 'EMP') { + $hasMatch = true; + $specifications['emp'] = static fn () => new EmpResource($itemData); + } + + // Cooler + if ($itemData->type === 'Cooler') { + $hasMatch = true; + $specifications['cooler'] = static fn () => new CoolerResource($itemData); + } + + // Turet + if ($itemData->type === 'Turret') { + $hasMatch = true; + $specifications['turret'] = static fn () => new TurretResource($itemData); + } + + // Tractor/Towing Beam + if (in_array($itemData->type, ['TractorBeam', 'TowingBeam'], true)) { + $hasMatch = true; + $specifications['tractor_beam'] = static fn () => new TractorBeamResource($itemData); + } + + // Shield + if ($itemData->type === 'Shield') { + $hasMatch = true; + $specifications['shield'] = static fn () => new ShieldResource($itemData); + } + + // Shield Controller + if ($itemData->type === 'ShieldController') { + $hasMatch = true; + $specifications['shield_controller'] = static fn () => new ShieldControllerResource($itemData); + } + + // Jump Drive + if ($itemData->type === 'JumpDrive') { + $hasMatch = true; + $specifications['jump_drive'] = static fn () => new JumpDriveResource($itemData); + } + + // Grenade (has both grenade and personal_weapon specs) + if ($itemData->type === 'WeaponPersonal' && $itemData->sub_type === 'Grenade') { + $hasMatch = true; + $specifications['grenade'] = static fn () => new GrenadeResource($itemData); + $specifications['personal_weapon'] = static fn () => new PersonalWeaponResource($itemData); + + } + + // Knife/Melee Weapon + if ($itemData->type === 'WeaponPersonal' && $itemData->sub_type === 'Knife') { + $hasMatch = true; + $specifications['melee_weapon'] = static fn () => new MeleeWeaponResource($itemData); + $specifications['knife'] = static fn () => new MeleeWeaponResource($itemData); + } + + // Personal Weapon (general - after specific grenade/knife checks) + if (($itemData->type === 'WeaponPersonal' || str_starts_with($itemData->classification ?? '', 'FPS.Weapon.')) && + ! isset($specifications['grenade']) && ! isset($specifications['melee_weapon'])) { + $hasMatch = true; + $specifications['personal_weapon'] = static fn () => new PersonalWeaponResource($itemData); + + $this->addMetadata('deprecated_fields', [ + 'personal_weapon' => [ + 'rof' => 'Use rpm instead', + 'effective_range' => 'Use range instead', + 'magazine_size' => 'Use capacity instead', + 'damage_per_shot' => 'Use damage.alpha_total instead', + ], + ]); + } + + // Salvage Modifier + if ($this->hasInStdItem($itemData, 'SalvageModifier')) { + $hasMatch = true; + $specifications['salvage_modifier'] = static fn () => new SalvageModifierResource($itemData); + } + + // Weapon Modifier + if ($this->hasInStdItem($itemData, 'WeaponModifier')) { + $hasMatch = true; + $specifications['weapon_modifier'] = static fn () => new WeaponModifierResource($itemData); + + $this->addMetadata('deprecated_fields', [ + 'weapon_modifier' => [ + 'fire_rate_multiplier' => 'use `base.fire_rate_multiplier` instead.', + 'damage_multiplier' => 'use `base.damage_multiplier` instead.', + 'damage_over_time_multiplier' => 'use `base.damage_over_time_multiplier` instead.', + 'projectile_speed_multiplier' => 'use `base.projectile_speed_multiplier` instead.', + 'ammo_cost_multiplier' => 'use `base.ammo_cost_multiplier` instead.', + 'heat_generation_multiplier' => 'use `base.heat_generation_multiplier` instead.', + 'sound_radius_multiplier' => 'use `base.sound_radius_multiplier` instead.', + 'charge_time_multiplier' => 'use `base.charge_time_multiplier` instead.', + ], + ]); + } + + // Weapon Attachment + if ($itemData->type === 'WeaponAttachment' || $this->hasInStdItem($itemData, 'WeaponAttachment')) { + $hasMatch = true; + + $attachment = (new WeaponAttachmentResource($itemData))->resolve(); + + foreach ($attachment as $key => $data) { + $specifications[$key] = static fn () => $data; + } + } + + // Food/Drink + if (in_array($itemData->type, ['Food', 'Bottle', 'Drink'], true) || $this->hasInStdItem($itemData, 'Food')) { + $hasMatch = true; + $specifications['food'] = static fn () => new FoodResource($itemData); + } + + // Medicine + if ($this->hasInStdItem($itemData, 'Medical')) { + $hasMatch = true; + $specifications['medical'] = static fn () => new MedicineResource($itemData); + } + + // Countermeasures + if ($itemData->type === 'WeaponDefensive' || str_contains($itemData->classification ?? '', 'WeaponDefensive')) { + $hasMatch = true; + $specifications['counter_measure'] = static fn () => new CounterMeasureResource($itemData); + } + + // Missile Rack + if (($itemData->type === 'MissileLauncher' && $itemData->sub_type === 'MissileRack') || + str_contains($itemData->classification ?? '', 'MissileRack')) { + $hasMatch = true; + $specifications['missile_rack'] = static fn () => new MissileRackResource($itemData); + } + + // Fuel Intake + if ($itemData->type === 'FuelIntake' || $this->hasInStdItem($itemData, 'FuelIntake')) { + $hasMatch = true; + $specifications['fuel_intake'] = static fn () => new FuelIntakeResource($itemData); + } + + // Power Plant + if ($itemData->type === 'PowerPlant' || str_contains($itemData->classification ?? '', 'PowerPlant')) { + $hasMatch = true; + $specifications['power_plant'] = static fn () => new PowerPlantResource($itemData); + } + + // Radar + if ($itemData->type === 'Radar' || str_contains($itemData->classification ?? '', 'Radar')) { + $hasMatch = true; + $specifications['radar'] = static fn () => new RadarResource($itemData); + } + + // Cargo Grid + if ($itemData->type === 'CargoGrid' || str_contains($itemData->classification ?? '', 'CargoGrid')) { + $hasMatch = true; + $specifications['cargo_grid'] = static fn () => new CargoGridResource($itemData); + } + + // Vehicle Weapon + if ($this->hasVehicleWeapon($itemData)) { + $hasMatch = true; + $specifications['vehicle_weapon'] = static fn () => new VehicleWeaponResource($itemData); + } + + if ($this->hasInStdItem($itemData, 'Seat')) { + $hasMatch = true; + $specifications['seat'] = static fn () => new SeatResource($itemData); + } + + if ($this->hasInStdItem($itemData, 'Ammunition')) { + $hasMatch = true; + $specifications['ammunition'] = static fn () => new AmmunitionResource($itemData); + + $this->addMetadata('deprecated_fields', [ + 'ammunition' => [ + 'impact_damage' => 'Use impact_damage_map instead.', + 'detonation_damage' => 'Use detonation_damage_map instead.', + ], + ]); + } + + if ($this->hasInStdItem($itemData, 'MiningModule')) { + $hasMatch = true; + $specifications['mining_modifier'] = static fn () => new MiningModifierResource($itemData); + } + + if (! $hasMatch) { + return [false, []]; + } + + return [ + true, + fn () => $this->resolveSpecifications($specifications), + ]; + } + + private function resolveSpecifications(array $specifications): array + { + return array_map( + static fn ($spec) => is_callable($spec) ? $spec() : $spec, + $specifications + ); + } + + protected function isTurret(ItemData $itemData): bool + { + return in_array($itemData->type, [ + 'Turret', + 'TurretBase', + 'UtilityTurret', + 'MissileLauncher', + 'BombLauncher', + 'WeaponMount', + ], true); + } + + protected function addTurretData(ItemData $itemData): array + { + $mountName = 'max_mounts'; + if ($itemData->type === 'MissileLauncher') { + $mountName = 'max_missiles'; + } elseif ($itemData->type === 'BombLauncher') { + $mountName = 'max_bombs'; + } + + $ports = collect($this->extractPorts($itemData)); + + return [ + $mountName => $ports->count(), + 'min_size' => $ports->min('MinSize') ?? $ports->min('min_size'), + 'max_size' => $ports->max('MaxSize') ?? $ports->max('max_size'), + ]; + } + + private function addAttachmentPosition(ItemData $itemData): array + { + if ($itemData->type !== 'WeaponAttachment' || $itemData->name === '<= PLACEHOLDER =>') { + return [false, []]; + } + + return [ + true, + fn () => [ + 'position' => match ($itemData->sub_type) { + 'Magazine' => 'Magazine Well', + 'Barrel' => 'Barrel', + 'IronSight' => 'Optic', + 'Utility' => 'Utility', + 'BottomAttachment' => 'Underbarrel', + default => $itemData->sub_type, + }, + ], + ]; + } + + private function getTranslation(Item $item, Request $request): array|string|null + { + return TranslationResolver::resolve($item, $request); + } + + private function hasVehicleWeapon(ItemData $itemData): bool + { + if ($itemData->type === 'WeaponPersonal' || str_starts_with($itemData->classification ?? '', 'FPS.Weapon.')) { + return false; + } + + return Arr::has($itemData->data, 'stdItem.Weapon'); + } + + private function buildWebUrl(Request $request): string + { + $url = route('web.items.show', ['item' => $this->uuid]); + $version = $request->query('version'); + + if ($version === null || $version === '') { + return $url; + } + + return url()->query($url, ['version' => $version]); + } + + private function buildTypeWebUrl(string $type, Request $request): ?string + { + if ($type === '') { + return null; + } + + $url = route('web.items.index', ['filter' => ['type' => $type]]); + $version = $request->query('version'); + + if ($version === null || $version === '') { + return $url; + } + + return url()->query($url, ['version' => $version]); + } + + private function formatGrade(ItemData $itemData): mixed + { + if (! str_starts_with($itemData->classification ?? '', 'Ship.')) { + return $itemData->grade; + } + + return match ($itemData->grade) { + 1 => 'A', + 2 => 'B', + 3 => 'C', + 4 => 'D', + default => $itemData->grade, + }; + } +} diff --git a/app/Http/Resources/Game/Item/ItemTemperatureResource.php b/app/Http/Resources/Game/Item/ItemTemperatureResource.php new file mode 100644 index 000000000..d7192b99b --- /dev/null +++ b/app/Http/Resources/Game/Item/ItemTemperatureResource.php @@ -0,0 +1,38 @@ + Arr::get($this, 'Calculated.Unit'), + 'cooling_threshold' => Arr::get($this, 'Calculated.CoolingThreshold'), + 'ir_threshold' => Arr::get($this, 'Calculated.IrThreshold'), + 'overheat_temperature' => Arr::get($this, 'Calculated.Overheat'), + 'max_temperature' => Arr::get($this, 'Calculated.Maximum'), + 'recovery_temperature' => Arr::get($this, 'Calculated.Recovery'), + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ResourceContainerResource.php b/app/Http/Resources/Game/Item/ResourceContainerResource.php new file mode 100644 index 000000000..249d958d7 --- /dev/null +++ b/app/Http/Resources/Game/Item/ResourceContainerResource.php @@ -0,0 +1,85 @@ + Arr::get($this, 'Mass'), + 'immutable' => Arr::get($this, 'Immutable'), + 'default_fill_fraction' => Arr::get($this, 'DefaultFillFraction'), + 'capacity' => [ + 'value' => Arr::get($capacity, 'Value'), + 'unit' => Arr::get($capacity, 'Unit'), + 'unit_name' => Arr::get($capacity, 'UnitName'), + 'scu' => Arr::get($capacity, 'SCU'), + ], + 'inclusive_resources' => Arr::get($this, 'InclusiveResources', []), + 'default_composition' => collect(Arr::get($this, 'DefaultComposition', [])) + ->map(fn (array $entry) => [ + 'entry' => Arr::get($entry, 'Entry'), + 'weight' => Arr::get($entry, 'Weight'), + ]) + ->values() + ->toArray(), + ]; + } +} diff --git a/app/Http/Resources/Game/Item/ResourceNetworkResource.php b/app/Http/Resources/Game/Item/ResourceNetworkResource.php new file mode 100644 index 000000000..6cc4b60f4 --- /dev/null +++ b/app/Http/Resources/Game/Item/ResourceNetworkResource.php @@ -0,0 +1,204 @@ +extractFromStdItem($this->resource, 'ResourceNetwork'); + + return array_filter([ + 'is_networked' => Arr::get($resourceNetwork, 'IsNetworked'), + 'is_relay' => Arr::get($resourceNetwork, 'IsRelay'), + 'default_priority' => Arr::get($resourceNetwork, 'DefaultPriority'), + 'states' => array_map(fn ($state) => [ + 'name' => Arr::get($state, 'Name'), + 'signature' => [ + 'em' => Arr::get($state, 'Signature.EM'), + 'ir' => Arr::get($state, 'Signature.IR'), + ], + 'deltas' => collect(Arr::get($state, 'Deltas', []))->map(fn ($delta) => [ + 'type' => Arr::get($delta, 'Type'), + 'resource' => Arr::get($delta, 'Resource'), + 'rate' => Arr::get($delta, 'Rate'), + 'minimum_fraction' => Arr::get($delta, 'MinimumFraction'), + 'generated_resource' => Arr::get($delta, 'GeneratedResource'), + 'generated_rate' => Arr::get($delta, 'GeneratedRate'), + 'discharge' => Arr::get($delta, 'Discharge'), + 'no_over_generation' => Arr::get($delta, 'NoOverGeneration'), + 'binary_evaluation' => Arr::get($delta, 'BinaryEvaluation'), + 'composition' => Arr::get($delta, 'Composition'), + ]), + 'power_ranges' => collect(Arr::get($state, 'PowerRanges', []))->map(fn ($range) => [ + 'start' => Arr::get($range, 'Start'), + 'modifier' => Arr::get($range, 'Modifier'), + 'register_range' => Arr::get($range, 'RegisterRange'), + ]), + ], Arr::get($resourceNetwork, 'States', [])), + 'repair' => [ + 'max_repair_count' => Arr::get($resourceNetwork, 'Repair.MaxRepairCount'), + 'time_to_repair' => Arr::get($resourceNetwork, 'Repair.TimeToRepair'), + 'health_ratio' => Arr::get($resourceNetwork, 'Repair.HealthRatio'), + ], + 'usage' => [ + 'power' => [ + 'minimum' => round(Arr::get($resourceNetwork, 'Usage.Power.Minimum', 0), 2), + 'maximum' => round(Arr::get($resourceNetwork, 'Usage.Power.Maximum', 0), 2), + ], + 'coolant' => [ + 'minimum' => Arr::get($resourceNetwork, 'Usage.Coolant.Minimum'), + 'maximum' => Arr::get($resourceNetwork, 'Usage.Coolant.Maximum'), + ], + ], + 'generation' => [ + 'coolant' => Arr::get($resourceNetwork, 'Generation.Coolant'), + 'power' => Arr::get($resourceNetwork, 'Generation.Power'), + ], + ], static fn ($value) => $value !== null && $value !== []); + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/AbstractItemSpecificationResource.php b/app/Http/Resources/Game/ItemSpecification/AbstractItemSpecificationResource.php new file mode 100644 index 000000000..08b0e7c93 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/AbstractItemSpecificationResource.php @@ -0,0 +1,89 @@ +toArray(); + } + + return []; + } + + protected function extractStdItem(array $data): array + { + $stdItem = Arr::get($data, 'stdItem', []); + + if ($stdItem === [] && Arr::has($data, 'Item.stdItem')) { + $stdItem = Arr::get($data, 'Item.stdItem', []); + } + + return is_array($stdItem) ? $stdItem : []; + } + + /** + * Build standardized damage array from damage data. + * + * Transforms damage data into the expected API format with + * physical, energy, distortion, thermal, biochemical, and stun values. + * Filters out damage types with zero values. + */ + protected function buildDamageArray(array $damageData, ?string $typePrefix = null): array + { + $damages = []; + $damageTypes = ['Physical', 'Energy', 'Distortion', 'Thermal', 'Biochemical', 'Stun']; + + foreach ($damageTypes as $type) { + $damageValue = Arr::get($damageData, $type, 0); + if ($damageValue > 0) { + $damages[] = [ + 'type' => str_starts_with($typePrefix ?? $type, 'Impact') ? 'impact' : 'detonation', + 'name' => $type, + 'damage' => $damageValue, + ]; + } + } + + return $damages; + } + + /** + * Calculate total damage from damage data array. + */ + protected function calculateTotalDamage(array $damageData): float + { + $total = 0; + $damageTypes = ['Physical', 'Energy', 'Distortion', 'Thermal', 'Biochemical', 'Stun']; + + foreach ($damageTypes as $type) { + $total += Arr::get($damageData, $type, 0); + } + + return $total; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/AmmunitionResource.php b/app/Http/Resources/Game/ItemSpecification/AmmunitionResource.php new file mode 100644 index 000000000..deab12059 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/AmmunitionResource.php @@ -0,0 +1,311 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + $ammunition = Arr::get($stdItem, 'Ammunition', []); + + $impactDamage = $this->buildDamageArray(Arr::get($ammunition, 'ImpactDamage', []), 'ImpactDamage'); + $detonationDamage = $this->buildDamageArray(Arr::get($ammunition, 'DetonationDamage', []), 'DetonationDamage'); + + $mapper = static fn ($value, $key) => [Str::snake($key) => $value]; + + $damageDropMinDistance = collect(Arr::get($ammunition, 'DamageDropMinDistance', []))->mapWithKeys($mapper); + $damageDropPerMeter = collect(Arr::get($ammunition, 'DamageDropPerMeter', []))->mapWithKeys($mapper); + $damageDropMinDamage = collect(Arr::get($ammunition, 'DamageDropMinDamage', []))->mapWithKeys($mapper); + + if ($damageDropMinDamage->isNotEmpty()) { + $damageDropMinDamage = $damageDropMinDamage->put('total', $damageDropMinDamage->sum())->toArray(); + } + + if ($damageDropPerMeter->isNotEmpty()) { + $damageDropPerMeter = $damageDropPerMeter->put('total', $damageDropPerMeter->sum())->toArray(); + } + + if ($damageDropMinDistance->isNotEmpty()) { + $damageDropMinDistance = $damageDropMinDistance->put('total', $damageDropMinDistance->sum())->toArray(); + } + + $penetration = Arr::get($ammunition, 'Penetration'); + + $bulletImpulseFalloff = [ + 'min_distance' => Arr::get($ammunition, 'BulletImpulseFalloff.MinDistance'), + 'drop_falloff' => Arr::get($ammunition, 'BulletImpulseFalloff.DropFalloff'), + 'max_falloff' => Arr::get($ammunition, 'BulletImpulseFalloff.MaxFalloff'), + ]; + + $damageFalloffs = [ + 'min_distance' => $damageDropMinDistance, + 'per_meter' => $damageDropPerMeter, + 'min_damage' => $damageDropMinDamage, + ]; + + return [ + 'uuid' => Arr::get($ammunition, 'UUID'), + 'size' => Arr::get($ammunition, 'Size'), + 'lifetime' => Arr::get($ammunition, 'Lifetime'), + 'speed' => Arr::get($ammunition, 'Speed'), + 'range' => Arr::get($ammunition, 'Range'), + + 'capacity' => Arr::get($ammunition, 'Capacity'), + 'initial_capacity' => Arr::get($ammunition, 'InitialCapacity'), + + 'damage_falloff_level_1' => Arr::get($ammunition, 'DamageFalloffLevel1'), + 'damage_falloff_level_2' => Arr::get($ammunition, 'DamageFalloffLevel2'), + 'damage_falloff_level_3' => Arr::get($ammunition, 'DamageFalloffLevel3'), + 'max_penetration_thickness' => Arr::get($ammunition, 'MaxPenetrationThickness'), + + 'penetration' => is_array($penetration) ? [ + 'base_distance' => Arr::get($penetration, 'BasePenetrationDistance'), + 'near_radius' => Arr::get($penetration, 'NearRadius'), + 'far_radius' => Arr::get($penetration, 'FarRadius'), + 'angle' => Arr::get($penetration, 'Angle'), + ] : null, + + $this->mergeWhen(! empty($impactDamage), [ + 'impact_damage' => $impactDamage, + 'impact_damage_map' => collect($impactDamage)->mapWithKeys(static fn ($entry) => [Str::snake($entry['name']) => $entry['damage']])->toArray(), + ]), + + $this->mergeWhen(! empty($detonationDamage), [ + 'detonation_damage' => $detonationDamage, + 'detonation_damage_map' => collect($detonationDamage)->mapWithKeys(static fn ($entry) => [Str::snake($entry['name']) => $entry['damage']])->toArray(), + ]), + + $this->mergeWhen(Arr::get($ammunition, 'ExplosionRadius') !== null, [ + 'explosion_radius' => [ + 'min' => Arr::get($ammunition, 'ExplosionRadius.Minimum'), + 'max' => Arr::get($ammunition, 'ExplosionRadius.Maximum'), + ], + ]), + + $this->mergeWhen(! empty($damageDropMinDistance), [ + 'damage_drop_min_distance' => $damageDropMinDistance, + ]), + $this->mergeWhen(! empty($damageDropPerMeter), [ + 'damage_drop_per_meter' => $damageDropPerMeter, + ]), + $this->mergeWhen(! empty($damageDropMinDamage), [ + 'damage_drop_min_damage' => $damageDropMinDamage, + ]), + + $this->mergeWhen(collect($bulletImpulseFalloff)->filter()->isNotEmpty(), [ + 'bullet_impulse_falloff' => $bulletImpulseFalloff, + ]), + + $this->mergeWhen(Arr::get($ammunition, 'BulletElectron') !== null, [ + 'bullet_electron' => [ + 'jump_range' => Arr::get($ammunition, 'BulletElectron.JumpRange'), + 'maximum_jumps' => Arr::get($ammunition, 'BulletElectron.MaximumJumps'), + 'residual_charge_multiplier' => Arr::get($ammunition, 'BulletElectron.ResidualChargeMultiplier'), + ], + ]), + + 'impulse_scale' => Arr::get($ammunition, 'ImpulseScale'), + 'bullet_type' => Arr::get($ammunition, 'BulletType'), + + $this->mergeWhen(collect($damageFalloffs)->filter()->isNotEmpty(), [ + 'damage_falloffs' => $damageFalloffs, + ]), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/ArmorResource.php b/app/Http/Resources/Game/ItemSpecification/ArmorResource.php new file mode 100644 index 000000000..848474f12 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/ArmorResource.php @@ -0,0 +1,232 @@ + 1.0 increase signature). Damage multipliers determine resistance to damage types (lower values = more resistant).', + properties: [ + new OA\Property( + property: 'health', + description: 'Armor health points from Durability system.', + type: 'double', + nullable: true + ), + new OA\Property( + property: 'signal_infrared', + description: 'Deprecated: Use signal_multiplier.infrared instead. Infrared signature multiplier. Lower values make the ship harder to detect via heat signature.', + type: 'double', + example: 1.0, + nullable: true, + deprecated: true + ), + new OA\Property( + property: 'signal_electromagnetic', + description: 'Deprecated: Use signal_multiplier.electromagnetic instead. Electromagnetic signature multiplier. Lower values provide better EM stealth.', + type: 'double', + example: 1.0, + nullable: true, + deprecated: true + ), + new OA\Property( + property: 'signal_cross_section', + description: 'Deprecated: Use signal_multiplier.cross_section instead. Radar cross-section multiplier. Affects radar detectability.', + type: 'double', + example: 1.0, + nullable: true, + deprecated: true + ), + new OA\Property( + property: 'damage_physical', + description: 'Deprecated: Use damage_multiplier.physical instead. Physical damage multiplier. Typically 0.62, providing 38% damage reduction.', + type: 'double', + example: 0.62, + nullable: true, + deprecated: true + ), + new OA\Property( + property: 'damage_energy', + description: 'Deprecated: Use damage_multiplier.energy instead. Energy weapon damage multiplier. Values around 1.0 are neutral.', + type: 'double', + example: 1.0, + nullable: true, + deprecated: true + ), + new OA\Property( + property: 'damage_distortion', + description: 'Deprecated: Use damage_multiplier.distortion instead. Distortion damage multiplier. Typically around 1.0.', + type: 'double', + example: 1.0, + nullable: true, + deprecated: true + ), + new OA\Property( + property: 'damage_thermal', + description: 'Deprecated: Use damage_multiplier.thermal instead. Thermal damage multiplier. Typically 1.0 (neutral, no resistance).', + type: 'double', + example: 1.0, + nullable: true, + deprecated: true + ), + new OA\Property( + property: 'damage_biochemical', + description: 'Deprecated: Use damage_multiplier.biochemical instead. Biochemical damage multiplier. Typically 1.0 (neutral, no resistance).', + type: 'double', + example: 1.0, + nullable: true, + deprecated: true + ), + new OA\Property( + property: 'damage_stun', + description: 'Deprecated: Use damage_multiplier.stun instead. Stun damage multiplier. Typically 0 (complete immunity to stun).', + type: 'double', + example: 0, + nullable: true, + deprecated: true + ), + new OA\Property( + property: 'signal_multiplier', + description: 'Grouped signal multipliers affecting detectability. Replacement for individual signal_* fields.', + properties: [ + new OA\Property(property: 'cross_section', description: 'Radar cross-section multiplier.', type: 'double', nullable: true), + new OA\Property(property: 'cross_section_change', description: 'Cross-section change from baseline (multiplier - 1). Negative values indicate reduction.', type: 'double', nullable: true), + new OA\Property(property: 'infrared', description: 'Infrared signature multiplier. Lower values reduce heat detectability.', type: 'double', nullable: true), + new OA\Property(property: 'infrared_change', description: 'Infrared change from baseline (multiplier - 1).', type: 'double', nullable: true), + new OA\Property(property: 'electromagnetic', description: 'Electromagnetic signature multiplier. Lower values improve EM stealth.', type: 'double', nullable: true), + new OA\Property(property: 'electromagnetic_change', description: 'EM change from baseline (multiplier - 1).', type: 'double', nullable: true), + ], + type: 'object', + nullable: true + ), + new OA\Property( + property: 'damage_multiplier', + description: 'Grouped damage multipliers determining armor resistance. Lower values = more resistant. Replacement for individual damage_* fields.', + properties: [ + new OA\Property(property: 'physical', description: 'Physical damage multiplier. Typical value 0.62 (38% reduction).', type: 'double', nullable: true), + new OA\Property(property: 'physical_change', description: 'Physical resistance change from neutral (multiplier - 1). Negative = more resistant.', type: 'double', nullable: true), + new OA\Property(property: 'energy', description: 'Energy damage multiplier.', type: 'double', nullable: true), + new OA\Property(property: 'energy_change', description: 'Energy resistance change from neutral.', type: 'double', nullable: true), + new OA\Property(property: 'distortion', description: 'Distortion damage multiplier.', type: 'double', nullable: true), + new OA\Property(property: 'distortion_change', description: 'Distortion resistance change from neutral.', type: 'double', nullable: true), + new OA\Property(property: 'thermal', description: 'Thermal damage multiplier.', type: 'double', nullable: true), + new OA\Property(property: 'thermal_change', description: 'Thermal resistance change from neutral.', type: 'double', nullable: true), + new OA\Property(property: 'biochemical', description: 'Biochemical damage multiplier.', type: 'double', nullable: true), + new OA\Property(property: 'biochemical_change', description: 'Biochemical resistance change from neutral.', type: 'double', nullable: true), + new OA\Property(property: 'stun', description: 'Stun damage multiplier. 0 = complete immunity.', type: 'double', nullable: true), + new OA\Property(property: 'stun_change', description: 'Stun resistance change from neutral.', type: 'double', nullable: true), + ], + type: 'object', + nullable: true + ), + new OA\Property( + property: 'resistance_multiplier', + description: 'Durability-based resistance multipliers from stdItem.Durability.Resistance system.', + properties: [ + new OA\Property(property: 'physical', description: 'Physical resistance multiplier from Durability.', type: 'double', nullable: true), + new OA\Property(property: 'energy', description: 'Energy resistance multiplier from Durability.', type: 'double', nullable: true), + new OA\Property(property: 'distortion', description: 'Distortion resistance multiplier from Durability.', type: 'double', nullable: true), + new OA\Property(property: 'thermal', description: 'Thermal resistance multiplier from Durability.', type: 'double', nullable: true), + new OA\Property(property: 'biochemical', description: 'Biochemical resistance multiplier from Durability.', type: 'double', nullable: true), + new OA\Property(property: 'stun', description: 'Stun resistance multiplier from Durability.', type: 'double', nullable: true), + ], + type: 'object', + nullable: true + ), + new OA\Property( + property: 'penetration_resistance', + description: 'Penetration resistance values determining how well armor resists armor-piercing rounds.', + properties: [ + new OA\Property(property: 'base', description: 'Base penetration resistance value.', type: 'double', nullable: true), + new OA\Property(property: 'physical', description: 'Physical penetration resistance.', type: 'double', nullable: true), + new OA\Property(property: 'energy', description: 'Energy penetration resistance.', type: 'double', nullable: true), + new OA\Property(property: 'distortion', description: 'Distortion penetration resistance.', type: 'double', nullable: true), + new OA\Property(property: 'thermal', description: 'Thermal penetration resistance.', type: 'double', nullable: true), + new OA\Property(property: 'biochemical', description: 'Biochemical penetration resistance.', type: 'double', nullable: true), + new OA\Property(property: 'stun', description: 'Stun penetration resistance.', type: 'double', nullable: true), + ], + type: 'object', + nullable: true + ), + ], + type: 'object' +)] +class ArmorResource extends AbstractItemSpecificationResource +{ + public function toArray(Request $request): array + { + $data = $this->parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $stdItem = $this->extractStdItem($data); + $armor = Arr::get($stdItem, 'Armor', []); + $signalMultipliers = Arr::get($armor, 'SignalMultipliers', []); + $damageMultipliers = Arr::get($armor, 'DamageMultipliers', []); + + return [ + 'health' => Arr::get($stdItem, 'Durability.Health'), + + 'signal_infrared' => Arr::get($signalMultipliers, 'Infrared'), + 'signal_electromagnetic' => Arr::get($signalMultipliers, 'Electromagnetic'), + 'signal_cross_section' => Arr::get($signalMultipliers, 'CrossSection'), + 'damage_physical' => Arr::get($damageMultipliers, 'Physical'), + 'damage_energy' => Arr::get($damageMultipliers, 'Energy'), + 'damage_distortion' => Arr::get($damageMultipliers, 'Distortion'), + 'damage_thermal' => Arr::get($damageMultipliers, 'Thermal'), + 'damage_biochemical' => Arr::get($damageMultipliers, 'Biochemical'), + 'damage_stun' => Arr::get($damageMultipliers, 'Stun'), + + 'signal_multiplier' => [ + 'cross_section' => Arr::get($armor, 'SignalMultipliers.CrossSection'), + 'cross_section_change' => round(Arr::get($armor, 'SignalMultipliers.CrossSection') - 1, 2), + + 'infrared' => Arr::get($armor, 'SignalMultipliers.Infrared'), + 'infrared_change' => round(Arr::get($armor, 'SignalMultipliers.Infrared') - 1, 2), + + 'electromagnetic' => Arr::get($armor, 'SignalMultipliers.Electromagnetic'), + 'electromagnetic_change' => round(Arr::get($armor, 'SignalMultipliers.Electromagnetic') - 1, 2), + ], + 'damage_multiplier' => [ + 'physical' => Arr::get($armor, 'DamageMultipliers.Physical'), + 'physical_change' => round(Arr::get($armor, 'DamageMultipliers.Physical') - 1, 2), + + 'energy' => Arr::get($armor, 'DamageMultipliers.Energy'), + 'energy_change' => round(Arr::get($armor, 'DamageMultipliers.Energy') - 1, 2), + + 'distortion' => Arr::get($armor, 'DamageMultipliers.Distortion'), + 'distortion_change' => round(Arr::get($armor, 'DamageMultipliers.Distortion') - 1, 2), + + 'thermal' => Arr::get($armor, 'DamageMultipliers.Thermal'), + 'thermal_change' => round(Arr::get($armor, 'DamageMultipliers.Thermal') - 1, 2), + + 'biochemical' => Arr::get($armor, 'DamageMultipliers.Biochemical'), + 'biochemical_change' => round(Arr::get($armor, 'DamageMultipliers.Biochemical') - 1, 2), + + 'stun' => Arr::get($armor, 'DamageMultipliers.Stun'), + 'stun_change' => round(Arr::get($armor, 'DamageMultipliers.Stun') - 1, 2), + ], + 'resistance_multiplier' => [ + 'physical' => Arr::get($stdItem, 'Durability.Resistance.Physical.Multiplier'), + 'energy' => Arr::get($stdItem, 'Durability.Resistance.Energy.Multiplier'), + 'distortion' => Arr::get($stdItem, 'Durability.Resistance.Distortion.Multiplier'), + 'thermal' => Arr::get($stdItem, 'Durability.Resistance.Thermal.Multiplier'), + 'biochemical' => Arr::get($stdItem, 'Durability.Resistance.Biochemical.Multiplier'), + 'stun' => Arr::get($stdItem, 'Durability.Resistance.Stun.Multiplier'), + ], + 'penetration_resistance' => [ + 'base' => Arr::get($armor, 'PenetrationResistance.Base'), + 'physical' => Arr::get($armor, 'PenetrationResistance.Physical'), + 'energy' => Arr::get($armor, 'PenetrationResistance.Energy'), + 'distortion' => Arr::get($armor, 'PenetrationResistance.Distortion'), + 'thermal' => Arr::get($armor, 'PenetrationResistance.Thermal'), + 'biochemical' => Arr::get($armor, 'PenetrationResistance.Biochemical'), + 'stun' => Arr::get($armor, 'PenetrationResistance.Stun'), + ], + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/BombResource.php b/app/Http/Resources/Game/ItemSpecification/BombResource.php new file mode 100644 index 000000000..f59b3318c --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/BombResource.php @@ -0,0 +1,184 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $bomb = Arr::get($data, 'stdItem.Bomb', []); + $damageData = Arr::get($bomb, 'Damage', []); + + $damages = $this->buildDamageArray($damageData); + $totalDamage = $this->calculateTotalDamage($damageData); + $damageMap = array_filter([ + 'physical' => Arr::get($damageData, 'Physical'), + 'energy' => Arr::get($damageData, 'Energy'), + 'distortion' => Arr::get($damageData, 'Distortion'), + 'thermal' => Arr::get($damageData, 'Thermal'), + 'biochemical' => Arr::get($damageData, 'Biochemical'), + 'stun' => Arr::get($damageData, 'Stun'), + ], static fn ($value) => $value !== null); + + return [ + 'arm_time' => Arr::get($bomb, 'ArmTime'), + 'ignite_time' => Arr::get($bomb, 'IgniteTime'), + 'collision_delay_time' => Arr::get($bomb, 'CollisionDelayTime'), + 'explosion_safety_distance' => Arr::get($bomb, 'ExplosionSafetyDistance'), + 'explosion_radius_min' => Arr::get($bomb, 'ExplosionMinRadius'), + 'explosion_radius_max' => Arr::get($bomb, 'ExplosionMaxRadius'), + 'maximum_drop_angle' => Arr::get($bomb, 'MaximumDropAngleFromFlatFlight'), + + 'explosion' => [ + 'requires_launcher' => Arr::get($bomb, 'RequiresLauncher'), + + 'radius_min' => Arr::get($bomb, 'ExplosionMinRadius'), + 'radius_max' => Arr::get($bomb, 'ExplosionMaxRadius'), + + 'safety_distance' => Arr::get($bomb, 'ExplosionSafetyDistance'), + 'proximity' => Arr::get($bomb, 'ProjectileProximity'), + ], + + 'delays' => [ + 'arm_time' => Arr::get($bomb, 'ArmTime'), + 'ignite_time' => Arr::get($bomb, 'IgniteTime'), + 'collision_delay_time' => Arr::get($bomb, 'CollisionDelayTime'), + ], + + 'damage' => $totalDamage > 0 ? $totalDamage : null, // V2 compatibility + 'damage_total' => $totalDamage > 0 ? $totalDamage : null, + 'damages' => WeaponDamageResource::collection($damages), + 'damage_map' => $damageMap === [] ? null : $damageMap, + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/CargoGridResource.php b/app/Http/Resources/Game/ItemSpecification/CargoGridResource.php new file mode 100644 index 000000000..72ad2c5cf --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/CargoGridResource.php @@ -0,0 +1,37 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + $container = Arr::get($stdItem, 'InventoryContainer', []); + $grid = Arr::get($stdItem, 'CargoGrid', []); + + return array_filter([ + 'x' => Arr::get($grid, 'Dimensions.X', Arr::get($container, 'x')), + 'y' => Arr::get($grid, 'Dimensions.Y', Arr::get($container, 'y')), + 'z' => Arr::get($grid, 'Dimensions.Z', Arr::get($container, 'z')), + ], static fn ($value) => $value !== null); + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/ClothingResource.php b/app/Http/Resources/Game/ItemSpecification/ClothingResource.php new file mode 100644 index 000000000..5ab46e718 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/ClothingResource.php @@ -0,0 +1,172 @@ + 0, + 'radiation_dissipation_rate' => 0, + ], + nullable: true + ), + ], + type: 'object' +)] +class ClothingResource extends AbstractBaseResource +{ + public function toArray(Request $request): array + { + $resource = $this->resource; + $classification = Arr::get($resource, 'classification', $this->resource->classification ?? null); + $slot = $this->deriveSlot($classification); + $type = $this->getType( + Arr::get($resource, 'type', ''), + Arr::get($resource, 'name', '') + ); + $damageResistances = Arr::get($resource, 'data.stdItem.DamageResistances', Arr::get($resource, 'data.damageResistances', [])); + + return [ + 'clothing_type' => $type, + 'slot' => $slot, + 'type' => $type, + 'resistances' => $this->mapLegacyResistances($damageResistances), + 'temp_resistance_min' => Arr::get($resource, 'data.stdItem.TemperatureResistance.Minimum'), + 'temp_resistance_max' => Arr::get($resource, 'data.stdItem.TemperatureResistance.Maximum'), + 'radiation_resistance' => Arr::has($resource, 'data.stdItem.RadiationResistance') + ? (new RadiationResistanceResource(Arr::get($resource, 'data.stdItem.RadiationResistance')))->toArray($request) + : null, + ]; + } + + private function getType(string $type, string $name): string + { + return match (true) { + str_contains($name, 'T-Shirt'), str_contains($name, 'Shirt') !== false => 'T-Shirt', + str_contains($name, 'Jacket') !== false => 'Jacket', + str_contains($name, 'Gloves') !== false => 'Gloves', + str_contains($name, 'Pants') !== false => 'Pants', + str_contains($name, 'Bandana') !== false => 'Bandana', + str_contains($name, 'Beanie') !== false => 'Beanie', + str_contains($name, 'Boots') !== false => 'Boots', + str_contains($name, 'Sweater') !== false => 'Sweater', + str_contains($name, 'Hat') !== false => 'Hat', + str_contains($name, 'Shoes') !== false => 'Shoes', + str_contains($name, 'Head Cover') !== false => 'Head Cover', + str_contains($name, 'Gown') !== false => 'Gown', + str_contains($name, 'Slippers') !== false => 'Slippers', + default => match (true) { + str_contains($type, 'Backpack') !== false => 'Backpack', + str_contains($type, 'Feet') !== false => 'Shoes', + str_contains($type, 'Hands') !== false => 'Gloves', + str_contains($type, 'Hat') !== false => 'Hat', + str_contains($type, 'Legs') !== false => 'Pants', + str_contains($type, 'Torso_0') !== false => 'Shirt', + str_contains($type, 'Torso_1') !== false => 'Jacket', + default => 'Unknown Type', + }, + }; + + } + + private function deriveSlot(?string $classification): ?string + { + if ($classification === null) { + return null; + } + + $parts = explode('.', $classification); + + return $parts !== [] ? Arr::last($parts) : null; + } + + private function mapLegacyResistances(array $damageResistances): ?array + { + return collect([ + ['type' => 'physical', ...$this->mapDamageResistance($damageResistances, 'Physical')], + ['type' => 'energy', ...$this->mapDamageResistance($damageResistances, 'Energy')], + ['type' => 'distortion', ...$this->mapDamageResistance($damageResistances, 'Distortion')], + ['type' => 'thermal', ...$this->mapDamageResistance($damageResistances, 'Thermal')], + ['type' => 'biochemical', ...$this->mapDamageResistance($damageResistances, 'Biochemical')], + ['type' => 'stun', ...$this->mapDamageResistance($damageResistances, 'Stun')], + ])->filter(fn (array $entry) => isset($entry['multiplier']))->toArray(); + } + + private function mapDamageResistance(array $damageResistances, string $key): ?array + { + $entry = Arr::get($damageResistances, $key); + + if (! is_array($entry)) { + return []; + } + + return [ + 'multiplier' => Arr::get($entry, 'Multiplier'), + 'threshold' => Arr::get($entry, 'Threshold'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/CoolerResource.php b/app/Http/Resources/Game/ItemSpecification/CoolerResource.php new file mode 100644 index 000000000..914567841 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/CoolerResource.php @@ -0,0 +1,54 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $cooler = Arr::get($data, 'stdItem.Cooler', []); + + return [ + 'cooling_rate' => Arr::get($cooler, 'CoolingRate'), + 'suppression_ir_factor' => Arr::get($cooler, 'SuppressionIRFactor'), + 'suppression_heat_factor' => Arr::get($cooler, 'SuppressionHeatFactor'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/CounterMeasureResource.php b/app/Http/Resources/Game/ItemSpecification/CounterMeasureResource.php new file mode 100644 index 000000000..87b50ad91 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/CounterMeasureResource.php @@ -0,0 +1,54 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + + return [ + 'type' => Arr::get($stdItem, 'WeaponDefensive.Type'), + 'signature' => [ + 'infrared' => Arr::get($stdItem, 'WeaponDefensive.Signatures.Infrared.End'), + 'cross_section' => Arr::get($stdItem, 'WeaponDefensive.Signatures.CrossSection.End'), + 'electromagnetic' => Arr::get($stdItem, 'WeaponDefensive.Signatures.Electromagnetic.End'), + 'decibel' => Arr::get($stdItem, 'WeaponDefensive.Signatures.Decibel.End'), + ], + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/EmpResource.php b/app/Http/Resources/Game/ItemSpecification/EmpResource.php new file mode 100644 index 000000000..e9de90872 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/EmpResource.php @@ -0,0 +1,83 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $emp = Arr::get($data, 'stdItem.Emp', []); + + $chargeDuration = Arr::get($emp, 'ChargeTime'); + $unleashDuration = Arr::get($emp, 'UnleashTime'); + $cooldownDuration = Arr::get($emp, 'CooldownTime'); + + return [ + 'distortion_damage' => Arr::get($emp, 'DistortionDamage'), + 'emp_radius' => Arr::get($emp, 'EmpRadius'), + 'min_emp_radius' => Arr::get($emp, 'MinEmpRadius'), + + 'charge_duration' => $chargeDuration, + 'unleash_duration' => $unleashDuration, + 'cooldown_duration' => $cooldownDuration, + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/FlightControllerResource.php b/app/Http/Resources/Game/ItemSpecification/FlightControllerResource.php new file mode 100644 index 000000000..1efb06b4c --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/FlightControllerResource.php @@ -0,0 +1,312 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $ifcs = Arr::get($data, 'stdItem.Ifcs', []); + $afterburner = Arr::get($ifcs, 'Afterburner', []); + $flightController = Arr::get($data, 'stdItem.FlightController', []); + + return [ + 'scm_speed' => Arr::get($ifcs, 'ScmSpeed'), + 'boost_speed_forward' => Arr::get($ifcs, 'BoostSpeedForward'), + 'boost_speed_backward' => Arr::get($ifcs, 'BoostSpeedBackward'), + 'max_speed' => Arr::get($ifcs, 'MaxSpeed'), + + 'pitch' => Arr::get($ifcs, 'Pitch'), + 'yaw' => Arr::get($ifcs, 'Yaw'), + 'roll' => Arr::get($ifcs, 'Roll'), + + 'pitch_boosted' => round(Arr::get($ifcs, 'Pitch') * Arr::get($afterburner, 'AngularMultiplier.Pitch', 1)), + 'yaw_boosted' => round(Arr::get($ifcs, 'Yaw') * Arr::get($afterburner, 'AngularMultiplier.Yaw', 1)), + 'roll_boosted' => round(Arr::get($ifcs, 'Roll') * Arr::get($afterburner, 'AngularMultiplier.Roll', 1)), + + 'boost_capacitor' => [ + 'capacity' => Arr::get($afterburner, 'CapacitorMax'), + 'threshold_ratio' => Arr::get($afterburner, 'AfterburnerCapacitorThresholdRatio'), + 'idle_cost' => Arr::get($afterburner, 'CapacitorAfterburnerIdleCost'), + 'linear_cost' => Arr::get($afterburner, 'CapacitorAfterburnerLinearCost'), + 'angular_cost' => Arr::get($afterburner, 'CapacitorAfterburnerAngularCost'), + 'regen_per_sec' => Arr::get($afterburner, 'CapacitorRegenPerSec'), + 'regen_delay' => Arr::get($afterburner, 'CapacitorRegenDelayAfterUse'), + 'regen_time' => Arr::get($afterburner, 'RegenTime'), + ], + + 'boost_activation' => [ + 'pre_delay_time' => Arr::get($afterburner, 'AfterburnerPreDelayTime'), + 'ramp_up_time' => Arr::get($afterburner, 'AfterburnerRampUpTime'), + 'ramp_down_time' => Arr::get($afterburner, 'AfterburnerRampDownTime'), + ], + + 'thruster_decay' => [ + 'linear_accel' => Arr::get($ifcs, 'LinearAccelDecay'), + 'angular_accel' => Arr::get($ifcs, 'AngularAccelDecay'), + ], + + 'multiplier' => [ + 'torque_imbalance' => Arr::get($ifcs, 'TorqueImbalanceMultiplier'), + 'lift' => Arr::get($ifcs, 'LiftMultiplier'), + 'drag' => Arr::get($ifcs, 'DragMultiplier'), + 'scm_max_drag' => Arr::get($ifcs, 'ScmMaxDragMultiplier'), + 'precision_landing' => Arr::get($ifcs, 'PrecisionLandingMultiplier'), + ], + + 'boost_multiplier' => [ + 'accel_x' => [ + 'positive' => Arr::get($afterburner, 'AccelerationMultiplierPositive.x'), + 'negative' => Arr::get($afterburner, 'AccelerationMultiplierNegative.x'), + ], + 'accel_y' => [ + 'positive' => Arr::get($afterburner, 'AccelerationMultiplierPositive.y'), + 'negative' => Arr::get($afterburner, 'AccelerationMultiplierNegative.y'), + ], + 'accel_z' => [ + 'positive' => Arr::get($afterburner, 'AccelerationMultiplierPositive.z'), + 'negative' => Arr::get($afterburner, 'AccelerationMultiplierNegative.z'), + ], + 'pitch' => Arr::get($afterburner, 'AngularMultiplier.Pitch'), + 'yaw' => Arr::get($afterburner, 'AngularMultiplier.Yaw'), + 'roll' => Arr::get($afterburner, 'AngularMultiplier.Roll'), + + 'pitch_accel' => Arr::get($afterburner, 'AngularAccelerationMultiplier.Pitch'), + 'yaw_accel' => Arr::get($afterburner, 'AngularAccelerationMultiplier.Yaw'), + 'roll_accel' => Arr::get($afterburner, 'AngularAccelerationMultiplier.Roll'), + ], + + 'precision_mode' => [ + 'max_speed_full_proximity_assist' => Arr::get($ifcs, 'MaxSpeedPrecisionModeFullProximityAssist'), + 'max_speed_zero_proximity_assist' => Arr::get($ifcs, 'MaxSpeedPrecisionModeZeroProximityAssist'), + + 'min_distance' => Arr::get($ifcs, 'PrecisionMinDistance'), + 'max_distance' => Arr::get($ifcs, 'PrecisionMaxDistance'), + ], + + 'recall_params' => $this->collapseEmpty([ + 'hover_height_at_destination' => Arr::get($flightController, 'RecallParams.HoverHeightAtDestination'), + 'forward_offset' => Arr::get($flightController, 'RecallParams.ForwardOffset'), + 'obstruction_detection_range' => Arr::get($flightController, 'RecallParams.ObstructionDetectionRange'), + 'default_platform_detection_range' => Arr::get($flightController, 'RecallParams.DefaultPlatformDetectionRange'), + 'minimum_recall_distance' => Arr::get($flightController, 'RecallParams.MinimumRecallDistance'), + 'braking_distance_offset' => Arr::get($flightController, 'RecallParams.BrakingDistanceOffset'), + ]), + + 'collision_detection' => $this->collapseEmpty([ + 'collision_warn_speed' => Arr::get($flightController, 'CollisionDetection.CollisionWarnSpeed'), + 'collision_warn_time' => Arr::get($flightController, 'CollisionDetection.CollisionWarnTime'), + 'collision_danger_close_warn_time' => Arr::get($flightController, 'CollisionDetection.CollisionDangerCloseWarnTime'), + ]), + + 'gravlev' => [ + 'max_speed' => Arr::get($flightController, 'Gravlev.HoverMaxSpeed'), + 'turn_friction' => Arr::get($flightController, 'Gravlev.TurnFriction'), + 'air_controller_multiplier' => Arr::get($flightController, 'Gravlev.AirControllerMultiplier'), + 'anti_fall_multiplier' => Arr::get($flightController, 'Gravlev.AntiFallMultiplier'), + 'lateral_strafe_multiplier' => Arr::get($flightController, 'Gravlev.LateralStafeMultiplier'), + ], + ]; + } + + /** + * Collapse arrays that only contain null values to a single null. + */ + protected function collapseEmpty(array $data): ?array + { + foreach ($data as $value) { + if ($value !== null) { + return $data; + } + } + + return null; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/FoodResource.php b/app/Http/Resources/Game/ItemSpecification/FoodResource.php new file mode 100644 index 000000000..693240d1a --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/FoodResource.php @@ -0,0 +1,203 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + $description = Arr::get($stdItem, 'DescriptionData', []); + $food = Arr::get($stdItem, $this->type, []); + + $effects = Arr::get($food, 'Effects', []); + + if (is_string($effects)) { + $effects = $this->splitEffects($effects); + } + + if ($effects === [] || $effects === null) { + $effects = $this->splitEffects( + Arr::get($description, 'Effects', Arr::get($description, 'Effect', '')) + ); + } + + return [ + 'nutrition' => collect(Arr::get($food, 'Nutrition', []))->mapWithKeys(fn ($value, $key) => [ + Str::snake($key) => Arr::get($value, 'Total'), + ]), + + 'buffs' => collect(Arr::get($food, 'Buffs', []))->mapWithKeys(fn ($buff) => [ + Str::snake(Arr::get($buff, 'Type')) => Arr::get($buff, 'Duration', 0), + ]), + + 'debuffs' => collect(Arr::get($food, 'Debuffs', []))->mapWithKeys(fn ($buff) => [ + Str::snake(Arr::get($buff, 'Type')) => Arr::get($buff, 'Duration', 0), + ]), + + 'container' => [ + 'type' => Arr::get($food, 'Container.Type'), + 'closed' => $this->toBool(Arr::get($food, 'Container.Closed')), + 'can_be_reclosed' => $this->toBool(Arr::get($food, 'Container.CanBeReclosed')), + 'discard_when_consumed' => $this->toBool(Arr::get($food, 'Container.DiscardWhenConsumed')), + ], + + 'consumption' => [ + 'volume' => Arr::get($food, 'Consumption.Volume'), + 'one_shot_consume' => $this->toBool(Arr::get($food, 'Consumption.OneShotConsume')), + ], + + 'nutritional_density_rating' => Arr::get($food, 'NutritionalDensityRating', Arr::get($description, 'NDR')), + 'hydration_efficacy_index' => Arr::get($food, 'HydrationEfficacyIndex', Arr::get($description, 'HEI')), + 'container_type' => Arr::get($food, 'Container.Type'), + 'one_shot_consume' => $this->toBool(Arr::get($food, 'Consumption.OneShotConsume')), + 'can_be_reclosed' => $this->toBool(Arr::get($food, 'Container.CanBeReclosed')), + 'discard_when_consumed' => $this->toBool(Arr::get($food, 'Container.DiscardWhenConsumed')), + 'effects' => $effects === [] ? null : $effects, + ]; + } + + private function splitEffects(string $effects): array + { + $effects = array_filter(array_map('trim', explode(',', $effects))); + + return array_values($effects); + } + + private function toBool(mixed $value): ?bool + { + if ($value === null) { + return null; + } + + if (is_bool($value)) { + return $value; + } + + if (is_numeric($value)) { + return (int) $value === 1; + } + + return null; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/FuelIntakeResource.php b/app/Http/Resources/Game/ItemSpecification/FuelIntakeResource.php new file mode 100644 index 000000000..8997a96df --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/FuelIntakeResource.php @@ -0,0 +1,34 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + $flowRates = Arr::get($stdItem, 'FuelIntake.FlowRates', Arr::get($stdItem, 'FuelIntake', [])); + + return [ + 'fuel_push_rate' => Arr::get($flowRates, 'FuelPushRate', Arr::get($flowRates, 'fuelPushRate')), + 'minimum_rate' => Arr::get($flowRates, 'MinimumRate', Arr::get($flowRates, 'minimumRate')), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/FuelTankResource.php b/app/Http/Resources/Game/ItemSpecification/FuelTankResource.php new file mode 100644 index 000000000..25f9ffe83 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/FuelTankResource.php @@ -0,0 +1,54 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + + return [ + 'fill_rate' => Arr::get($stdItem, 'ResourceNetwork.States.0.Deltas.0.GenerateRate'), + 'drain_rate' => Arr::get($stdItem, 'ResourceNetwork.States.0.Deltas.0.Discharge'), + 'capacity' => Arr::get($stdItem, 'FuelTank.Capacity'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/GrenadeResource.php b/app/Http/Resources/Game/ItemSpecification/GrenadeResource.php new file mode 100644 index 000000000..30da1975e --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/GrenadeResource.php @@ -0,0 +1,90 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + $grenade = Arr::get($stdItem, 'Grenade', []); + + $areaOfEffectMax = Arr::get($grenade, 'AreaOfEffect'); + + return [ + + 'damage_type' => Arr::get($grenade, 'DamageType'), + 'damage' => Arr::get($grenade, 'Damage'), + + 'aoe' => [ + 'minimum' => Arr::get($grenade, 'MinAreaOfEffect'), + 'maximum' => $areaOfEffectMax, + ], + + 'area_of_effect' => $areaOfEffectMax, + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/HackingChipResource.php b/app/Http/Resources/Game/ItemSpecification/HackingChipResource.php new file mode 100644 index 000000000..566d864ec --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/HackingChipResource.php @@ -0,0 +1,62 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $chip = Arr::get($data, 'HackingChip', []); + + return [ + 'max_charges' => Arr::get($chip, 'MaxCharges'), + 'duration_multiplier' => Arr::get($chip, 'DurationMultiplier'), + 'error_chance' => Arr::get($chip, 'ErrorChance'), + 'access_tag' => Arr::get($chip, 'AccessTag'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/JumpDriveResource.php b/app/Http/Resources/Game/ItemSpecification/JumpDriveResource.php new file mode 100644 index 000000000..9d9abaf9f --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/JumpDriveResource.php @@ -0,0 +1,40 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $item = Arr::get($data, 'stdItem.JumpDrive', []); + + return [ + 'alignment_rate' => Arr::get($item, 'AlignmentRate'), + 'alignment_decay_rate' => Arr::get($item, 'AlignmentDecayRate'), + 'tuning_rate' => Arr::get($item, 'TuningRate'), + 'tuning_decay_rate' => Arr::get($item, 'TuningDecayRate'), + 'fuel_usage_efficiency_multiplier' => Arr::get($item, 'FuelUsageEfficiencyMultiplier'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/MedicineResource.php b/app/Http/Resources/Game/ItemSpecification/MedicineResource.php new file mode 100644 index 000000000..186e543d9 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/MedicineResource.php @@ -0,0 +1,107 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + $medicine = Arr::get($stdItem, 'Medical', []); + + $combatBuffs = collect(Arr::get($medicine, 'MedicalEffects.CombatBuffs', []))->toArray(); + $impactResistance = Arr::get($medicine, 'MedicalEffects.ImpactResistance', []); + + $return = [ + ...parent::toArray($request), + 'combat_buffs' => collect(Arr::get($medicine, 'Buffs', [])) + ->reject(fn ($value) => ! in_array($value['Type'], $combatBuffs, true)) + ->mapWithKeys(fn ($value) => [ + str_replace(['_mask'], '', Str::snake($value['Type'])) => true, + ])->toArray(), + 'impact_resistances' => collect(Arr::get($medicine, 'Buffs', [])) + ->reject(fn ($value) => ! in_array($value['Type'], $impactResistance, true)) + ->mapWithKeys(fn ($value) => [ + str_replace(['impact_resistance_', '_mask'], '', Str::snake($value['Type'])) => true, + ]) + ->toArray(), + ]; + + unset( + $return['buffs'], + $return['nutritional_density_rating'], + $return['hydration_efficacy_index'], + $return['container_type'], + $return['one_shot_consume'], + $return['can_be_reclosed'], + $return['discard_when_consumed'], + $return['effects'], + ); + + return $return; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/MeleeWeaponResource.php b/app/Http/Resources/Game/ItemSpecification/MeleeWeaponResource.php new file mode 100644 index 000000000..ba32a28b0 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/MeleeWeaponResource.php @@ -0,0 +1,119 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + + $melee = Arr::get($stdItem, 'MeleeWeapon', Arr::get($stdItem, 'meleeWeapon', [])); + $attackConfigs = Arr::get($melee, 'AttackConfig', []); + + return [ + 'can_be_used_for_take_down' => Arr::get($melee, 'CanBeUsedForTakeDown', Arr::get($melee, 'canBeUsedForTakeDown')), + 'can_block' => Arr::get($melee, 'CanBlock', Arr::get($melee, 'canBlock')), + 'can_be_used_in_prone' => Arr::get($melee, 'CanBeUsedInProne', Arr::get($melee, 'canBeUsedInProne')), + 'can_dodge' => Arr::get($melee, 'CanDodge', Arr::get($melee, 'canDodge')), + 'stance_transition_melee_delay' => Arr::get($melee, 'StanceTransitionMeleeDelay', Arr::get($melee, 'stanceTransitionMeleeDelay')), + 'melee_combat_config' => Arr::get($melee, 'MeleeCombatConfig'), + 'attack_modes' => collect(is_array($attackConfigs) ? $attackConfigs : [$attackConfigs])->map( + static function (mixed $attack): array { + $damage = Arr::get($attack, 'Damage', []); + + return [ + 'category' => Arr::get($attack, 'ActionCategory'), + 'damage' => Arr::get($attack, 'DamageTotal'), + 'stun_recovery_modifier' => Arr::get($attack, 'StunRecoveryModifier'), + 'block_stun_reduction_modifier' => Arr::get($attack, 'BlockStunReductionModifier'), + 'block_stun_stamina_modifier' => Arr::get($attack, 'BlockStunStaminaModifier'), + 'attack_impulse' => Arr::get($attack, 'AttackImpulse'), + 'ignore_body_part_impulse_scale' => Arr::get($attack, 'IgnoreBodyPartImpulseScale'), + 'force_knockdown' => Arr::get($attack, 'ForceKnockdown'), + 'damages' => [ + 'physical' => Arr::get($damage, 'Physical'), + 'energy' => Arr::get($damage, 'Energy'), + 'distortion' => Arr::get($damage, 'Distortion'), + 'thermal' => Arr::get($damage, 'Thermal'), + 'biochemical' => Arr::get($damage, 'Biochemical'), + 'stun' => Arr::get($damage, 'Stun'), + ], + ]; + } + ), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/MiningLaserResource.php b/app/Http/Resources/Game/ItemSpecification/MiningLaserResource.php new file mode 100644 index 000000000..98470a1ba --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/MiningLaserResource.php @@ -0,0 +1,225 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $stdItem = $this->extractStdItem($data); + $description = Arr::get($stdItem, 'DescriptionData', []); + $miningLaser = Arr::get($stdItem, 'MiningLaser', []); + $modifiers = is_array($miningLaser) ? Arr::get($miningLaser, 'Modifiers', []) : []; + + $modifierBlock = [ + 'resistance' => $this->toFloat(Arr::get($modifiers, 'Resistance')), + 'laser_instability' => $this->toFloat(Arr::get($modifiers, 'Instability')), + 'optimal_charge_window_size' => $this->toFloat(Arr::get($modifiers, 'OptimalChargeWindow')), + 'optimal_charge_rate' => $this->toFloat(Arr::get($modifiers, 'OptimalChargeRate')), + 'inert_materials' => $this->toFloat(Arr::get($modifiers, 'InertMaterials')), + + 'all_charge_rates' => $this->toFloat(Arr::get($modifiers, 'AllChargeRates')), + ]; + + return [ + 'laser_power' => [ + 'minimum' => Arr::get($miningLaser, 'MinPowerTransfer'), + 'maximum' => Arr::get($miningLaser, 'PowerTransfer'), + ], + + 'modifiers' => collect($modifierBlock) + ->map(static fn ($value, $key) => [ + 'name' => $key, + 'display_name' => Str::of($key)->snake()->replace('_', ' ')->title()->toString(), + 'value' => $value, + ]) + ->filter(static fn ($value) => $value['value'] !== null) + ->values() + ->toArray(), + + 'module_slots' => Arr::get($miningLaser, 'ModuleSlots'), + + 'throttle_lerp_speed' => Arr::get($miningLaser, 'ThrottleLerpSpeed'), + 'throttle_minimum' => Arr::get($miningLaser, 'ThrottleMinimum'), + + 'power_transfer' => Arr::get($miningLaser, 'PowerTransfer'), + + 'optimal_range' => Arr::get($miningLaser, 'OptimalRange'), + 'maximum_range' => Arr::get($miningLaser, 'MaximumRange'), + + 'extraction_throughput' => Arr::get($miningLaser, 'ExtractionThroughput'), + 'extraction_laser_power' => Arr::get($description, 'Extraction Laser Power'), + 'mining_laser_power' => $this->formatPowerRange( + Arr::get($miningLaser, 'MinPowerTransfer'), + Arr::get($miningLaser, 'PowerTransfer'), + ), + + 'modifier_map' => array_filter($modifierBlock, static fn ($value) => $value !== null), + ]; + } + + private function toFloat(mixed $value): ?float + { + if ($value === null) { + return null; + } + + if (is_float($value) || is_int($value)) { + return (float) $value; + } + + if (is_string($value)) { + $normalized = str_replace('%', '', $value); + + if (is_numeric($normalized)) { + return (float) $normalized; + } + + if (preg_match('/(-?\d+(?:\.\d+)?)/', $value, $matches)) { + return (float) $matches[1]; + } + } + + return null; + } + + private function formatPowerRange(?float $min, ?float $max): ?string + { + if ($min === null && $max === null) { + return null; + } + + if ($min !== null && $max !== null) { + return sprintf('%d - %d', $min, $max); + } + + return (string) ($max ?? $min); + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/MiningModifierResource.php b/app/Http/Resources/Game/ItemSpecification/MiningModifierResource.php new file mode 100644 index 000000000..2ca46ce15 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/MiningModifierResource.php @@ -0,0 +1,109 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $stdItem = $this->extractStdItem($data); + $modifier = Arr::get($stdItem, 'MiningModule', []); + + $modifiers = Arr::get($modifier, 'Modifiers', []); + + return [ + 'type' => Arr::get($modifier, 'Type'), + 'item_type' => Arr::get($data, 'classification') === 'Mining.Gadget' ? 'Gadget' : 'Module', + 'charges' => Arr::get($modifier, 'Charges') === -1 ? null : Arr::get($modifier, 'Charges'), + 'duration' => Arr::get($modifier, 'Lifetime'), + 'power_modifier' => Arr::get($modifiers, 'DamageMultiplierChange'), + + 'modifier_map' => array_filter([ + 'resistance' => Arr::get($modifiers, 'Resistance'), + 'laser_instability' => Arr::get($modifiers, 'Instability'), + 'optimal_charge_window_size' => Arr::get($modifiers, 'OptimalChargeWindow'), + 'optimal_charge_rate' => Arr::get($modifiers, 'OptimalChargeRate'), + 'cluster_factor' => Arr::get($modifiers, 'ClusterFactor'), + 'overcharge_rate' => Arr::get($modifiers, 'OverchargeRate'), + 'shatter_damage' => Arr::get($modifiers, 'ShatterDamage'), + 'inert_materials' => Arr::get($modifiers, 'InertMaterials'), + ]), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/MiningModuleResource.php b/app/Http/Resources/Game/ItemSpecification/MiningModuleResource.php new file mode 100644 index 000000000..b8c07cb30 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/MiningModuleResource.php @@ -0,0 +1,160 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $stdItem = $this->extractStdItem($data); + $description = Arr::get($stdItem, 'DescriptionData', []); + $miningModule = Arr::get($stdItem, 'MiningModule', []); + $modifiers = is_array($miningModule) ? Arr::get($miningModule, 'Modifiers', []) : []; + + $legacyModifiers = [ + [ + 'name' => 'all_charge_rates', + 'display_name' => 'All Charge Rates', + 'value' => Arr::get($description, 'All Charge Rates', Arr::get($modifiers, 'AllChargeRates')), + ], + [ + 'name' => 'collection_point_radius', + 'display_name' => 'Collection Point Radius', + 'value' => Arr::get($description, 'Collection Point Radius', Arr::get($modifiers, 'CollectionPointRadius')), + ], + [ + 'name' => 'instability', + 'display_name' => 'Instability', + 'value' => Arr::get($description, 'Instability', Arr::get($modifiers, 'Instability')), + ], + [ + 'name' => 'optimal_charge_rate', + 'display_name' => 'Optimal Charge Rate', + 'value' => Arr::get($description, 'Optimal Charge Rate', Arr::get($modifiers, 'OptimalChargeRate')), + ], + [ + 'name' => 'optimal_charge_window', + 'display_name' => 'Optimal Charge Window', + 'value' => Arr::get($description, 'Optimal Charge Window', Arr::get($description, 'Optimal Charge Window Size', Arr::get($modifiers, 'OptimalChargeWindow'))), + ], + [ + 'name' => 'overcharge_rate', + 'display_name' => 'Overcharge Rate', + 'value' => Arr::get($description, 'Overcharge Rate', Arr::get($description, 'Catastrophic Charge Rate', Arr::get($modifiers, 'OverchargeRate'))), + ], + [ + 'name' => 'resistance', + 'display_name' => 'Resistance', + 'value' => Arr::get($description, 'Resistance', Arr::get($modifiers, 'Resistance')), + ], + [ + 'name' => 'shatter_damage', + 'display_name' => 'Shatter Damage', + 'value' => Arr::get($description, 'Shatter Damage', Arr::get($modifiers, 'ShatterDamage')), + ], + [ + 'name' => 'extraction_rate', + 'display_name' => 'Extraction Rate', + 'value' => Arr::get($description, 'Extraction Rate', Arr::get($modifiers, 'ExtractionRate')), + ], + [ + 'name' => 'inert_materials', + 'display_name' => 'Inert Materials', + 'value' => Arr::get($description, 'Inert Materials', Arr::get($modifiers, 'InertMaterials')), + ], + ]; + + return [ + 'type' => Arr::get($description, 'Item Type'), + 'uses' => Arr::get($description, 'Uses'), + 'duration' => Arr::get($description, 'Duration'), + 'modifiers' => array_filter($legacyModifiers, static fn ($value) => $value['value'] !== null), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/MissileRackResource.php b/app/Http/Resources/Game/ItemSpecification/MissileRackResource.php new file mode 100644 index 000000000..04e3af69e --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/MissileRackResource.php @@ -0,0 +1,33 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + + return [ + 'missile_count' => Arr::get($stdItem, 'MissileRack.MissileCount'), + 'missile_size' => Arr::get($stdItem, 'MissileRack.MissileSize'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/MissileResource.php b/app/Http/Resources/Game/ItemSpecification/MissileResource.php new file mode 100644 index 000000000..ab14823a0 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/MissileResource.php @@ -0,0 +1,310 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $missile = Arr::get($data, 'stdItem.Missile', []); + $targeting = Arr::get($missile, 'Targeting', []); + $gcs = Arr::get($missile, 'GCS', []); + $damageData = Arr::get($missile, 'Damage', []); + + $damages = $this->buildDamageArray($damageData); + $totalDamage = $this->calculateTotalDamage($damageData); + $damageMap = array_filter([ + 'physical' => Arr::get($damageData, 'Physical'), + 'energy' => Arr::get($damageData, 'Energy'), + 'distortion' => Arr::get($damageData, 'Distortion'), + 'thermal' => Arr::get($damageData, 'Thermal'), + 'biochemical' => Arr::get($damageData, 'Biochemical'), + 'stun' => Arr::get($damageData, 'Stun'), + ], static fn ($value) => $value !== null); + + return [ + 'cluster_size' => Arr::has($missile, 'Cluster.Size') ? Arr::get($missile, 'Cluster.Size') : null, + 'signal_type' => Arr::get($targeting, 'TrackingSignalType'), + 'tracking_signal_min' => Arr::get($targeting, 'TrackingSignalMin'), + + 'lock_time' => Arr::get($targeting, 'LockTime'), + 'lock_range_max' => Arr::get($targeting, 'LockRangeMax'), + 'lock_range_min' => Arr::get($targeting, 'LockRangeMin'), + 'lock_angle' => Arr::get($targeting, 'LockingAngle'), + 'speed' => Arr::get($gcs, 'LinearSpeed'), + 'fuel_tank_size' => Arr::get($gcs, 'FuelTankSize'), + 'explosion_radius_min' => Arr::get($missile, 'ExplosionRadius.Minimum'), + 'explosion_radius_max' => Arr::get($missile, 'ExplosionRadius.Maximum'), + + 'flight' => [ + 'enable_lifetime' => Arr::get($missile, 'EnableLifetime'), + 'max_lifetime' => Arr::get($missile, 'MaxLifetime'), + 'range' => Arr::get($missile, 'Distance'), + + 'speed' => Arr::get($gcs, 'LinearSpeed'), + 'boost_speed' => Arr::get($gcs, 'BoostSpeed'), + 'intercept_speed' => Arr::get($gcs, 'InterceptSpeed'), + 'terminal_speed' => Arr::get($gcs, 'TerminalSpeed'), + + 'boost_phase_duration' => Arr::get($gcs, 'BoostPhaseDuration'), + 'terminal_phase_engagement_time' => Arr::get($gcs, 'TerminalPhaseEngagementTime'), + 'terminal_phase_engagement_angle' => Arr::get($gcs, 'TerminalPhaseEngagementAngle'), + + 'fuel_tank_size' => Arr::get($gcs, 'FuelTankSize'), + ], + + 'target_lock' => [ + 'signal_resilience_min' => Arr::get($targeting, 'SignalResilienceMin'), + 'signal_resilience_max' => Arr::get($targeting, 'SignalResilienceMax'), + + 'range_max' => Arr::get($targeting, 'LockRangeMax'), + 'range_min' => Arr::get($targeting, 'LockRangeMin'), + + 'angle' => Arr::get($targeting, 'LockingAngle'), + + 'signal_amplifier' => Arr::get($targeting, 'LockSignalAmplifier'), + 'increase_rate' => Arr::get($targeting, 'LockIncreaseRate'), + + 'allow_dumb_firing' => Arr::get($targeting, 'AllowDumbFiring'), + ], + + 'explosion' => [ + 'is_cluster' => Arr::get($missile, 'IsCluster'), + 'cluster_size' => Arr::has($missile, 'Cluster.Size') ? Arr::get($missile, 'Cluster.Size') : null, + 'requires_launcher' => Arr::get($missile, 'RequiresLauncher'), + 'allow_dumb_firing' => Arr::get($targeting, 'AllowDumbFiring'), + + 'radius_min' => Arr::get($missile, 'ExplosionRadius.Minimum'), + 'radius_max' => Arr::get($missile, 'ExplosionRadius.Maximum'), + + 'safety_distance' => Arr::get($missile, 'ExplosionSafetyDistance'), + 'proximity' => Arr::get($missile, 'ProjectileProximity'), + ], + + 'delays' => [ + 'arm_time' => Arr::get($missile, 'ArmTime'), + 'ignite_time' => Arr::get($missile, 'IgniteTime'), + 'collision_delay_time' => Arr::get($missile, 'CollisionDelayTime'), + 'lock_time' => Arr::get($targeting, 'LockTime'), + ], + + 'damage_total' => $totalDamage > 0 ? $totalDamage : null, + // Deprecated: + 'damages' => WeaponDamageResource::collection($damages), + 'damage_map' => $damageMap === [] ? null : $damageMap, + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/PersonalWeaponResource.php b/app/Http/Resources/Game/ItemSpecification/PersonalWeaponResource.php new file mode 100644 index 000000000..8eec93c7b --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/PersonalWeaponResource.php @@ -0,0 +1,315 @@ +extractFromStdItem($this->resource, 'Ammunition'); + $weapon = $this->extractFromStdItem($this->resource, 'Weapon'); + $mode = Arr::get($weapon, 'Modes.0'); + $damage = Arr::get($weapon, 'Damage'); + + $damages = array_filter([ + ['type' => 'impact', 'name' => 'physical', 'damage' => Arr::get($ammo, 'ImpactDamage.Physical')], + ['type' => 'impact', 'name' => 'energy', 'damage' => Arr::get($ammo, 'ImpactDamage.Energy')], + ['type' => 'impact', 'name' => 'distortion', 'damage' => Arr::get($ammo, 'ImpactDamage.Distortion')], + ['type' => 'impact', 'name' => 'thermal', 'damage' => Arr::get($ammo, 'ImpactDamage.Thermal')], + ['type' => 'impact', 'name' => 'biochemical', 'damage' => Arr::get($ammo, 'ImpactDamage.Biochemical')], + ['type' => 'impact', 'name' => 'stun', 'damage' => Arr::get($ammo, 'ImpactDamage.Stun')], + + ['type' => 'detonation', 'name' => 'physical', 'damage' => Arr::get($ammo, 'DetonationDamage.Physical')], + ['type' => 'detonation', 'name' => 'energy', 'damage' => Arr::get($ammo, 'DetonationDamage.Energy')], + ['type' => 'detonation', 'name' => 'distortion', 'damage' => Arr::get($ammo, 'DetonationDamage.Distortion')], + ['type' => 'detonation', 'name' => 'thermal', 'damage' => Arr::get($ammo, 'DetonationDamage.Thermal')], + ['type' => 'detonation', 'name' => 'biochemical', 'damage' => Arr::get($ammo, 'DetonationDamage.Biochemical')], + ['type' => 'detonation', 'name' => 'stun', 'damage' => Arr::get($ammo, 'DetonationDamage.Stun')], + ], static fn (array $entry) => $entry !== [] && ! empty($entry['damage'])); + + $modes = collect(Arr::get($weapon, 'Modes', [])) + ->map(static fn (mixed $mode): array => [ + 'mode' => Arr::get($mode, 'Name'), + 'localised' => Arr::get($mode, 'LocalisedName'), + 'type' => Arr::get($mode, 'FireType'), + 'rpm' => Arr::get($mode, 'RoundsPerMinute'), + 'ammo_per_shot' => Arr::get($mode, 'AmmoPerShot'), + 'pellets_per_shot' => Arr::get($mode, 'PelletsPerShot'), + 'damage_per_second' => Arr::get($mode, 'DamagePerSecond'), + ]) + ->values() + ->toArray(); + + return [ + 'class' => $this->extractFromStdItem($this->resource, 'DescriptionData.Class'), + 'type' => $this->extractFromStdItem($this->resource, 'DescriptionData.Item Type'), + + // deprecated + 'magazine_type' => '', + // deprecated + 'magazine_size' => Arr::get($weapon, 'Capacity'), + + // deprecated + 'effective_range' => Arr::get($weapon, 'EffectiveRange'), + + 'capacity' => Arr::get($weapon, 'Capacity'), + + 'range' => Arr::get($weapon, 'EffectiveRange'), + + 'damage_per_shot' => Arr::get($mode, 'Alpha'), + 'pellets_per_shot' => Arr::get($weapon, 'PelletsPerShot'), + + // deprecated + 'rof' => Arr::get($mode, 'RoundsPerMinute'), + + 'rpm' => Arr::get($mode, 'RoundsPerMinute'), + + 'damages' => $damages, + 'modes' => $modes, + + 'fire_mode' => Arr::get($weapon, 'FireMode'), + + 'damage' => [ + 'dps_total' => Arr::get($damage, 'DpsTotal'), + 'alpha_total' => Arr::get($damage, 'AlphaTotal'), + 'maximum' => Arr::get($damage, 'MaxPerMag'), + 'dps' => [ + 'physical' => Arr::get($weapon, 'Damage.Dps.Physical'), + 'energy' => Arr::get($weapon, 'Damage.Dps.Energy'), + 'distortion' => Arr::get($weapon, 'Damage.Dps.Distortion'), + 'thermal' => Arr::get($weapon, 'Damage.Dps.Thermal'), + 'biochemical' => Arr::get($weapon, 'Damage.Dps.Biochemical'), + 'stun' => Arr::get($weapon, 'Damage.Dps.Stun'), + ], + 'alpha' => [ + 'physical' => Arr::get($weapon, 'Damage.Alpha.Physical'), + 'energy' => Arr::get($weapon, 'Damage.Alpha.Energy'), + 'distortion' => Arr::get($weapon, 'Damage.Alpha.Distortion'), + 'thermal' => Arr::get($weapon, 'Damage.Alpha.Thermal'), + 'biochemical' => Arr::get($weapon, 'Damage.Alpha.Biochemical'), + 'stun' => Arr::get($weapon, 'Damage.Alpha.Stun'), + ], + ], + + $this->mergeWhen(Arr::get($mode, 'Spread.Minimum') !== null, [ + 'spread' => [ + 'minimum' => Arr::get($weapon, 'Spread.Minimum'), + 'maximum' => Arr::get($weapon, 'Spread.Maximum'), + 'first_attack' => Arr::get($weapon, 'Spread.FirstAttack'), + 'per_attack' => Arr::get($weapon, 'Spread.Attack'), + 'decay' => Arr::get($weapon, 'Spread.Decay'), + ], + 'ads_spread' => [ + 'minimum' => Arr::get($weapon, 'AdsSpread.Minimum') == 0 ? null : Arr::get($weapon, 'AdsSpread.Min'), + 'maximum' => Arr::get($weapon, 'AdsSpread.Maximum') == 0 ? null : Arr::get($weapon, 'AdsSpread.Max'), + 'first_attack' => Arr::get($weapon, 'AdsSpread.FirstAttack') == 0 ? null : Arr::get($weapon, 'AdsSpread.FirstAttack'), + 'per_attack' => Arr::get($weapon, 'AdsSpread.Attack') == 0 ? null : Arr::get($weapon, 'AdsSpread.Attack'), + 'decay' => Arr::get($weapon, 'AdsSpread.Decay') == 0 ? null : Arr::get($weapon, 'AdsSpread.Decay'), + ], + ]), + + $this->mergeWhen(Arr::get($mode, 'Charge') !== null, [ + 'charge' => [ + 'time' => Arr::get($weapon, 'Charge.ChargeTime'), + 'overcharge_time' => Arr::get($weapon, 'Charge.OverchargeTime'), + 'overcharged_time' => Arr::get($weapon, 'Charge.OverchargedTime'), + 'cooldown_time' => Arr::get($weapon, 'Charge.CooldownTime'), + ], + 'charge_modifier' => [ + 'damage' => Arr::get($weapon, 'ChargeModifier.Damage'), + 'fire_rate' => Arr::get($weapon, 'ChargeModifier.FireRate'), + 'ammo_speed' => Arr::get($weapon, 'ChargeModifier.AmmoSpeed'), + ], + ]), + + 'ammunition' => new AmmunitionResource($this->resource), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/PowerPlantResource.php b/app/Http/Resources/Game/ItemSpecification/PowerPlantResource.php new file mode 100644 index 000000000..05b841bdb --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/PowerPlantResource.php @@ -0,0 +1,36 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + $power = Arr::get($stdItem, 'PowerConnection', []); + + if ($power === []) { + $power = Arr::get($data, 'Raw.Entity.Components.EntityComponentPowerConnection', []); + } + + return [ + 'power_output' => Arr::get($power, 'PowerDraw', Arr::get($power, 'powerDraw')), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/QuantumDriveJumpProfileResource.php b/app/Http/Resources/Game/ItemSpecification/QuantumDriveJumpProfileResource.php new file mode 100644 index 000000000..f88db5dd4 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/QuantumDriveJumpProfileResource.php @@ -0,0 +1,141 @@ +resource) ? $this->resource : []; + + return [ + $this->mergeWhen($this->type !== null, [ + 'type' => $this->type, + ]), + 'drive_speed' => Arr::get($profile, 'DriveSpeed'), + 'cooldown_time' => Arr::get($profile, 'CooldownTime'), + 'stage_one_accel_rate' => Arr::get($profile, 'StageOneAccelRate'), + 'stage_two_accel_rate' => Arr::get($profile, 'StageTwoAccelRate'), + 'engage_speed' => Arr::get($profile, 'EngageSpeed'), + 'interdiction_effect_time' => Arr::get($profile, 'InterdictionEffectTime'), + 'calibration_rate' => Arr::get($profile, 'CalibrationRate'), + 'min_calibration_requirement' => Arr::get($profile, 'MinCalibrationRequirement'), + 'max_calibration_requirement' => Arr::get($profile, 'MaxCalibrationRequirement'), + 'calibration_process_angle_limit' => Arr::get($profile, 'CalibrationProcessAngleLimit'), + 'calibration_warning_angle_limit' => Arr::get($profile, 'CalibrationWarningAngleLimit'), + 'calibration_delay_in_seconds' => Arr::get($profile, 'CalibrationDelayInSeconds'), + 'spool_up_time' => Arr::get($profile, 'SpoolUpTime'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/QuantumDriveResource.php b/app/Http/Resources/Game/ItemSpecification/QuantumDriveResource.php new file mode 100644 index 000000000..1c8382b1b --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/QuantumDriveResource.php @@ -0,0 +1,156 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $quantumDrive = Arr::get($data, 'stdItem.QuantumDrive', []); + + $heat = Arr::get($quantumDrive, 'Heat', []); + $standardJump = Arr::get($quantumDrive, 'StandardJump', []); + $splineJump = Arr::get($quantumDrive, 'SplineJump', []); + + return [ + 'quantum_fuel_requirement' => Arr::get($quantumDrive, 'QuantumFuelRequirement'), + 'jump_range' => Arr::get($quantumDrive, 'JumpRange'), + 'disconnect_range' => Arr::get($quantumDrive, 'DisconnectRange'), + 'fuel_rate' => Arr::get($quantumDrive, 'FuelRate'), + 'thermal_energy_draw' => [ + 'pre_ramp_up' => Arr::get($heat, 'PreRampUpThermalEnergyDraw'), + 'ramp_up' => Arr::get($heat, 'RampUpThermalEnergyDraw'), + 'in_flight' => Arr::get($heat, 'InFlightThermalEnergyDraw'), + 'ramp_down' => Arr::get($heat, 'RampDownThermalEnergyDraw'), + 'post_ramp_down' => Arr::get($heat, 'PostRampDownThermalEnergyDraw'), + ], + 'standard_jump' => new QuantumDriveJumpProfileResource($standardJump), + 'spline_jump' => new QuantumDriveJumpProfileResource($splineJump), + + 'modes' => [ + new QuantumDriveJumpProfileResource($standardJump, 'normal_jump'), + new QuantumDriveJumpProfileResource($splineJump, 'spline_jump'), + ], + + 'fuel_consumption_scu_per_gm' => Arr::get($quantumDrive, 'FuelConsumptionSCUPerGM'), + 'fuel_efficiency' => Arr::get($quantumDrive, 'FuelEfficiencyGMPerSCU'), + + 'travel_time_10gm' => [ + 'seconds' => Arr::get($quantumDrive, 'TravelTime10GMSeconds'), + 'formatted' => Arr::get($quantumDrive, 'TravelTime10GM'), + ], + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/QuantumInterdictionGeneratorResource.php b/app/Http/Resources/Game/ItemSpecification/QuantumInterdictionGeneratorResource.php new file mode 100644 index 000000000..497a2e162 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/QuantumInterdictionGeneratorResource.php @@ -0,0 +1,192 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $quantumInterdiction = Arr::get($data, 'stdItem.QuantumInterdictionGenerator', []); + + $jammer = Arr::get($quantumInterdiction, 'Jammer', []); + $pulse = Arr::get($quantumInterdiction, 'Pulse', []); + + return [ + 'power_fractions' => [ + 'base' => Arr::get($quantumInterdiction, 'BasePowerDrawFraction'), + 'pulse' => Arr::get($quantumInterdiction, 'PulsePowerFraction'), + 'jammer' => Arr::get($quantumInterdiction, 'JammerPowerFraction'), + ], + 'jamming' => [ + 'range' => Arr::get($quantumInterdiction, 'JammingRange'), + 'max_power_draw' => Arr::get($jammer, 'MaxPowerDraw'), + 'green_zone_check_range' => Arr::get($jammer, 'GreenZoneCheckRange'), + ], + 'pulse' => [ + 'charge_time' => Arr::get($pulse, 'ChargeTimeSecs'), + 'discharge_time' => Arr::get($pulse, 'DischargeTimeSecs'), + 'cooldown_time' => Arr::get($pulse, 'CooldownTimeSecs'), + 'radius' => Arr::get($pulse, 'RadiusMeters'), + 'decrease_charge_rate_time' => Arr::get($pulse, 'DecreaseChargeRateTimeSeconds'), + 'increase_charge_rate_time' => Arr::get($pulse, 'IncreaseChargeRateTimeSeconds'), + 'activation_phase_duration' => Arr::get($pulse, 'ActivationPhaseDuration_seconds'), + 'disperse_charge_time' => Arr::get($pulse, 'DisperseChargeTimeSeconds'), + 'max_power_draw' => Arr::get($pulse, 'MaxPowerDraw'), + 'stop_charging_power_fraction' => Arr::get($pulse, 'StopChargingPowerDrawFraction'), + 'max_charge_rate_power_fraction' => Arr::get($pulse, 'MaxChargeRatePowerDrawFraction'), + 'active_power_fraction' => Arr::get($pulse, 'ActivePowerDrawFraction'), + 'tethering_power_fraction' => Arr::get($pulse, 'TetheringPowerDrawFraction'), + 'green_zone_check_range' => Arr::get($pulse, 'GreenZoneCheckRange'), + ], + + 'interdiction_range' => Arr::get($quantumInterdiction, 'InterdictionRange'), + 'jammer_range' => Arr::get($quantumInterdiction, 'JammingRange'), + + 'charge_duration' => Arr::get($pulse, 'ChargeTimeSecs'), + 'activation_duration' => Arr::get($pulse, 'ActivationPhaseDurationSeconds'), + 'discharge_duration' => Arr::get($pulse, 'DischargeTimeSecs'), + 'cooldown_duration' => Arr::get($pulse, 'CooldownTimeSecs'), + 'disperse_charge_duration' => Arr::get($pulse, 'DisperseChargeTimeSeconds'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/RadarResource.php b/app/Http/Resources/Game/ItemSpecification/RadarResource.php new file mode 100644 index 000000000..a825a452f --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/RadarResource.php @@ -0,0 +1,118 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + $radar = Arr::get($stdItem, 'Radar', []); + + return [ + 'detection_lifetime' => null, + 'altitude_ceiling' => null, + 'enable_cross_section_occlusion' => null, + + 'cooldown' => Arr::get($radar, 'Cooldown'), + 'sensitivity' => [ + 'infrared' => Arr::get($radar, 'Sensitivity.IR'), + 'cross_section' => Arr::get($radar, 'Sensitivity.CS'), + 'electromagnetic' => Arr::get($radar, 'Sensitivity.EM'), + 'resource' => Arr::get($radar, 'Sensitivity.RS'), + 'db' => Arr::get($radar, 'Sensitivity.dB'), + ], + 'ground_vehicle_sensitivity' => [ + 'infrared' => Arr::get($radar, 'GroundVehicleDetectionSensitivity.IR'), + 'cross_section' => Arr::get($radar, 'GroundVehicleDetectionSensitivity.CS'), + 'electromagnetic' => Arr::get($radar, 'GroundVehicleDetectionSensitivity.EM'), + 'resource' => Arr::get($radar, 'GroundVehicleDetectionSensitivity.RS'), + 'db' => Arr::get($radar, 'GroundVehicleDetectionSensitivity.dB'), + ], + 'piercing' => [ + 'infrared' => Arr::get($radar, 'Piercing.IR'), + 'cross_section' => Arr::get($radar, 'Piercing.CS'), + 'electromagnetic' => Arr::get($radar, 'Piercing.EM'), + 'resource' => Arr::get($radar, 'Piercing.RS'), + 'db' => Arr::get($radar, 'Piercing.dB'), + ], + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/RadiationResistanceResource.php b/app/Http/Resources/Game/ItemSpecification/RadiationResistanceResource.php new file mode 100644 index 000000000..34a108f2b --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/RadiationResistanceResource.php @@ -0,0 +1,41 @@ + Arr::get($this, 'MaximumRadiationCapacity'), + 'radiation_dissipation_rate' => Arr::get($this, 'RadiationDissipationRate'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/SalvageModifierResource.php b/app/Http/Resources/Game/ItemSpecification/SalvageModifierResource.php new file mode 100644 index 000000000..35b3d604f --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/SalvageModifierResource.php @@ -0,0 +1,40 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + $salvageModifier = Arr::get($stdItem, 'SalvageModifier', []); + + $speed = Arr::get($salvageModifier, 'SalvageSpeedMultiplier'); + $radius = Arr::get($salvageModifier, 'RadiusMultiplier'); + $efficiency = Arr::get($salvageModifier, 'ExtractionEfficiency'); + + return [ + 'salvage_speed_multiplier' => $speed, + 'radius_multiplier' => $radius, + 'extraction_efficiency' => $efficiency, + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/SeatResource.php b/app/Http/Resources/Game/ItemSpecification/SeatResource.php new file mode 100644 index 000000000..294a67b9e --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/SeatResource.php @@ -0,0 +1,98 @@ + Arr::get($this, 'SeatType'), + 'yaw' => $this->axisLimits(Arr::get($this, 'Yaw')), + 'pitch' => $this->axisLimits(Arr::get($this, 'Pitch')), + 'set_yaw_pitch_limits' => Arr::get($this, 'SetYawPitchLimits'), + 'has_ejection' => Arr::get($this, 'HasEjection'), + 'ejection' => $this->ejectionData($ejection), + ]; + } + + private function axisLimits(?array $axis): ?array + { + if (empty($axis)) { + return null; + } + + return [ + 'minimum' => Arr::get($axis, 'Minimum'), + 'maximum' => Arr::get($axis, 'Maximum'), + ]; + } + + private function hasEjection(?array $ejection): bool + { + return is_array($ejection) && $ejection !== []; + } + + private function ejectionData(?array $ejection): ?array + { + if (! $this->hasEjection($ejection)) { + return null; + } + + return [ + 'max_linear_velocity' => Arr::get($ejection, 'MaxLinearVelocity'), + 'max_linear_acceleration' => Arr::get($ejection, 'MaxLinearAcceleration'), + 'max_angular_velocity' => Arr::get($ejection, 'MaxAngularVelocity'), + 'max_angular_acceleration' => Arr::get($ejection, 'MaxAngularAcceleration'), + 'ejection_loop_time' => Arr::get($ejection, 'EjectionLoopTime'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/SelfDestructResource.php b/app/Http/Resources/Game/ItemSpecification/SelfDestructResource.php new file mode 100644 index 000000000..01a627054 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/SelfDestructResource.php @@ -0,0 +1,89 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $selfDestruct = Arr::get($data, 'stdItem.SelfDestruct', []); + + $time = Arr::get($selfDestruct, 'Time'); + + return [ + 'damage' => Arr::get($selfDestruct, 'Damage'), + 'radius' => Arr::get($selfDestruct, 'Radius'), + 'min_radius' => Arr::get($selfDestruct, 'MinRadius'), + 'phys_radius' => Arr::get($selfDestruct, 'PhysRadius'), + 'min_phys_radius' => Arr::get($selfDestruct, 'MinPhysRadius'), + 'time' => $time, + 'countdown' => $time, + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/ShieldControllerResource.php b/app/Http/Resources/Game/ItemSpecification/ShieldControllerResource.php new file mode 100644 index 000000000..1c3bbed6d --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/ShieldControllerResource.php @@ -0,0 +1,38 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $item = Arr::get($data, 'stdItem.ShieldController', []); + + return [ + 'face_type' => Arr::get($item, 'FaceType'), + 'max_reallocation' => Arr::get($item, 'MaxReallocation'), + 'reconfiguration_cooldown' => Arr::get($item, 'ReconfigurationCooldown'), + 'max_electrical_charge_damage_rate' => Arr::get($item, 'MaxElectricalChargeDamageRate'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/ShieldResource.php b/app/Http/Resources/Game/ItemSpecification/ShieldResource.php new file mode 100644 index 000000000..7932c985c --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/ShieldResource.php @@ -0,0 +1,225 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $shield = Arr::get($data, 'stdItem.Shield', []); + + $maxShieldHealth = Arr::get($shield, 'MaxShieldHealth'); + $maxShieldRegen = Arr::get($shield, 'MaxShieldRegen'); + $decayRatio = Arr::get($shield, 'DecayRatio'); + $downedDelay = Arr::get($shield, 'DownedDelay'); + $damagedDelay = Arr::get($shield, 'DamagedDelay'); + $absorptions = Arr::get($shield, 'Absorption', []); + + $reservePool = [ + 'regen_rate' => Arr::get($shield, 'ReservePool.MaxShieldRegen'), + 'regen_time' => Arr::get($shield, 'ReservePool.RegenerationTime'), + + 'initial_health_ratio' => Arr::get($shield, 'ReservePoolInitialHealthRatio'), + 'max_health_ratio' => Arr::get($shield, 'ReservePoolMaxHealthRatio'), + 'regen_rate_ratio' => Arr::get($shield, 'ReservePoolRegenRateRatio'), + 'drain_rate_ratio' => Arr::get($shield, 'ReservePoolDrainRateRatio'), + ]; + + return [ + 'max_health' => $maxShieldHealth, + 'regen_rate' => $maxShieldRegen, + 'regen_time' => round(Arr::get($shield, 'RegenerationTime', 0), 2), + 'decay_ratio' => $decayRatio, + 'reserve_pool' => $reservePool, + 'regen_delay' => [ + 'downed' => $downedDelay, + 'damage' => $damagedDelay, + ], + 'electrical_charge_damage_resistance' => Arr::get($shield, 'ElectricalChargeDamageResistance'), + + 'absorption' => $this->mapAbsorptions($absorptions), + 'resistance' => $this->mapAbsorptions(Arr::get($shield, 'Resistance', [])), + + // Deprecated v2 fields + 'max_shield_health' => $maxShieldHealth, + 'max_shield_regen' => $maxShieldRegen, + ]; + } + + private function mapAbsorptions(array $absorptions): ?array + { + if ($absorptions === []) { + return null; + } + + $keys = ['Physical', 'Energy', 'Distortion', 'Thermal', 'Biochemical', 'Stun']; + + $mapped = []; + + foreach ($keys as $key) { + if (! isset($absorptions[$key])) { + continue; + } + + $mapped[strtolower($key)] = [ + 'min' => Arr::get($absorptions, "{$key}.Minimum"), + 'max' => Arr::get($absorptions, "{$key}.Maximum"), + ]; + } + + return $mapped === [] ? null : $mapped; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/SuitArmorResource.php b/app/Http/Resources/Game/ItemSpecification/SuitArmorResource.php new file mode 100644 index 000000000..4902bc9f1 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/SuitArmorResource.php @@ -0,0 +1,236 @@ + 5, + 'infrared' => 2, + ], + nullable: true + ), + + new OA\Property( + property: 'temp_resistance_min', + description: 'Deprecated: Use temperature_resistance from root.', + type: 'double', + example: 2, + nullable: true, + deprecated: true + ), + new OA\Property( + property: 'temp_resistance_max', + description: 'Deprecated: Use temperature_resistance from root.', + type: 'double', + example: 10, + nullable: true, + deprecated: true + ), + + new OA\Property( + property: 'radiation_resistance', + ref: '#/components/schemas/radiation_resistance', + description: 'Radiation protection values from RadiationResistance.', + example: [ + 'maximum_radiation_capacity' => 26400, + 'radiation_dissipation_rate' => 145.8, + ], + nullable: true + ), + ], + type: 'object' +)] +class SuitArmorResource extends AbstractItemSpecificationResource +{ + public function toArray(Request $request): array + { + $data = $this->parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + $armor = Arr::get($stdItem, 'SuitArmor', []); + + $slot = $this->deriveSlot(); + + return [ + 'slot' => $slot, + 'armor_type' => $slot, + 'damage_resistance' => [ + 'impact' => Arr::get($armor, 'DamageResistance.Impact'), + 'physical' => $this->mapTypeResistance($armor, 'Physical'), + 'energy' => $this->mapTypeResistance($armor, 'Energy'), + 'distortion' => $this->mapTypeResistance($armor, 'Distortion'), + 'thermal' => $this->mapTypeResistance($armor, 'Thermal'), + 'biochemical' => $this->mapTypeResistance($armor, 'Biochemical'), + 'stun' => $this->mapTypeResistance($armor, 'Stun'), + ], + 'damage_resistance_map' => [ + 'impact' => Arr::get($armor, 'DamageResistance.Impact'), + 'impact_change' => Arr::get($armor, 'DamageResistance.Impact', 1) - 1, + 'physical' => Arr::get($this->mapTypeResistance($armor, 'Physical'), 'multiplier'), + 'physical_change' => Arr::get($this->mapTypeResistance($armor, 'Physical'), 'multiplier', 1) - 1, + 'energy' => Arr::get($this->mapTypeResistance($armor, 'Energy'), 'multiplier'), + 'energy_change' => Arr::get($this->mapTypeResistance($armor, 'Energy'), 'multiplier', 1) - 1, + 'distortion' => Arr::get($this->mapTypeResistance($armor, 'Distortion'), 'multiplier'), + 'distortion_change' => Arr::get($this->mapTypeResistance($armor, 'Distortion'), 'multiplier', 1) - 1, + 'thermal' => Arr::get($this->mapTypeResistance($armor, 'Thermal'), 'multiplier'), + 'thermal_change' => Arr::get($this->mapTypeResistance($armor, 'Thermal'), 'multiplier', 1) - 1, + 'biochemical' => Arr::get($this->mapTypeResistance($armor, 'Biochemical'), 'multiplier'), + 'biochemical_change' => Arr::get($this->mapTypeResistance($armor, 'Biochemical'), 'multiplier', 1) - 1, + 'stun' => Arr::get($this->mapTypeResistance($armor, 'Stun'), 'multiplier'), + 'stun_change' => Arr::get($this->mapTypeResistance($armor, 'Stun'), 'multiplier', 1) - 1, + ], + 'protected_body_parts' => Arr::get($armor, 'ProtectedBodyParts', []), + 'signature' => collect(Arr::get($armor, 'Signature', [])) + ->mapWithKeys(static fn ($value, $key) => [ + Str::snake($key) => $value, + ]) + ->toArray(), + 'temp_resistance_min' => Arr::get($stdItem, 'data.stdItem.TemperatureResistance.Minimum'), + 'temp_resistance_max' => Arr::get($stdItem, 'data.stdItem.TemperatureResistance.Maximum'), + 'radiation_resistance' => Arr::has($stdItem, 'RadiationResistance') + ? (new RadiationResistanceResource(Arr::get($stdItem, 'RadiationResistance')))->toArray($request) + : null, + ]; + } + + private function mapTypeResistance(array $armor, string $key): ?array + { + if (! Arr::has($armor, "DamageResistance.{$key}")) { + return null; + } + + return [ + 'type' => strtolower($key), + 'multiplier' => Arr::get($armor, "DamageResistance.{$key}.Multiplier"), + 'threshold' => Arr::get($armor, "DamageResistance.{$key}.Threshold"), + ]; + } + + private function deriveSlot(): ?string + { + $classification = $this->resource->classification ?? Arr::get($this->resource, 'classification'); + + if (! is_string($classification)) { + return null; + } + + $parts = explode('.', $classification); + + return $parts !== [] ? Arr::last($parts) : null; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/TemperatureResistanceResource.php b/app/Http/Resources/Game/ItemSpecification/TemperatureResistanceResource.php new file mode 100644 index 000000000..e04d8aa27 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/TemperatureResistanceResource.php @@ -0,0 +1,41 @@ + Arr::get($this, 'Minimum'), + 'maximum' => Arr::get($this, 'Maximum'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/ThrusterResource.php b/app/Http/Resources/Game/ItemSpecification/ThrusterResource.php new file mode 100644 index 000000000..2579ae9ff --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/ThrusterResource.php @@ -0,0 +1,161 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $stdItem = $this->extractStdItem($data); + $thruster = Arr::get($stdItem, 'Thruster', []); + + $performance = [ + 'thrust_capacity' => Arr::get($thruster, 'ThrustCapacity'), + 'thrust_capacity_new' => Arr::get($thruster, 'ThrustCapacityNew'), + 'max_supported_atmospheric_efficiency' => Arr::get($thruster, 'MaxSupportedAtmosphericEfficiency'), + 'min_health_thrust_multiplier' => Arr::get($thruster, 'MinHealthThrustMultiplier'), + ]; + + $fuel = [ + 'burn_rate_per_10k_newton' => Arr::get($thruster, 'FuelBurnRatePer10KNewton'), + ]; + + $backwash = [ + 'enabled' => Arr::get($thruster, 'ToggleThrusterBackwash'), + 'automate_size' => Arr::get($thruster, 'AutomateBackwashSize'), + 'max_speed' => Arr::get($thruster, 'ThrusterBackwashMaxSpeed'), + 'max_density' => Arr::get($thruster, 'ThrusterBackwashMaxDensity'), + 'max_resistance' => Arr::get($thruster, 'ThrusterBackwashMaxResistance'), + 'afterburner_multiplier' => Arr::get($thruster, 'ThrusterBackwashAfterburnerMultiplier'), + ]; + + $handling = [ + 'strength_smoothing' => Arr::get($thruster, 'ThrusterStrengthSmoothing'), + ]; + + return [ + 'performance' => $performance, + 'fuel' => $fuel, + 'role' => Arr::get($thruster, 'ThrusterType'), + 'vtol_only' => Arr::has($thruster, 'OnlyActiveInVTOL') ? (bool) Arr::get($thruster, 'OnlyActiveInVTOL') : null, + 'backwash' => $backwash, + 'handling' => $handling, + + // Legacy flat keys (backward compatibility) + 'thrust_capacity' => Arr::get($thruster, 'ThrustCapacity'), + 'min_health_thrust_multiplier' => Arr::get($thruster, 'MinHealthThrustMultiplier'), + 'fuel_burn_per_10k_newton' => Arr::get($thruster, 'FuelBurnRatePer10KNewton'), + 'type' => Arr::get($thruster, 'ThrusterType'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/TractorBeamResource.php b/app/Http/Resources/Game/ItemSpecification/TractorBeamResource.php new file mode 100644 index 000000000..29fd56d87 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/TractorBeamResource.php @@ -0,0 +1,274 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $stdItem = $this->extractStdItem($data); + $tractor = Arr::get($stdItem, 'TractorBeam', []); + + $force = [ + 'min' => Arr::get($tractor, 'MinForce'), + 'max' => Arr::get($tractor, 'MaxForce'), + 'max_volume' => Arr::get($tractor, 'MaxVolume'), + 'volume_force_coefficient' => Arr::get($tractor, 'VolumeForceCoefficient'), + ]; + + $range = [ + 'min' => Arr::get($tractor, 'MinDistance'), + 'max' => Arr::get($tractor, 'MaxDistance'), + 'full_strength_distance' => Arr::get($tractor, 'FullStrengthDistance'), + 'max_angle' => Arr::get($tractor, 'MaxAngle'), + 'hit_radius' => Arr::get($tractor, 'HitRadius'), + ]; + + $tether = [ + 'tether_break_time' => Arr::get($tractor, 'TetherBreakTime'), + 'safe_range_value_factor' => Arr::get($tractor, 'SafeRangeValueFactor'), + 'allow_scrolling_into_breaking_range' => Arr::has($tractor, 'AllowScrollingIntoBreakingRange') + ? (bool) Arr::get($tractor, 'AllowScrollingIntoBreakingRange') + : null, + ]; + + $cargoOverride = Arr::get($tractor, 'CargoModeOverride', []); + $cargoModeOverride = is_array($cargoOverride) ? [ + 'min_force' => Arr::get($cargoOverride, 'MinForceOverride'), + 'max_force' => Arr::get($cargoOverride, 'MaxForceOverride'), + 'min_acceleration' => Arr::get($cargoOverride, 'MinAccelerationOverride'), + 'max_acceleration' => Arr::get($cargoOverride, 'MaxAccelerationOverride'), + 'min_speed' => Arr::get($cargoOverride, 'MinSpeedOverride'), + 'max_speed' => Arr::get($cargoOverride, 'MaxSpeedOverride'), + 'acceleration_factor' => Arr::get($cargoOverride, 'AccelerationFactorOverride'), + 'degrees_per_action' => Arr::get($cargoOverride, 'DegreesPerActionOverride'), + 'max_angular_acceleration' => Arr::get($cargoOverride, 'MaxAngularAccelerationOverride'), + 'max_angular_velocity' => Arr::get($cargoOverride, 'MaxAngularVelocityOverride'), + 'degrees_per_action_scroll_wheel' => Arr::get($cargoOverride, 'DegreesPerActionScrollWheelOverride'), + 'force_fraction_rotation' => Arr::get($cargoOverride, 'ForceFractionRotationOverride'), + 'min_distance' => Arr::get($cargoOverride, 'MinDistanceOverride'), + 'max_distance' => Arr::get($cargoOverride, 'MaxDistanceOverride'), + 'full_strength_distance' => Arr::get($cargoOverride, 'FullStrengthDistanceOverride'), + ] : null; + + return [ + 'force' => $force, + 'range' => $range, + 'tether' => $tether, + + 'cargo_mode_override' => $cargoModeOverride, + + 'towing' => [ + 'force' => Arr::get($tractor, 'Towing.TowingForce'), + 'max_acceleration' => Arr::get($tractor, 'Towing.TowingMaxAcceleration'), + 'max_distance' => Arr::get($tractor, 'Towing.TowingMaxDistance'), + 'qt_mass_limit' => Arr::get($tractor, 'Towing.QuantumTowMassLimit'), + ], + + // Backward compatibility + 'min_force' => Arr::get($tractor, 'MinForce'), + 'max_force' => Arr::get($tractor, 'MaxForce'), + 'min_distance' => Arr::get($tractor, 'MinDistance'), + 'max_distance' => Arr::get($tractor, 'MaxDistance'), + 'full_strength_distance' => Arr::get($tractor, 'FullStrengthDistance'), + 'max_angle' => Arr::get($tractor, 'MaxAngle'), + 'max_volume' => Arr::get($tractor, 'MaxVolume'), + 'volume_force_coefficient' => Arr::get($tractor, 'VolumeForceCoefficient'), + 'tether_break_time' => Arr::get($tractor, 'TetherBreakTime'), + 'safe_range_value_factor' => Arr::get($tractor, 'SafeRangeValueFactor'), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/TurretResource.php b/app/Http/Resources/Game/ItemSpecification/TurretResource.php new file mode 100644 index 000000000..5fd795ae4 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/TurretResource.php @@ -0,0 +1,137 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + + $turret = Arr::get($data, 'stdItem.Turret', []); + + $yaw = collect(Arr::get($turret, 'MovementList', []))->firstWhere('JointName', 'yaw_part') ?? []; + $pitch = collect(Arr::get($turret, 'MovementList', []))->firstWhere('JointName', 'pitch_part') ?? []; + + $ports = collect($this->extractPorts($this->resource)); + + return [ + 'rotation_style' => Arr::get($turret, 'RotationStyle'), + + 'mounts' => $ports->count(), + 'min_size' => $ports->min('MinSize') ?? $ports->min('min_size'), + 'max_size' => $ports->max('MaxSize') ?? $ports->max('max_size'), + + 'yaw_axis' => [ + 'slaved_only' => Arr::get($yaw, 'YawAxis.SlavedOnly') === 1, + 'speed' => Arr::get($yaw, 'YawAxis.Speed'), + 'time_to_full_speed' => Arr::get($yaw, 'YawAxis.AccelerationTimeToFullSpeed'), + 'acceleration_decay' => Arr::get($yaw, 'YawAxis.AccelerationDecay'), + $this->mergeWhen(Arr::get($yaw, 'YawAxis.RestrictTargetAngles') === 1, [ + 'angle_limit_min' => Arr::get($yaw, 'YawAxis.AngleLimits.0.LowestAngle'), + 'angle_limit_max' => Arr::get($yaw, 'YawAxis.AngleLimits.0.HighestAngle'), + ]), + ], + 'pitch_axis' => [ + 'slaved_only' => Arr::get($pitch, 'PitchAxis.SlavedOnly') === 1, + 'speed' => Arr::get($pitch, 'PitchAxis.Speed'), + 'time_to_full_speed' => Arr::get($pitch, 'PitchAxis.AccelerationTimeToFullSpeed'), + 'acceleration_decay' => Arr::get($pitch, 'PitchAxis.AccelerationDecay'), + $this->mergeWhen(Arr::get($yaw, 'PitchAxis.RestrictTargetAngles') === 1, [ + 'angle_limit_min' => Arr::get($pitch, 'PitchAxis.AngleLimits.0.LowestAngle'), + 'angle_limit_max' => Arr::get($pitch, 'PitchAxis.AngleLimits.0.HighestAngle'), + ]), + ], + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/VehicleWeaponResource.php b/app/Http/Resources/Game/ItemSpecification/VehicleWeaponResource.php new file mode 100644 index 000000000..fb8b54c5a --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/VehicleWeaponResource.php @@ -0,0 +1,350 @@ +extractFromStdItem($this->resource, 'Ammunition'); + $weapon = $this->extractFromStdItem($this->resource, 'Weapon'); + $mode = Arr::get($weapon, 'Modes.0'); + $heat = Arr::get($weapon, 'Heat'); + + $damages = array_filter([ + ['type' => 'impact', 'name' => 'physical', 'damage' => Arr::get($ammo, 'ImpactDamage.Physical')], + ['type' => 'impact', 'name' => 'energy', 'damage' => Arr::get($ammo, 'ImpactDamage.Energy')], + ['type' => 'impact', 'name' => 'distortion', 'damage' => Arr::get($ammo, 'ImpactDamage.Distortion')], + ['type' => 'impact', 'name' => 'thermal', 'damage' => Arr::get($ammo, 'ImpactDamage.Thermal')], + ['type' => 'impact', 'name' => 'biochemical', 'damage' => Arr::get($ammo, 'ImpactDamage.Biochemical')], + ['type' => 'impact', 'name' => 'stun', 'damage' => Arr::get($ammo, 'ImpactDamage.Stun')], + + ['type' => 'detonation', 'name' => 'physical', 'damage' => Arr::get($ammo, 'DetonationDamage.Physical')], + ['type' => 'detonation', 'name' => 'energy', 'damage' => Arr::get($ammo, 'DetonationDamage.Energy')], + ['type' => 'detonation', 'name' => 'distortion', 'damage' => Arr::get($ammo, 'DetonationDamage.Distortion')], + ['type' => 'detonation', 'name' => 'thermal', 'damage' => Arr::get($ammo, 'DetonationDamage.Thermal')], + ['type' => 'detonation', 'name' => 'biochemical', 'damage' => Arr::get($ammo, 'DetonationDamage.Biochemical')], + ['type' => 'detonation', 'name' => 'stun', 'damage' => Arr::get($ammo, 'DetonationDamage.Stun')], + ], static fn (array $entry) => $entry !== [] && ! empty($entry['damage'])); + + $modes = collect(Arr::get($weapon, 'Modes', [])) + ->map(static fn (mixed $mode): array => [ + 'mode' => Arr::get($mode, 'Name'), + 'localised' => Arr::get($mode, 'LocalisedName'), + 'type' => Arr::get($mode, 'FireType'), + 'rounds_per_minute' => Arr::get($mode, 'RoundsPerMinute'), + 'ammo_per_shot' => Arr::get($mode, 'AmmoPerShot'), + 'pellets_per_shot' => Arr::get($mode, 'PelletsPerShot'), + 'damage_per_second' => Arr::get($mode, 'DamagePerSecond'), + ]) + ->values() + ->toArray(); + + return [ + 'class' => Arr::get($weapon, 'WeaponClass'), + 'type' => $this->extractFromStdItem($this->resource, 'DescriptionData.Item Type'), + 'capacity' => Arr::get($ammo, 'Capacity'), + 'range' => Arr::get($weapon, 'EffectiveRange'), + + // deprecated + 'damage_per_shot' => Arr::get($mode, 'Alpha'), + 'regeneration' => Arr::get($weapon, 'Capacitor.MaxRegenPerSec'), + + 'rpm' => Arr::get($mode, 'RoundsPerMinute'), + + 'damages' => $damages, + 'modes' => $modes, + + 'damage' => [ + 'sustained_60s' => Arr::get($weapon, 'Damage.Sustained60s'), + 'burst' => Arr::get($weapon, 'Damage.Burst'), + 'alpha_total' => Arr::get($weapon, 'Damage.Alpha'), + 'maximum' => Arr::get($weapon, 'Damage.Maximum'), + 'dps' => [ + 'physical' => Arr::get($mode, 'DpsPhysical'), + 'energy' => Arr::get($mode, 'DpsEnergy'), + 'distortion' => Arr::get($mode, 'DpsDistortion'), + 'thermal' => Arr::get($mode, 'DpsThermal'), + 'biochemical' => Arr::get($mode, 'DpsBiochemical'), + 'stun' => Arr::get($mode, 'DpsStun'), + ], + 'alpha' => [ + 'physical' => Arr::get($mode, 'AlphaPhysical'), + 'energy' => Arr::get($mode, 'AlphaEnergy'), + 'distortion' => Arr::get($mode, 'AlphaDistortion'), + 'thermal' => Arr::get($mode, 'AlphaThermal'), + 'biochemical' => Arr::get($mode, 'AlphaBiochemical'), + 'stun' => Arr::get($mode, 'AlphaStun'), + ], + ], + + $this->mergeWhen(Arr::get($mode, 'Spread') !== null, [ + 'spread' => [ + 'minimum' => Arr::get($mode, 'Spread.Min'), + 'maximum' => Arr::get($mode, 'Spread.Max'), + 'first_attack' => Arr::get($mode, 'Spread.FirstAttack'), + 'per_attack' => Arr::get($mode, 'Spread.Attack'), + 'decay' => Arr::get($mode, 'Spread.Decay'), + ], + ]), + + $this->mergeWhen(Arr::get($mode, 'BarrelSpinTime') !== null, [ + 'barrel_spin_time' => [ + 'up' => Arr::get($mode, 'BarrelSpinTime.Up'), + 'down' => Arr::get($mode, 'BarrelSpinTime.Down'), + ], + ]), + + $this->mergeWhen(! empty($heat), [ + 'heat' => [ + 'per_shot' => Arr::get($heat, 'HeatPerShot'), + 'cooling_delay' => Arr::get($heat, 'CoolingDelay'), + 'cooling_per_second' => Arr::get($heat, 'CoolingPerSecond'), + 'overheat_max_shots' => Arr::get($heat, 'ShotsToOverheat'), + 'overheat_max_time' => Arr::get($heat, 'TimeToOverheat'), + 'overheat_cooldown' => Arr::get($heat, 'OverheatFixTime'), + ], + ]), + + $this->mergeWhen(Arr::get($weapon, 'Capacitor.MaxAmmoLoad') !== null, [ + 'capacitor' => [ + 'max_ammo_load' => Arr::get($weapon, 'Capacitor.MaxAmmoLoad'), + 'regen_per_second' => Arr::get($weapon, 'Capacitor.MaxRegenPerSec'), + 'cooldown' => Arr::get($weapon, 'Capacitor.Cooldown'), + + 'requested_ammo_load' => Arr::get($weapon, 'Capacitor.RequestedAmmoLoad'), + 'costs_per_shot' => Arr::get($weapon, 'Capacitor.CostPerBullet'), + ], + ]), + + $this->mergeWhen(Arr::get($mode, 'Charge') !== null, [ + 'charge' => [ + 'time' => Arr::get($mode, 'Charge.ChargeTime'), + 'overcharge_time' => Arr::get($mode, 'Charge.OverchargeTime'), + 'overcharged_time' => Arr::get($mode, 'Charge.OverchargedTime'), + 'cooldown_time' => Arr::get($mode, 'Charge.CooldownTime'), + ], + 'charge_modifier' => [ + 'damage' => Arr::get($mode, 'ChargeModifier.Damage'), + 'fire_rate' => Arr::get($mode, 'ChargeModifier.FireRate'), + 'ammo_speed' => Arr::get($mode, 'ChargeModifier.AmmoSpeed'), + ], + ]), + + 'ammunition' => new AmmunitionResource($this->resource), + ]; + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/WeaponAttachmentResource.php b/app/Http/Resources/Game/ItemSpecification/WeaponAttachmentResource.php new file mode 100644 index 000000000..0115d18cc --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/WeaponAttachmentResource.php @@ -0,0 +1,194 @@ +parseSpecificationData($this->resource['data'] ?? $this->resource->data ?? null); + $stdItem = $this->extractStdItem($data); + $weaponAttachment = Arr::get($stdItem, 'WeaponAttachment', []); + + $ironSight = Arr::get($weaponAttachment, 'IronSight', []); + $ironSightData = $ironSight !== [] ? [ + 'default_range' => Arr::get($ironSight, 'DefaultRange'), + 'max_range' => Arr::get($ironSight, 'MaxRange'), + 'range_increment' => Arr::get($ironSight, 'RangeIncrement'), + 'auto_zeroing_time' => Arr::get($ironSight, 'AutoZeroingTime'), + 'zoom_scale' => Arr::get($ironSight, 'ZoomScale'), + 'zoom_time_scale' => Arr::get($ironSight, 'ZoomTimeScale'), + 'zoom_time_change' => 1 - Arr::get($ironSight, 'ZoomTimeScale', 1), + ] : null; + + $magazine = Arr::get($weaponAttachment, 'Magazine', []); + $magazineData = $magazine !== [] ? [ + 'initial_ammo_count' => Arr::get($magazine, 'InitialAmmoCount'), + 'max_ammo_count' => Arr::get($magazine, 'MaxAmmoCount'), + 'max_restock_count' => Arr::get($magazine, 'MaxRestockCount'), + ] : null; + + $laserPointer = Arr::get($weaponAttachment, 'LaserPointer', []); + $laserPointerData = $laserPointer !== [] ? [ + 'range' => Arr::get($laserPointer, 'Range'), + 'color' => Arr::get($laserPointer, 'Color') ? [ + 'r' => Arr::get($laserPointer, 'Color.R'), + 'g' => Arr::get($laserPointer, 'Color.G'), + 'b' => Arr::get($laserPointer, 'Color.B'), + ] : null, + 'color_css' => Arr::get($laserPointer, 'ColorCss'), + ] : null; + + $flashLightData = collect(Arr::get($weaponAttachment, 'Flashlight', [])) + ->mapWithKeys(fn ($value) => [ + str_contains($value['ClassName'], 'narrow') ? 'narrow' : 'wide' => [ + 'port_name' => Arr::get($value, 'PortName'), + 'name' => Arr::get($value, 'Name'), + 'light_type' => Arr::get($value, 'LightType'), + 'light_radius' => Arr::get($value, 'LightRadius'), + 'intensity' => Arr::get($value, 'Intensity'), + 'color' => Arr::get($value, 'Color') ? [ + 'r' => Arr::get($value, 'Color.R'), + 'g' => Arr::get($value, 'Color.G'), + 'b' => Arr::get($value, 'Color.B'), + ] : null, + 'color_css' => Arr::get($value, 'ColorCss'), + ], + ]) + ->toArray(); + + $barrelAttachment = Arr::get($weaponAttachment, 'Barrel', []); + $barrelAttachmentType = Arr::get($barrelAttachment, 'Type'); + + $key = $barrelAttachmentType === 'Compensator' + ? 'compensator' + : ($barrelAttachmentType === 'Flash Hider' ? 'flash_hider' : null); + + $out = [ + 'iron_sight' => $ironSightData, + 'laser_pointer' => $laserPointerData, + 'flashlight' => $flashLightData, + 'magazine' => $magazineData, + ]; + + if ($key) { + $out[$key] = collect($barrelAttachment) + ->mapWithKeys(fn ($value, $key) => [Str::snake($key) => $value]) + ->put('attachment_point', Arr::get($weaponAttachment, 'AttachmentPoint')) + ->put('type', Arr::get($weaponAttachment, 'AttachmentPoint')) + ->toArray(); + } + + return array_filter($out, static fn ($value) => ! empty($value)); + } +} diff --git a/app/Http/Resources/Game/ItemSpecification/WeaponModifierResource.php b/app/Http/Resources/Game/ItemSpecification/WeaponModifierResource.php new file mode 100644 index 000000000..631e93017 --- /dev/null +++ b/app/Http/Resources/Game/ItemSpecification/WeaponModifierResource.php @@ -0,0 +1,337 @@ +extractFromStdItem($this->resource, 'WeaponModifier', []); + $weaponStats = Arr::get($weaponModifier, 'WeaponStats', []); + $base = Arr::get($weaponStats, 'Base', []); + $recoil = Arr::get($weaponStats, 'Recoil', []); + $spread = Arr::get($weaponStats, 'Spread', []); + $aim = Arr::get($weaponStats, 'Aim', []); + $regen = Arr::get($weaponStats, 'Regen', []); + $salvage = Arr::get($weaponStats, 'Salvage', []); + $zeroing = Arr::get($weaponModifier, 'Zeroing', []); + + return [ + 'activate_on_attach' => Arr::get($weaponModifier, 'ActivateOnAttach'), + 'ignore_wear' => Arr::get($weaponModifier, 'IgnoreWear'), + + $this->mergeWhen(! collect($base)->reject(fn ($val) => $val == 0)->every(fn ($val) => (float) $val === 1.0), [ + 'base' => [ + 'muzzle_flash_multiplier' => Arr::get($base, 'MuzzleFlashScale'), + 'muzzle_flash_change' => round(Arr::get($base, 'MuzzleFlashScale', 1) - 1, 2), + + 'fire_rate_multiplier' => Arr::get($base, 'FireRateMultiplier'), + 'fire_rate_change' => round(Arr::get($base, 'FireRateMultiplier', 1) - 1, 2), + + 'damage_multiplier' => Arr::get($base, 'DamageMultiplier'), + 'damage_change' => round(Arr::get($base, 'DamageMultiplier', 1) - 1, 2), + + 'projectile_speed_multiplier' => Arr::get($base, 'ProjectileSpeedMultiplier'), + 'projectile_speed_change' => round(Arr::get($base, 'ProjectileSpeedMultiplier', 1) - 1, 2), + + 'ammo_cost_multiplier' => Arr::get($base, 'AmmoCostMultiplier'), + 'ammo_cost_change' => round(Arr::get($base, 'AmmoCostMultiplier', 1) - 1, 2), + + 'heat_generation_multiplier' => Arr::get($base, 'HeatGenerationMultiplier'), + 'heat_generation_change' => round(Arr::get($base, 'HeatGenerationMultiplier', 1) - 1, 2), + + 'sound_radius_multiplier' => Arr::get($base, 'SoundRadiusMultiplier'), + 'sound_radius_change' => round(Arr::get($base, 'SoundRadiusMultiplier', 1) - 1, 2), + + 'charge_time_multiplier' => Arr::get($base, 'ChargeTimeMultiplier'), + 'charge_time_change' => round(Arr::get($base, 'ChargeTimeMultiplier', 1) - 1, 2), + ], + ]), + + $this->mergeWhen(! collect($recoil)->reject(fn ($val) => $val == 0)->every(fn ($val) => (float) $val === 1.0), [ + 'recoil' => [ + 'decay_multiplier' => Arr::get($recoil, 'DecayMultiplier'), + 'decay_change' => round(Arr::get($recoil, 'DecayMultiplier', 1) - 1, 2), + + 'multiplier' => Arr::get($recoil, 'RandomnessMultiplier'), + 'multiplier_change' => round(Arr::get($recoil, 'RandomnessMultiplier', 1) - 1, 2), + ], + ]), + + $this->mergeWhen(! collect($spread)->reject(fn ($val) => $val == 0)->every(fn ($val) => (float) $val === 1.0), [ + 'spread' => [ + 'min_multiplier' => Arr::get($spread, 'MinMultiplier'), + 'min_change' => round(Arr::get($spread, 'MinMultiplier', 1) - 1, 2), + + 'max_multiplier' => Arr::get($spread, 'MaxMultiplier'), + 'max_change' => round(Arr::get($spread, 'MaxMultiplier', 1) - 1, 2), + + 'first_attack_multiplier' => Arr::get($spread, 'FirstAttackMultiplier'), + 'first_attack_change' => round(Arr::get($spread, 'FirstAttackMultiplier', 1) - 1, 2), + + 'per_attack_multiplier' => Arr::get($spread, 'AttackMultiplier'), + 'per_attack_change' => round(Arr::get($spread, 'AttackMultiplier', 1) - 1, 2), + + 'decay_multiplier' => Arr::get($spread, 'DecayMultiplier'), + 'decay_change' => round(Arr::get($spread, 'DecayMultiplier', 1) - 1, 2), + ], + ]), + + $this->mergeWhen(! collect($aim)->reject(fn ($val) => $val == 0)->every(fn ($val) => (float) $val === 1.0), [ + 'aim' => [ + 'zoom_scale' => Arr::get($aim, 'ZoomScale'), + 'second_zoom_scale' => Arr::get($aim, 'SecondZoomScale'), + 'zoom_time_scale' => Arr::get($aim, 'ZoomTimeScale'), + 'zoom_time_change' => round(Arr::get($aim, 'ZoomTimeScale', 1) - 1, 2), + 'hide_weapon_in_ads' => Arr::get($aim, 'HideWeaponInAds'), + 'fstop_multiplier' => Arr::get($aim, 'FstopMultiplier'), + ], + ]), + + $this->mergeWhen(! collect($regen)->reject(fn ($val) => $val == 0)->every(fn ($val) => (float) $val === 1.0), [ + 'regen' => [ + 'power_ratio_multiplier' => Arr::get($regen, 'PowerRatioMultiplier'), + 'max_ammo_load_multiplier' => Arr::get($regen, 'MaxAmmoLoadMultiplier'), + 'max_regen_per_sec_multiplier' => Arr::get($regen, 'MaxRegenPerSecMultiplier'), + ], + ]), + + $this->mergeWhen(! collect($regen)->reject(fn ($val) => $val == 0)->every(fn ($val) => (float) $val === 1.0), [ + 'salvage' => [ + 'salvage_speed_multiplier' => Arr::get($salvage, 'SalvageSpeedMultiplier'), + 'radius_multiplier' => Arr::get($salvage, 'RadiusMultiplier'), + 'extraction_efficiency' => Arr::get($salvage, 'ExtractionEfficiency'), + ], + ]), + + $this->mergeWhen(collect($zeroing)->reject(fn ($item) => $item !== null)->isNotEmpty(), [ + 'zeroing' => [ + 'default_range' => Arr::get($zeroing, 'DefaultRange'), + 'max_range' => Arr::get($zeroing, 'MaxRange'), + 'range_increment' => Arr::get($zeroing, 'RangeIncrement'), + 'auto_zeroing_time' => Arr::get($zeroing, 'AutoZeroingTime'), + ], + ]), + + 'fire_rate_multiplier' => Arr::get($base, 'FireRateMultiplier'), + 'damage_multiplier' => Arr::get($base, 'DamageMultiplier'), + 'damage_over_time_multiplier' => Arr::get($base, 'DamageOverTimeMultiplier'), + 'projectile_speed_multiplier' => Arr::get($base, 'ProjectileSpeedMultiplier'), + 'ammo_cost_multiplier' => Arr::get($base, 'AmmoCostMultiplier'), + 'heat_generation_multiplier' => Arr::get($base, 'HeatGenerationMultiplier'), + 'sound_radius_multiplier' => Arr::get($base, 'SoundRadiusMultiplier'), + 'charge_time_multiplier' => Arr::get($base, 'ChargeTimeMultiplier'), + ]; + } +} diff --git a/app/Http/Resources/Game/Manufacturer/ManufacturerLinkResource.php b/app/Http/Resources/Game/Manufacturer/ManufacturerLinkResource.php new file mode 100644 index 000000000..e4f80ebbf --- /dev/null +++ b/app/Http/Resources/Game/Manufacturer/ManufacturerLinkResource.php @@ -0,0 +1,33 @@ + $this->name, + 'code' => $this->code, + 'uuid' => $this->uuid, + 'link' => route('manufacturers.show', ['manufacturer' => $this->name]), + ]; + } +} diff --git a/app/Http/Resources/Game/Manufacturer/ManufacturerResource.php b/app/Http/Resources/Game/Manufacturer/ManufacturerResource.php new file mode 100644 index 000000000..33a030c1d --- /dev/null +++ b/app/Http/Resources/Game/Manufacturer/ManufacturerResource.php @@ -0,0 +1,59 @@ + $this->name, + 'code' => $this->code, + 'uuid' => $this->uuid, + // TODO + // 'ships' => VehicleLinkResource::collection($this->ships()), + // 'vehicles' => VehicleLinkResource::collection($this->groundVehicles()), + // 'items' => ItemLinkResource::collection($this->items()), + ]; + } +} diff --git a/app/Http/Resources/Game/Vehicle/CargoGridResource.php b/app/Http/Resources/Game/Vehicle/CargoGridResource.php new file mode 100644 index 000000000..3f1de30d4 --- /dev/null +++ b/app/Http/Resources/Game/Vehicle/CargoGridResource.php @@ -0,0 +1,83 @@ + Arr::get($this->resource, 'UUID'), + 'class_name' => Arr::get($this->resource, 'Class'), + 'scu' => Arr::get($this->resource, 'SCU'), + 'capacity' => Arr::get($this->resource, 'Capacity'), + 'capacity_name' => Arr::get($this->resource, 'CapacityName'), + 'is_open' => Arr::get($this->resource, 'IsOpenContainer'), + 'is_external' => Arr::get($this->resource, 'IsExternalContainer'), + 'is_closed' => Arr::get($this->resource, 'IsClosedContainer'), + 'x' => Arr::get($this->resource, 'X'), + 'y' => Arr::get($this->resource, 'Y'), + 'z' => Arr::get($this->resource, 'Z'), + 'min_size' => $this->sizeBlock('MinSize'), + 'max_size' => $this->sizeBlock('MaxSize'), + ], static fn ($value) => $value !== null && $value !== []); + } + + private function sizeBlock(string $key): ?array + { + $data = Arr::get($this->resource, $key); + + if (! is_array($data)) { + return null; + } + + $block = array_filter([ + 'x' => Arr::get($data, 'X'), + 'y' => Arr::get($data, 'Y'), + 'z' => Arr::get($data, 'Z'), + ], static fn ($value) => $value !== null); + + return $block === [] ? null : $block; + } +} diff --git a/app/Http/Resources/Game/Vehicle/Concerns/ProcessesHardpointData.php b/app/Http/Resources/Game/Vehicle/Concerns/ProcessesHardpointData.php new file mode 100644 index 000000000..80a02a511 --- /dev/null +++ b/app/Http/Resources/Game/Vehicle/Concerns/ProcessesHardpointData.php @@ -0,0 +1,65 @@ +resource, 'UUID'); + + if (! $hasEquippedItem) { + return null; + } + + $uuid = Arr::get($this->resource, 'UUID', []); + + if (! is_string($uuid) || $uuid === '') { + return null; + } + + return $this->loadItemForVersion($uuid); + } + + protected function extractHealth(?Item $resolvedItem): ?float + { + if ($resolvedItem === null) { + return null; + } + + return $this->extractFromStdItem($resolvedItem->data?->first(), 'Durability.Health'); + } + + protected function buildCompatibleTypes(): array + { + return array_map( + static fn ($type) => [ + 'type' => Arr::get($type, 'Type'), + 'subtype' => [Arr::get($type, 'SubType')], + ], + Arr::get($this->resource, 'ItemTypes', []) + ); + } + + protected function shouldIncludeChildren(): bool + { + return Arr::has($this->resource, 'Loadout'); + } + + protected function getChildrenArray(): array + { + return Arr::get($this->resource, 'Loadout', []); + } + + protected function extractTypeAndSubtype(): array + { + [$type, $subtype] = explode('.', Arr::get($this->resource, 'Type', '.')); + + return [$type, $subtype]; + } +} diff --git a/app/Http/Resources/Game/Vehicle/HardpointItemResource.php b/app/Http/Resources/Game/Vehicle/HardpointItemResource.php new file mode 100644 index 000000000..6d627eb83 --- /dev/null +++ b/app/Http/Resources/Game/Vehicle/HardpointItemResource.php @@ -0,0 +1,91 @@ +uuid === null) { + return []; + } + + $itemData = $this->data->first(); + + if ($itemData === null) { + return []; + } + + return [ + 'uuid' => $this->uuid, + 'name' => $itemData->name, + 'class_name' => $itemData->class_name, + 'link' => route('items.show', ['identifier' => $this->uuid]), + 'size' => $itemData->size, + 'mass' => $this->extractNumeric($itemData, 'Mass'), + 'grade' => match ($itemData->grade) { + 1 => 'A', + 2 => 'B', + 3 => 'C', + 4 => 'D', + default => $itemData->grade, + }, + 'class' => $itemData->class, + 'manufacturer' => new ManufacturerLinkResource($itemData->manufacturer), + 'type' => str_replace('NOITEM_', '', ($itemData->type ?? '')), + 'sub_type' => $itemData->sub_type, + $this->mergeWhen($this->isTurret($itemData), fn () => $this->addTurretData($itemData)), + $this->mergeWhen(...$this->addSpecification($this->resource, $itemData)), + $this->mergeWhen($this->hasInStdItem($itemData, 'InventoryContainer'), [ + 'inventory' => new ItemInventoryResource($this->extractFromStdItem($itemData, 'InventoryContainer')), + ]), + 'ports' => ItemPortResource::collection($this->when($this->hasInStdItem($itemData, 'Ports'), $this->extractPorts($itemData))), + 'updated_at' => $this->updated_at, + 'version' => $itemData->gameVersion->code, + ]; + } +} diff --git a/app/Http/Resources/Game/Vehicle/HardpointResource.php b/app/Http/Resources/Game/Vehicle/HardpointResource.php new file mode 100644 index 000000000..eb06468af --- /dev/null +++ b/app/Http/Resources/Game/Vehicle/HardpointResource.php @@ -0,0 +1,85 @@ +routeIs('vehicles.show')) { + $resolvedItem = $this->loadEquippedItem(); + } + + $health = $this->extractHealth($resolvedItem); + [$type, $subtype] = $this->extractTypeAndSubtype(); + + $data = [ + 'name' => Arr::get($this, 'HardpointName'), + 'position' => Arr::get($this, 'Position'), + 'class_name' => Arr::get($this, 'ClassName'), + 'min_size' => Arr::get($this, 'MinSize'), + 'max_size' => Arr::get($this, 'MaxSize'), + 'health' => $health, + 'type' => $type, + 'sub_type' => $subtype, + ]; + + if ($resolvedItem !== null) { + $data['item'] = new HardpointItemResource($resolvedItem); + } + + if ($this->shouldIncludeChildren()) { + $data['children'] = self::collection($this->getChildrenArray()); + } + + return array_filter( + $data, + static fn ($value) => $value !== null && $value !== [], + ARRAY_FILTER_USE_BOTH + ); + } +} diff --git a/app/Http/Resources/Game/Vehicle/PartResource.php b/app/Http/Resources/Game/Vehicle/PartResource.php new file mode 100644 index 000000000..e63870bda --- /dev/null +++ b/app/Http/Resources/Game/Vehicle/PartResource.php @@ -0,0 +1,86 @@ +resource, 'Name'); + + return [ + 'name' => $name, + 'display_name' => $this->generateDisplayName($name), + 'damage_max' => Arr::get($this->resource, 'DamageMax'), + $this->mergeWhen(Arr::has($this->resource, 'Children'), [ + 'children' => self::collection(Arr::get($this->resource, 'Children', [])), + ]), + ]; + } + + /** + * Generates a display name for a part by extracting positional prefixes. + * + * This method replicates the logic from v2/app/Models/SC/Vehicle/VehiclePart.php + * to ensure consistent display name generation across API versions. + * + * Examples: + * - "LEFT_WING" → "Wing (left)" + * - "FRONT_MID_LOWER_WING" → "Wing (front mid lower)" + * - "NOSE" → "Nose" + * - "LEFT" → "Left" (position as entire name) + */ + private function generateDisplayName(?string $name): ?string + { + if ($name === null) { + return null; + } + + $cleaned = strtolower(Str::replace('_', ' ', $name)); + + preg_match( + '/((left|right|tail|top|bottom|front|mid_|lower|upper|back|rear)_?)+/', + strtolower($name), + $matches + ); + + if (isset($matches[0]) && $matches[0] !== strtolower($name)) { + $partName = trim(str_replace('_', ' ', str_replace($matches[0], '', strtolower($name)))); + $position = trim(str_replace('_', ' ', $matches[0])); + + return Str::ucfirst(sprintf('%s (%s)', $partName, $position)); + } + + return Str::ucfirst($cleaned); + } +} diff --git a/app/Http/Resources/Game/Vehicle/PortItemResource.php b/app/Http/Resources/Game/Vehicle/PortItemResource.php new file mode 100644 index 000000000..891c1beaf --- /dev/null +++ b/app/Http/Resources/Game/Vehicle/PortItemResource.php @@ -0,0 +1,54 @@ +data?->first(); + + if ($itemData === null) { + return []; + } + + return [ + 'uuid' => $this->uuid, + 'name' => $itemData->name, + 'class_name' => $itemData->class_name, + 'type' => $itemData->type, + 'sub_type' => $itemData->sub_type, + 'link' => route('items.show', ['identifier' => $this->uuid]), + 'size' => $itemData->size, + 'mass' => $this->extractFromStdItem($itemData, 'Mass'), + 'grade' => match ($itemData->grade) { + 1 => 'A', + 2 => 'B', + 3 => 'C', + 4 => 'D', + default => $this->extractFromStdItem($itemData, 'DescriptionData.Grade') ?? $itemData->grade, + }, + 'class' => $itemData->class ?? $this->extractFromStdItem($itemData, 'DescriptionData.Class'), + + $this->mergeWhen($itemData->manufacturer !== null, [ + 'manufacturer' => new ManufacturerLinkResource($itemData->manufacturer), + ]), + + $this->mergeWhen(...$this->addSpecification($this->resource, $itemData)), + 'version' => $itemData->gameVersion->code, + ]; + } +} diff --git a/app/Http/Resources/Game/Vehicle/PortResource.php b/app/Http/Resources/Game/Vehicle/PortResource.php new file mode 100644 index 000000000..87715963b --- /dev/null +++ b/app/Http/Resources/Game/Vehicle/PortResource.php @@ -0,0 +1,118 @@ +routeIs('vehicles.show')) { + $resolvedItem = $this->loadEquippedItem(); + } + + $health = $this->extractHealth($resolvedItem); + [$type, $subtype] = $this->extractTypeAndSubtype(); + $compatibleTypes = $this->buildCompatibleTypes(); + + $minSize = Arr::get($this, 'MinSize'); + $maxSize = Arr::get($this, 'MaxSize'); + + $data = [ + 'name' => Arr::get($this, 'HardpointName'), + 'position' => Arr::get($this, 'Position'), + 'class_name' => Arr::get($this, 'ClassName'), + 'editable' => Arr::get($this, 'Editable'), + 'editable_children' => Arr::get($this, 'EditableChildren'), + 'equipped_item_uuid' => Arr::get($this->resource, 'UUID'), + 'type' => $type, + 'subtype' => $subtype, + ]; + + if ($minSize !== null || $maxSize !== null) { + $data['sizes'] = [ + 'min' => $minSize, + 'max' => $maxSize, + ]; + } + + if ($compatibleTypes !== []) { + $data['compatible_types'] = $compatibleTypes; + } + + if ($health !== null) { + $data['health'] = $health; + } + + if ($resolvedItem !== null) { + $data['equipped_item'] = new PortItemResource($resolvedItem); + } + + if ($this->shouldIncludeChildren()) { + $data['ports'] = self::collection($this->getChildrenArray()); + } + + return array_filter( + $data, + static fn ($value) => $value !== null && $value !== [], + ARRAY_FILTER_USE_BOTH + ); + } +} diff --git a/app/Http/Resources/Game/Vehicle/TurretSummaryResource.php b/app/Http/Resources/Game/Vehicle/TurretSummaryResource.php new file mode 100644 index 000000000..3b7063ae5 --- /dev/null +++ b/app/Http/Resources/Game/Vehicle/TurretSummaryResource.php @@ -0,0 +1,38 @@ + Arr::get($this->resource, 'Size'), + 'fixed' => Arr::get($this->resource, 'Fixed'), + 'weapon_sizes' => Arr::get($this->resource, 'WeaponSizes'), + ], static fn ($value) => $value !== null && $value !== []); + } +} diff --git a/app/Http/Resources/Game/Vehicle/VehicleLinkCollection.php b/app/Http/Resources/Game/Vehicle/VehicleLinkCollection.php new file mode 100644 index 000000000..2e6f8ec80 --- /dev/null +++ b/app/Http/Resources/Game/Vehicle/VehicleLinkCollection.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/Game/Vehicle/VehicleLinkResource.php b/app/Http/Resources/Game/Vehicle/VehicleLinkResource.php new file mode 100644 index 000000000..b0eb719bc --- /dev/null +++ b/app/Http/Resources/Game/Vehicle/VehicleLinkResource.php @@ -0,0 +1,58 @@ +data->first(); + + return [ + 'uuid' => $this->uuid, + 'name' => $data->name, + 'class_name' => $data->class_name, + 'career' => $data->career, + 'role' => $data->role, + 'size' => $data->size, + 'is_vehicle' => $data->is_vehicle, + 'is_gravlev' => $data->is_gravlev, + 'is_spaceship' => $data->is_spaceship, + 'manufacturer' => new ManufacturerLinkResource($data->manufacturer), + 'link' => route('vehicles.show', ['vehicle' => $this->uuid ?? $data->name]), + 'updated_at' => $this->updated_at, + 'version' => $data->gameVersion->code, + ]; + } +} diff --git a/app/Http/Resources/Game/Vehicle/VehicleResource.php b/app/Http/Resources/Game/Vehicle/VehicleResource.php new file mode 100644 index 000000000..e0661621b --- /dev/null +++ b/app/Http/Resources/Game/Vehicle/VehicleResource.php @@ -0,0 +1,816 @@ + damage capacity before destruction.', + type: 'object', + example: ['Nose' => 2500, 'Body' => 2500], + nullable: true + ), + new OA\Property( + property: 'before_detach', + description: 'Map of part name => damage before detachment.', + type: 'object', + example: ['WingTipLeft' => 1000, 'WingTipRight' => 1000], + nullable: true + ), + ], + type: 'object', + nullable: true + ), + new OA\Property( + property: 'ports', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/game_vehicle_port'), + nullable: true + ), + new OA\Property( + property: 'parts', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/game_vehicle_part'), + nullable: true + ), + new OA\Property( + property: 'turrets', + properties: [ + new OA\Property(property: 'manned', type: 'array', items: new OA\Items(ref: '#/components/schemas/game_vehicle_turret'), nullable: true), + new OA\Property(property: 'remote', type: 'array', items: new OA\Items(ref: '#/components/schemas/game_vehicle_turret'), nullable: true), + ], + type: 'object', + nullable: true + ), + new OA\Property( + property: 'item_description_data', + description: 'Description data from the vehicle\'s ItemData. Returns null when no matching ItemData exists.', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/item_description_data'), + nullable: true + ), + new OA\Property( + property: 'description_data', + description: 'Key/value pairs parsed from the description block in the game files.', + type: 'object', + example: ['Manufacturer' => 'Aegis Dynamics', 'Focus' => 'Light Freight'], + nullable: true + ), + new OA\Property(property: 'career', type: 'string', example: 'Light Freight', nullable: true), + new OA\Property(property: 'role', type: 'string', example: 'Combat', nullable: true), + new OA\Property(property: 'description', type: 'string', nullable: true), + new OA\Property( + property: 'id', + description: 'Ship-Matrix CIG ID', + type: 'integer', + nullable: true + ), + new OA\Property( + property: 'chassis_id', + description: 'Ship-Matrix chassis ID', + type: 'integer', + nullable: true + ), + new OA\Property( + property: 'shipmatrix_name', + description: 'Ship-Matrix vehicle name', + type: 'string', + nullable: true + ), + new OA\Property( + property: 'foci', + description: 'Ship-Matrix vehicle foci/roles', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/translation'), + nullable: true + ), + new OA\Property( + property: 'production_status', + ref: '#/components/schemas/translation', + description: 'Ship-Matrix production status', + nullable: true + ), + new OA\Property( + property: 'production_note', + ref: '#/components/schemas/translation', + description: 'Ship-Matrix production note', + nullable: true + ), + new OA\Property( + property: 'type', + ref: '#/components/schemas/translation', + description: 'Ship-Matrix vehicle type', + nullable: true + ), + new OA\Property( + property: 'shipmatrix_description', + ref: '#/components/schemas/translation', + description: 'Ship-Matrix vehicle description', + nullable: true + ), + new OA\Property( + property: 'size_name', + ref: '#/components/schemas/translation', + description: 'Ship-Matrix size name (Small, Medium, Large, etc.)', + nullable: true + ), + new OA\Property( + property: 'msrp', + description: 'Ship-Matrix MSRP in USD', + type: 'number', + nullable: true + ), + new OA\Property( + property: 'pledge_url', + description: 'Link to RSI pledge store', + type: 'string', + nullable: true + ), + new OA\Property( + property: 'loaner', + description: 'Ship-Matrix loaner vehicles', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/vehicle_loaner'), + nullable: true + ), + new OA\Property( + property: 'skus', + description: 'Ship-Matrix SKUs', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/vehicle_sku'), + nullable: true + ), + new OA\Property( + property: 'components', + description: 'Ship-Matrix components (only included when ?include=components)', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/vehicle_component'), + nullable: true + ), + new OA\Property(property: 'updated_at', type: 'string'), + new OA\Property(property: 'version', type: 'string', example: '4.4.0-LIVE.12340123'), + ], + type: 'object' +)] +class VehicleResource extends AbstractBaseResource +{ + use CalculatesCargoGridSizeLimits; + use ExtractsJsonData; + + public static function validIncludes(): array + { + return [ + 'manufacturer', + 'shipMatrixVehicle', + 'components', + ]; + } + + public function toArray(Request $request): array + { + $includes = collect(explode(',', $request->get('include', ''))) + ->map('trim') + ->map('strtolower') + ->toArray(); + + $vehicleData = $this->data->first(); + + if ($vehicleData === null) { + return []; + } + + $payload = $vehicleData->data ?? []; + $flight = Arr::get($payload, 'FlightCharacteristics', []); + + $apiVersion = $this->getApiVersion($request); + + $hardpoints = $apiVersion === 'v2' + ? HardpointResource::collection(Arr::get($payload, 'Loadout', [])) + : PortResource::collection(Arr::get($payload, 'Loadout', [])); + + $portKey = $apiVersion === 'v2' ? 'hardpoints' : 'ports'; + + $cargoGridPayload = Arr::get($payload, 'CargoGrids', []); + $cargoGrids = CargoGridResource::collection($cargoGridPayload); + $cargoLimits = self::calculateCargoGridSizeLimits($cargoGridPayload); + + $this->addMetadata('deprecated_fields', [ + 'sizes' => 'Use length, width, and height properties from dimension instead', + 'emission' => 'Use properties from signature instead. Emission.ir currently maps to IR with shields active. Emission.em_max maps to EM Signature with quantum drive active. Emission.em_idle maps to EM Signature with shields active.', + 'mass' => 'Use mass_total property instead. Mass is equal to mass_hull.', + 'shield_hp' => 'Use shield.hp property instead.', + 'shield_face_type' => 'Use shield.face_type property instead.', + + 'personal_inventory' => 'No replacement.', + + "{$portKey}[].equipped_item.$portKey" => "Use {$portKey}[].$portKey instead.", + ]); + + $data = [ + 'uuid' => $this->uuid, + 'name' => $vehicleData->display_name ?? $vehicleData->name, + 'game_name' => $vehicleData->name, + 'slug' => Str::slug($vehicleData->display_name ?? $vehicleData->name), + 'class_name' => $vehicleData->class_name, + + 'sizes' => [ + 'length' => $vehicleData->length ?? Arr::get($payload, 'Length'), + 'beam' => $vehicleData->width ?? Arr::get($payload, 'Width'), + 'height' => $vehicleData->height ?? Arr::get($payload, 'Height'), + ], + 'dimension' => [ + 'length' => Arr::get($payload, 'Length'), + 'width' => Arr::get($payload, 'Width'), + 'height' => Arr::get($payload, 'Height'), + ], + 'emission' => [ + 'ir' => Arr::get($payload, 'Emission.IrShields'), + 'em_idle' => Arr::get($payload, 'Emission.EmShields'), + 'em_max' => Arr::get($payload, 'Emission.EmQuantum'), + ], + + 'mass' => $vehicleData->mass ?? Arr::get($payload, 'Mass'), + + 'mass_hull' => $this->roundNullable($vehicleData->mass_vehicle ?? Arr::get($payload, 'Mass')), + 'mass_loadout' => $this->roundNullable($vehicleData->mass_loadout ?? Arr::get($payload, 'MassLoadout')), + 'mass_total' => $this->roundNullable($vehicleData->mass_total ?? Arr::get($payload, 'MassTotal')), + + 'cargo_capacity' => $vehicleData->cargo ?? Arr::get($payload, 'Cargo'), + 'cargo_grids' => $cargoGrids, + $this->mergeWhen( + $cargoLimits !== null, + fn () => ['cargo_limits' => $cargoLimits] + ), + 'vehicle_inventory' => round(Arr::get($payload, 'Stowage', 0), 2), + 'inventory_containers' => Arr::get($payload, 'InventoryContainers'), + + 'crew' => [ + 'min' => Arr::get($payload, 'Crew'), + 'max' => Arr::get($payload, 'Crew'), // TODO + 'weapon' => Arr::get($payload, 'WeaponCrew'), + 'operation' => null, // TODO + ], + + 'health' => round(Arr::get($payload, 'Health', 0)), + + 'shield_hp' => Arr::get($payload, 'ShieldsTotal.Hp'), + 'shield_face_type' => Arr::get($payload, 'ShieldController.FaceType'), + + 'shield' => [ + 'hp' => round(Arr::get($payload, 'ShieldsTotal.Hp', 0)), + 'regeneration' => $this->roundNullable(Arr::get($payload, 'ShieldsTotal.Regen')), + 'face_type' => Arr::get($payload, 'ShieldController.FaceType'), + 'max_reallocation' => Arr::get($payload, 'ShieldController.MaxReallocation'), + 'reconfiguration_cooldown' => Arr::get($payload, 'ShieldController.ReconfigurationCooldown'), + 'max_electrical_charge_damage_rate' => Arr::get($payload, 'ShieldController.MaxElectricalChargeDamageRate'), + ], + + 'speed' => $this->buildSpeed($flight), + + 'afterburner' => [ + 'pitch_boost_multiplier' => Arr::get($flight, 'Afterburner.AngularAccelerationMultiplier.Pitch'), + 'roll_boost_multiplier' => Arr::get($flight, 'Afterburner.AngularAccelerationMultiplier.Roll'), + 'yaw_boost_multiplier' => Arr::get($flight, 'Afterburner.AngularAccelerationMultiplier.Yaw'), + + 'capacitor' => Arr::get($flight, 'Afterburner.CapacitorMax'), + 'idle_cost' => Arr::get($flight, 'Afterburner.CapacitorAfterburnerIdleCost'), + 'linear_cost' => Arr::get($flight, 'Afterburner.CapacitorAfterburnerLinearCost'), + 'angular_cost' => Arr::get($flight, 'Afterburner.CapacitorAfterburnerAngularCost'), + 'regen_per_second' => Arr::get($flight, 'Afterburner.CapacitorRegenPerSec'), + 'regen_delay_after_use' => Arr::get($flight, 'Afterburner.CapacitorRegenDelayAfterUse'), + 'pre_delay_time' => Arr::get($flight, 'Afterburner.AfterburnerPreDelayTime'), + 'ramp_up_time' => Arr::get($flight, 'Afterburner.AfterburnerRampUpTime'), + 'ramp_down_time' => Arr::get($flight, 'Afterburner.AfterburnerRampDownTime'), + ], + + 'fuel' => $this->buildFuel($payload), + 'quantum' => $this->buildQuantum($payload), + + 'agility' => $this->buildAgility($flight), + + 'armor' => [ + 'uuid' => Arr::get($payload, 'Armor.UUID'), + 'health' => Arr::get($payload, 'Armor.Health'), + + 'signal_infrared' => Arr::get($payload, 'Armor.SignalMultipliers.Infrared'), + 'signal_electromagnetic' => Arr::get($payload, 'Armor.SignalMultipliers.Electromagnetic'), + 'signal_cross_section' => Arr::get($payload, 'Armor.SignalMultipliers.CrossSection'), + + 'damage_physical' => Arr::get($payload, 'Armor.DamageMultipliers.Physical'), + 'damage_energy' => Arr::get($payload, 'Armor.DamageMultipliers.Energy'), + 'damage_distortion' => Arr::get($payload, 'Armor.DamageMultipliers.Distortion'), + 'damage_thermal' => Arr::get($payload, 'Armor.DamageMultipliers.Thermal'), + 'damage_biochemical' => Arr::get($payload, 'Armor.DamageMultipliers.Biochemical'), + 'damage_stun' => Arr::get($payload, 'Armor.DamageMultipliers.Stun'), + + 'signal_multipliers' => [ + 'cross_section' => Arr::get($payload, 'Armor.SignalMultipliers.CrossSection'), + 'infrared' => Arr::get($payload, 'Armor.SignalMultipliers.Infrared'), + 'electromagnetic' => Arr::get($payload, 'Armor.SignalMultipliers.Electromagnetic'), + ], + 'damage_multipliers' => [ + 'physical' => Arr::get($payload, 'Armor.DamageMultipliers.Physical'), + 'energy' => Arr::get($payload, 'Armor.DamageMultipliers.Energy'), + 'distortion' => Arr::get($payload, 'Armor.DamageMultipliers.Distortion'), + 'thermal' => Arr::get($payload, 'Armor.DamageMultipliers.Thermal'), + 'biochemical' => Arr::get($payload, 'Armor.DamageMultipliers.Biochemical'), + 'stun' => Arr::get($payload, 'Armor.DamageMultipliers.Stun'), + ], + 'resistance_multipliers' => [ + 'physical' => Arr::get($payload, 'Armor.ResistanceMultiplier.Physical'), + 'energy' => Arr::get($payload, 'Armor.ResistanceMultiplier.Energy'), + 'distortion' => Arr::get($payload, 'Armor.ResistanceMultiplier.Distortion'), + 'thermal' => Arr::get($payload, 'Armor.ResistanceMultiplier.Thermal'), + 'biochemical' => Arr::get($payload, 'Armor.ResistanceMultiplier.Biochemical'), + 'stun' => Arr::get($payload, 'Armor.ResistanceMultiplier.Stun'), + ], + 'penetration_resistance' => [ + 'base' => Arr::get($payload, 'Armor.PenetrationResistance.Base'), + 'physical' => Arr::get($payload, 'Armor.PenetrationResistance.Physical'), + 'energy' => Arr::get($payload, 'Armor.PenetrationResistance.Energy'), + 'distortion' => Arr::get($payload, 'Armor.PenetrationResistance.Distortion'), + 'thermal' => Arr::get($payload, 'Armor.PenetrationResistance.Thermal'), + 'biochemical' => Arr::get($payload, 'Armor.PenetrationResistance.Biochemical'), + 'stun' => Arr::get($payload, 'Armor.PenetrationResistance.Stun'), + ], + ], + + 'manufacturer' => new ManufacturerLinkResource($vehicleData->manufacturer), + 'size_class' => $vehicleData->size ?? Arr::get($payload, 'Size'), + + 'cross_section' => [ + 'length' => Arr::get($payload, 'CrossSection.X'), + 'width' => Arr::get($payload, 'CrossSection.Y'), + 'height' => Arr::get($payload, 'CrossSection.Z'), + ], + + 'is_vehicle' => Arr::get($payload, 'IsVehicle'), + 'is_gravlev' => Arr::get($payload, 'IsGravlev'), + 'is_spaceship' => Arr::get($payload, 'IsSpaceship'), + + 'signature' => [ + 'ir_quantum' => Arr::get($payload, 'Emission.IrQuantum'), + 'ir_shields' => Arr::get($payload, 'Emission.IrShields'), + + 'em_quantum' => Arr::get($payload, 'Emission.EmQuantum'), + 'em_shields' => Arr::get($payload, 'Emission.EmShields'), + + 'em_groups_quantum' => Arr::get($payload, 'Emission.EmGroupsQuantum'), + 'em_groups_shields' => Arr::get($payload, 'Emission.EmGroupsShields'), + + 'em_segment_groups_quantum' => Arr::get($payload, 'Emission.EmSegmentGroupsQuantum'), + 'em_segment_groups_shields' => Arr::get($payload, 'Emission.EmSegmentGroupsShields'), + + 'em_per_segment' => Arr::get($payload, 'Emission.EmPerSegment'), + ], + + 'cooling' => [ + 'generation_segments' => Arr::get($payload, 'Cooling.GenerationSegments'), + 'usage_shields_pct' => Arr::get($payload, 'Cooling.UsageShieldsPct'), + 'usage_quantum_pct' => Arr::get($payload, 'Cooling.UsageQuantumPct'), + + 'used_segments_shields' => Arr::get($payload, 'Cooling.UsedSegmentsShields'), + 'used_segments_quantum' => Arr::get($payload, 'Cooling.UsedSegmentsQuantum'), + ], + + 'power' => [ + 'used_segments_shields' => Arr::get($payload, 'Power.UsedSegmentsShields'), + 'used_segments_quantum' => Arr::get($payload, 'Power.UsedSegmentsQuantum'), + 'generation_segments' => Arr::get($payload, 'Power.GenerationSegments'), + 'usage' => Arr::get($payload, 'Power.Usage'), + ], + + 'penetration_multiplier' => [ + 'fuse' => Arr::get($payload, 'PenetrationMultiplier.Fuse'), + 'components' => Arr::get($payload, 'PenetrationMultiplier.Components'), + ], + + 'insurance' => [ + 'claim_time' => Arr::get($payload, 'Insurance.StandardClaimTime'), + 'expedite_time' => Arr::get($payload, 'Insurance.ExpeditedClaimTime'), + 'expedite_cost' => Arr::get($payload, 'Insurance.ExpeditedCost'), + ], + 'damage_limits' => [ + 'before_destruction' => Arr::get($payload, 'DamageBeforeDestruction'), + 'before_detach' => Arr::get($payload, 'DamageBeforeDetach'), + ], + $portKey => $hardpoints, + 'parts' => PartResource::collection(Arr::get($payload, 'Parts', [])), + 'turrets' => [ + 'manned' => TurretSummaryResource::collection(Arr::get($payload, 'MannedTurrets', [])), + 'remote' => TurretSummaryResource::collection(Arr::get($payload, 'RemoteTurrets', [])), + ], + + 'career' => $vehicleData->career ?? Arr::get($payload, 'Career'), + 'role' => $vehicleData->role ?? Arr::get($payload, 'Role'), + + $this->mergeWhen( + $this->shouldIncludeComponents($includes, $vehicleData) && $this->isVehicleShowRoute($request), + fn () => ['components' => $this->getComponents($vehicleData)] + ), + + 'web_url' => $this->buildWebUrl($request), + 'link' => route('vehicles.show', ['vehicle' => $this->uuid ?? $vehicleData->name]), + + 'loaner' => VehicleLoanerResource::collection($vehicleData->shipMatrixVehicle?->loaner), + 'skus' => VehicleSkuResource::collection($vehicleData->shipMatrixVehicle?->skus), + 'msrp' => $vehicleData->shipMatrixVehicle?->msrp, + 'pledge_url' => $vehicleData->shipMatrixVehicle?->pledge_url, + + 'updated_at' => $vehicleData->updated_at, + 'version' => $vehicleData->gameVersion?->code, + ]; + + $this->loadShipMatrixData($data, $request); + + return $data; + } + + private function buildSpeed(array $flight): ?array + { + return [ + 'scm' => Arr::get($flight, 'Speeds.Scm'), + 'max' => Arr::get($flight, 'Speeds.Max'), + 'boost_forward' => Arr::get($flight, 'Speeds.BoostForward'), + 'boost_backward' => Arr::get($flight, 'Speeds.BoostBackward'), + 'zero_to_scm' => Arr::get($flight, 'Timing.ZeroToScm'), + 'zero_to_max' => Arr::get($flight, 'Timing.ZeroToMax'), + 'scm_to_zero' => Arr::get($flight, 'Timing.ScmToZero'), + 'max_to_zero' => Arr::get($flight, 'Timing.MaxToZero'), + ]; + } + + private function buildAgility(array $flight): ?array + { + return [ + 'pitch' => Arr::get($flight, 'AngularRates.Pitch'), + 'yaw' => Arr::get($flight, 'AngularRates.Yaw'), + 'roll' => Arr::get($flight, 'AngularRates.Roll'), + 'pitch_boosted' => Arr::get($flight, 'AngularRatesBoosted.Pitch'), + 'yaw_boosted' => Arr::get($flight, 'AngularRatesBoosted.Yaw'), + 'roll_boosted' => Arr::get($flight, 'AngularRatesBoosted.Roll'), + + 'acceleration' => array_filter([ + 'main' => round(Arr::get($flight, 'Acceleration.Raw.Forward', 0), 2), + 'retro' => round(Arr::get($flight, 'Acceleration.Raw.Backward', 0), 2), + 'vtol' => round(Arr::get($flight, 'Acceleration.Raw.Vtol', 0), 2), + 'maneuvering' => round(Arr::get($flight, 'Acceleration.Raw.Maneuvering', 0), 2), + + 'main_g' => round(Arr::get($flight, 'Acceleration.RawG.Forward', 0), 2), + 'retro_g' => round(Arr::get($flight, 'Acceleration.RawG.Backward', 0), 2), + 'vtol_g' => round(Arr::get($flight, 'Acceleration.RawG.Vtol', 0), 2), + 'maneuvering_g' => round(Arr::get($flight, 'Acceleration.RawG.Maneuvering', 0), 2), + ], static fn ($value) => $value !== null), + ]; + } + + private function buildFuel(Collection $payload): ?array + { + return [ + 'capacity' => Arr::get($payload, 'Propulsion.FuelCapacity') / 1000, + 'intake_rate' => Arr::get($payload, 'Propulsion.FuelIntakeRate'), + 'usage' => [ + 'main' => $this->roundNullable(Arr::get($payload, 'Propulsion.FuelUsage.Main')), + 'retro' => $this->roundNullable(Arr::get($payload, 'Propulsion.FuelUsage.Retro')), + 'vtol' => $this->roundNullable(Arr::get($payload, 'Propulsion.FuelUsage.Vtol')), + 'maneuvering' => $this->roundNullable(Arr::get($payload, 'Propulsion.FuelUsage.Maneuvering')), + ], + ]; + } + + private function buildQuantum(Collection $payload): ?array + { + return [ + 'quantum_speed' => Arr::get($payload, 'QuantumTravel.Speed'), + 'quantum_spool_time' => Arr::get($payload, 'QuantumTravel.SpoolTime'), + 'quantum_fuel_capacity' => Arr::get($payload, 'QuantumTravel.FuelCapacity') / 1000, + 'quantum_range' => Arr::get($payload, 'QuantumTravel.Range'), + 'port_olisar_to_arccorp_time' => Arr::get($payload, 'QuantumTravel.PortOlisarToArcCorpTime'), + 'port_olisar_to_arccorp_fuel' => Arr::get($payload, 'QuantumTravel.PortOlisarToArcCorpFuel'), + ]; + } + + private function buildWebUrl(Request $request): string + { + $url = route('web.vehicles.show', ['vehicle' => $this->uuid]); + $version = $request->query('version'); + + if ($version === null || $version === '') { + return $url; + } + + return url()->query($url, ['version' => $version]); + } + + /** + * Adds Ship-Matrix information to the vehicle data. + * Only adds non-empty fields from the Ship-Matrix vehicle. + */ + private function loadShipMatrixData(array &$data, Request $request): void + { + $vehicleData = $this->data->first(); + + if ($vehicleData === null) { + return; + } + + if (! $vehicleData->relationLoaded('shipMatrixVehicle')) { + return; + } + + $shipMatrixVehicle = $vehicleData->shipMatrixVehicle; + + if (! $shipMatrixVehicle->exists) { + return; + } + + $matrixVehicle = (new \App\Http\Resources\StarCitizen\Vehicle\VehicleResource($shipMatrixVehicle)) + ->resolve($request); + + $fieldMap = [ + 'id' => 'id', + 'chassis_id' => 'chassis_id', + 'name' => 'shipmatrix_name', + 'foci' => 'foci', + 'production_status' => 'production_status', + 'production_note' => 'production_note', + 'type' => 'type', + 'description' => 'description', + 'size' => 'size', + 'msrp' => 'msrp', + 'pledge_url' => 'pledge_url', + 'loaner' => 'loaner', + 'skus' => 'skus', + ]; + + foreach ($fieldMap as $sourceKey => $targetKey) { + if (array_key_exists($sourceKey, $matrixVehicle) && $matrixVehicle[$sourceKey] !== null) { + $data[$targetKey] = $matrixVehicle[$sourceKey]; + } + } + } + + /** + * Determine if components should be included in the response. + */ + private function shouldIncludeComponents(array $includes, ?VehicleData $vehicleData): bool + { + if (! in_array('components', $includes, true) && ! in_array('shipmatrixvehicle.components', $includes, true)) { + return false; + } + + if ($vehicleData === null) { + return false; + } + + if (! $vehicleData->relationLoaded('shipMatrixVehicle')) { + return false; + } + + $shipMatrixVehicle = $vehicleData->shipMatrixVehicle; + + if (! $shipMatrixVehicle->exists) { + return false; + } + + return $shipMatrixVehicle->relationLoaded('components'); + } + + /** + * Get components for the ship-matrix vehicle. + */ + private function getComponents(VehicleData $vehicleData): array + { + $shipMatrixVehicle = $vehicleData->shipMatrixVehicle; + + return ComponentResource::collection($shipMatrixVehicle->components)->resolve(); + } + + private function isVehicleShowRoute(Request $request): bool + { + return $request->routeIs('vehicles.show') || $request->routeIs('*.vehicles.show'); + } + + private function getApiVersion(Request $request): ?string + { + return $request->route('api_version'); + } + + private function roundNullable(mixed $value, int $precision = 2): ?float + { + if ($value === null || ! is_numeric($value)) { + return null; + } + + return round((float) $value, $precision); + } +} diff --git a/app/Http/Resources/Game/Weapon/WeaponDamageResource.php b/app/Http/Resources/Game/Weapon/WeaponDamageResource.php new file mode 100644 index 000000000..7088c17d8 --- /dev/null +++ b/app/Http/Resources/Game/Weapon/WeaponDamageResource.php @@ -0,0 +1,33 @@ + Arr::get($this->resource, 'type'), + 'name' => Arr::get($this->resource, 'name'), + 'damage' => Arr::get($this->resource, 'damage'), + ]; + } +} diff --git a/app/Http/Resources/OpenApiParameters.php b/app/Http/Resources/OpenApiParameters.php new file mode 100644 index 000000000..af2728612 --- /dev/null +++ b/app/Http/Resources/OpenApiParameters.php @@ -0,0 +1,61 @@ + $this->href, diff --git a/app/Http/Resources/Rsi/CommLink/CommLinkResource.php b/app/Http/Resources/Rsi/CommLink/CommLinkResource.php index bd0767d6d..b87b65622 100644 --- a/app/Http/Resources/Rsi/CommLink/CommLinkResource.php +++ b/app/Http/Resources/Rsi/CommLink/CommLinkResource.php @@ -6,12 +6,12 @@ use App\Http\Resources\AbstractBaseResource; use App\Http\Resources\Rsi\CommLink\Image\ImageResource; -use App\Http\Resources\TranslationResourceFactory; +use App\Http\Resources\TranslationResolver; use Illuminate\Http\Request; use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'comm_link_v2', + schema: 'comm_link', title: 'Comm-Link', description: 'A Comm-Link', properties: [ @@ -26,18 +26,19 @@ new OA\Property( property: 'images', type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link_image_v2'), + items: new OA\Items(ref: '#/components/schemas/comm_link_image'), ), new OA\Property(property: 'images_count', type: 'integer'), new OA\Property( property: 'links', type: 'array', - items: new OA\Items(ref: '#/components/schemas/comm_link_link_v2'), + items: new OA\Items(ref: '#/components/schemas/comm_link_link'), ), new OA\Property(property: 'links_count', type: 'integer'), new OA\Property(property: 'comment_count', type: 'integer'), new OA\Property(property: 'created_at', type: 'string'), - new OA\Property(property: 'translations', ref: '#/components/schemas/translation_v2'), + new OA\Property(property: 'translations', ref: '#/components/schemas/translation'), + new OA\Property(property: 'created_at_human', type: 'string', example: '1 hour ago'), ], type: 'object' )] @@ -52,29 +53,25 @@ public static function validIncludes(): array ]; } - /** - * Transform the resource into an array. - * - * @param Request $request - */ - public function toArray($request): array + public function toArray(Request $request): array { return [ 'id' => $this->cig_id, 'title' => $this->title, 'rsi_url' => $this->getCommLinkUrl(), - 'api_url' => $this->makeApiUrl(self::COMM_LINKS_SHOW, $this->getRouteKey()), + 'api_url' => route('comm-links.show', ['id' => $this->getRouteKey()]), 'api_public_url' => route('web.comm-links.show', $this->getRouteKey()), 'channel' => $this->channel->name, 'category' => $this->category->name, 'series' => $this->series->name, 'images' => ImageResource::collection($this->whenLoaded('images')), 'images_count' => $this->images_count, - 'translations' => TranslationResourceFactory::getTranslationResource($request, $this->whenLoaded('translations')), + 'translations' => TranslationResolver::resolve($this, $request), 'links' => CommLinkLinkResource::collection($this->whenLoaded('links')), 'links_count' => $this->links_count, 'comment_count' => $this->comment_count, - 'created_at' => $this->created_at, + 'created_at' => $this->created_at->toIso8601String(), + 'created_at_human' => $this->created_at->diffForHumans(), ]; } @@ -83,6 +80,6 @@ public function toArray($request): array */ private function getCommLinkUrl(): string { - return sprintf('%s%s', config('api.rsi_url'), ($this->url ?? "/comm-link/SCW/{$this->cig_id}-API")); + return sprintf('%s%s', config('services.rsi_url'), ($this->url ?? "/comm-link/SCW/{$this->cig_id}-API")); } } diff --git a/app/Http/Resources/Rsi/CommLink/Image/ImageHashResource.php b/app/Http/Resources/Rsi/CommLink/Image/ImageHashResource.php index 1422b137c..8c8162713 100644 --- a/app/Http/Resources/Rsi/CommLink/Image/ImageHashResource.php +++ b/app/Http/Resources/Rsi/CommLink/Image/ImageHashResource.php @@ -12,12 +12,7 @@ */ class ImageHashResource extends ImageResource { - /** - * Transform the resource into an array. - * - * @param Request $request - */ - public function toArray($request): array + public function toArray(Request $request): array { $data = parent::toArray($request); diff --git a/app/Http/Resources/Rsi/CommLink/Image/ImageResource.php b/app/Http/Resources/Rsi/CommLink/Image/ImageResource.php index ad1cf7a04..f2e882c33 100644 --- a/app/Http/Resources/Rsi/CommLink/Image/ImageResource.php +++ b/app/Http/Resources/Rsi/CommLink/Image/ImageResource.php @@ -1,5 +1,7 @@ $this->id, + 'name' => $this->name, 'rsi_url' => $this->url, - 'api_url' => $this->local ? asset("storage/comm_link_images/{$this->dir}/{$this->name}") : null, 'alt' => $this->alt, 'size' => $this->metadata->size, 'mime_type' => $this->metadata->mime, - 'last_modified' => $this->metadata->last_modified, - $this->mergeWhen($this->whenLoaded('tags'), [ - 'tags' => $this->tags->map(fn ($tag) => $tag->translated_name), + 'last_modified' => $this->metadata->last_modified->toIso8601String(), + $this->mergeWhen($this->relationLoaded('tags'), [ + 'tags' => $this->tags->map(fn ($tag) => [ + 'name' => $tag->name, + 'translated_name' => $tag->translated_name, + 'images_count' => $tag->images_count, + ]), + ]), + $this->mergeWhen($this->relationLoaded('commLinks'), [ + 'comm_links' => $this->commLinks->map(fn ($commLink) => [ + 'id' => $commLink->cig_id, + 'title' => $commLink->title, + 'api_url' => route('comm-links.show', ['id' => $commLink->cig_id]), + 'web_url' => route('web.comm-links.show', $commLink->cig_id), + ]), + ]), + $this->mergeWhen($this->relationLoaded('duplicates'), [ + 'duplicates' => $this->duplicates->map(fn ($image) => [ + 'id' => $image->id, + 'name' => $image->name, + ]), + ]), + $this->mergeWhen($this->relationLoaded('baseImage'), [ + 'base_image' => $this->baseImage === null ? null : [ + 'id' => $this->baseImage->id, + 'name' => $this->baseImage->name, + ], ]), - 'similar_url' => $this->makeApiUrl(static::COMM_LINK_IMAGES_SIMILAR, $this->getRouteKey().'/similar'), + 'api_url' => route('comm-link-images.show', ['image' => $this->getRouteKey()]), + 'similar_url' => route('comm-link-images.similar', ['image' => $this->getRouteKey()]), ]; } } diff --git a/app/Http/Resources/SC/Ammunition/AmmunitionDamageFalloffResource.php b/app/Http/Resources/SC/Ammunition/AmmunitionDamageFalloffResource.php deleted file mode 100644 index 1a1da5fa5..000000000 --- a/app/Http/Resources/SC/Ammunition/AmmunitionDamageFalloffResource.php +++ /dev/null @@ -1,38 +0,0 @@ - $this->physical ?? null, - 'energy' => $this->energy ?? null, - 'distortion' => $this->distortion ?? null, - 'thermal' => $this->thermal ?? null, - 'biochemical' => $this->biochemical ?? null, - 'stun' => $this->stun ?? null, - ]; - } -} diff --git a/app/Http/Resources/SC/Ammunition/AmmunitionPiercabilityResource.php b/app/Http/Resources/SC/Ammunition/AmmunitionPiercabilityResource.php deleted file mode 100644 index 3dd84d944..000000000 --- a/app/Http/Resources/SC/Ammunition/AmmunitionPiercabilityResource.php +++ /dev/null @@ -1,33 +0,0 @@ - $this->damage_falloff_level_1 ?? null, - 'damage_falloff_level_2' => $this->damage_falloff_level_2 ?? null, - 'damage_falloff_level_3' => $this->damage_falloff_level_3 ?? null, - 'max_penetration_thickness' => $this->max_penetration_thickness ?? null, - ]; - } -} diff --git a/app/Http/Resources/SC/Ammunition/AmmunitionResource.php b/app/Http/Resources/SC/Ammunition/AmmunitionResource.php deleted file mode 100644 index 8ca2ed15f..000000000 --- a/app/Http/Resources/SC/Ammunition/AmmunitionResource.php +++ /dev/null @@ -1,48 +0,0 @@ - $this->uuid ?? null, - 'size' => $this->size ?? null, - 'lifetime' => $this->lifetime ?? null, - 'speed' => $this->speed ?? null, - 'range' => $this->range ?? null, - 'piercability' => new AmmunitionPiercabilityResource($this->piercability), - 'damage_falloffs' => [ - 'min_distance' => new AmmunitionDamageFalloffResource($this->damageFalloffs()->where('type', 'min_distance')->first() ?? []), - 'per_meter' => new AmmunitionDamageFalloffResource($this->damageFalloffs()->where('type', 'per_meter')->first() ?? []), - 'min_damage' => new AmmunitionDamageFalloffResource($this->damageFalloffs()->where('type', 'min_damage')->first() ?? []), - ], - ]; - } -} diff --git a/app/Http/Resources/SC/Char/ClothingRadiationResistanceResource.php b/app/Http/Resources/SC/Char/ClothingRadiationResistanceResource.php deleted file mode 100644 index 44d5d595a..000000000 --- a/app/Http/Resources/SC/Char/ClothingRadiationResistanceResource.php +++ /dev/null @@ -1,40 +0,0 @@ - $this->maximum_radiation_capacity, - 'radiation_dissipation_rate' => $this->radiation_dissipation_rate, - ]; - } -} diff --git a/app/Http/Resources/SC/Char/ClothingResistanceResource.php b/app/Http/Resources/SC/Char/ClothingResistanceResource.php deleted file mode 100644 index 6f31ecc37..000000000 --- a/app/Http/Resources/SC/Char/ClothingResistanceResource.php +++ /dev/null @@ -1,42 +0,0 @@ - $this->type, - 'threshold' => $this->threshold, - 'multiplier' => $this->multiplier, - ]; - } -} diff --git a/app/Http/Resources/SC/Char/ClothingResource.php b/app/Http/Resources/SC/Char/ClothingResource.php deleted file mode 100644 index c3db0f0ac..000000000 --- a/app/Http/Resources/SC/Char/ClothingResource.php +++ /dev/null @@ -1,91 +0,0 @@ -type, 'Char_Clothing')) { - $typeKey = 'clothing_type'; - } - - return [ - $typeKey => $this->clothing_type, - 'damage_reduction' => $this->damage_reduction, - 'temp_resistance_min' => $this->temp_resistance_min, - 'temp_resistance_max' => $this->temp_resistance_max, - 'radiation_resistance' => ClothingRadiationResistanceResource::make($this->radiation_resistance), - 'resistances' => ClothingResistanceResource::collection($this->damageResistances), - ]; - } -} diff --git a/app/Http/Resources/SC/Char/PersonalWeapon/BarrelAttachResource.php b/app/Http/Resources/SC/Char/PersonalWeapon/BarrelAttachResource.php deleted file mode 100644 index 4092f266f..000000000 --- a/app/Http/Resources/SC/Char/PersonalWeapon/BarrelAttachResource.php +++ /dev/null @@ -1,28 +0,0 @@ - $this->type ?? null, - ]; - } -} diff --git a/app/Http/Resources/SC/Char/PersonalWeapon/GrenadeResource.php b/app/Http/Resources/SC/Char/PersonalWeapon/GrenadeResource.php deleted file mode 100644 index a36912a2f..000000000 --- a/app/Http/Resources/SC/Char/PersonalWeapon/GrenadeResource.php +++ /dev/null @@ -1,42 +0,0 @@ - $this->aoe, - 'damage_type' => $this->damage_type, - 'damage' => $this->damage, - ]; - } -} diff --git a/app/Http/Resources/SC/Char/PersonalWeapon/IronSightResource.php b/app/Http/Resources/SC/Char/PersonalWeapon/IronSightResource.php deleted file mode 100644 index c814d9a50..000000000 --- a/app/Http/Resources/SC/Char/PersonalWeapon/IronSightResource.php +++ /dev/null @@ -1,40 +0,0 @@ - $this->zoom_scale ?? null, - 'optic_type' => $this->optic_type ?? null, - 'default_range' => $this->default_range ?? null, - 'max_range' => $this->max_range ?? null, - 'range_increment' => $this->range_increment ?? null, - 'auto_zeroing_time' => $this->auto_zeroing_time ?? null, - 'zoom_time_scale' => $this->zoom_time_scale ?? null, - ]); - } -} diff --git a/app/Http/Resources/SC/Char/PersonalWeapon/KnifeResource.php b/app/Http/Resources/SC/Char/PersonalWeapon/KnifeResource.php deleted file mode 100644 index 861459c92..000000000 --- a/app/Http/Resources/SC/Char/PersonalWeapon/KnifeResource.php +++ /dev/null @@ -1,42 +0,0 @@ - $this->can_be_used_for_take_down, - 'can_block' => $this->can_block, - 'can_be_used_in_prone' => $this->can_be_used_in_prone, - 'can_dodge' => $this->can_dodge, - 'attack_modes' => MeleeCombatConfigResource::collection($this->combatConfig), - ]; - } -} diff --git a/app/Http/Resources/SC/Char/PersonalWeapon/PersonalWeaponAttachmentResource.php b/app/Http/Resources/SC/Char/PersonalWeapon/PersonalWeaponAttachmentResource.php deleted file mode 100644 index d638dfa54..000000000 --- a/app/Http/Resources/SC/Char/PersonalWeapon/PersonalWeaponAttachmentResource.php +++ /dev/null @@ -1,58 +0,0 @@ - $this->initial_ammo_count ?? null, - 'max_ammo_count' => $this->max_ammo_count ?? null, - 'type' => $this->getDescriptionDatum('Item Type'), - ]; - } -} diff --git a/app/Http/Resources/SC/Char/PersonalWeapon/PersonalWeaponResource.php b/app/Http/Resources/SC/Char/PersonalWeapon/PersonalWeaponResource.php deleted file mode 100644 index ef5e1e61b..000000000 --- a/app/Http/Resources/SC/Char/PersonalWeapon/PersonalWeaponResource.php +++ /dev/null @@ -1,80 +0,0 @@ - $this->weapon_class, - 'type' => $this->weapon_type, - 'magazine_type' => $this->magazineType, - 'magazine_size' => $this->magazine->max_ammo_count ?? null, - 'effective_range' => $this->effective_range ?? null, - 'damage_per_shot' => $this->ammunition?->damage ?? null, - 'rof' => $this->rof ?? null, - 'modes' => WeaponModeResource::collection($this->whenLoaded('modes')), - 'damages' => WeaponDamageResource::collection($this->damages()), - ]; - - if ($this->sub_type !== 'Knife') { - $data['ammunition'] = new AmmunitionResource($this->ammunition); - } - - return $data; - } -} diff --git a/app/Http/Resources/SC/Faction/FactionLinkResource.php b/app/Http/Resources/SC/Faction/FactionLinkResource.php deleted file mode 100644 index 80e066b42..000000000 --- a/app/Http/Resources/SC/Faction/FactionLinkResource.php +++ /dev/null @@ -1,31 +0,0 @@ - $this->uuid, - 'name' => $this->name, - 'class_name' => $this->class_name, - 'link' => $this->makeApiUrl(self::FACTIONS_SHOW, $this->uuid), - ]; - } -} diff --git a/app/Http/Resources/SC/Faction/FactionRelationResource.php b/app/Http/Resources/SC/Faction/FactionRelationResource.php deleted file mode 100644 index c03ead1cd..000000000 --- a/app/Http/Resources/SC/Faction/FactionRelationResource.php +++ /dev/null @@ -1,32 +0,0 @@ -faction))->resolve($request) + [ - 'relation' => $this->relation, - ]; - } -} diff --git a/app/Http/Resources/SC/Faction/FactionResource.php b/app/Http/Resources/SC/Faction/FactionResource.php deleted file mode 100644 index cc05d0d76..000000000 --- a/app/Http/Resources/SC/Faction/FactionResource.php +++ /dev/null @@ -1,43 +0,0 @@ - $this->uuid, - 'name' => $this->name, - 'description' => $this->description, - 'class_name' => $this->class_name, - 'game_token' => $this->game_token, - 'default_reaction' => $this->default_reaction, - 'relations' => FactionRelationResource::collection($this->relations), - ]; - } -} diff --git a/app/Http/Resources/SC/FoodResource.php b/app/Http/Resources/SC/FoodResource.php deleted file mode 100644 index 7d753dd74..000000000 --- a/app/Http/Resources/SC/FoodResource.php +++ /dev/null @@ -1,64 +0,0 @@ - $this->nutritional_density_rating, - 'hydration_efficacy_index' => $this->hydration_efficacy_index, - 'container_type' => $this->container_type, - 'one_shot_consume' => $this->one_shot_consume, - 'can_be_reclosed' => $this->can_be_reclosed, - 'discard_when_consumed' => $this->discard_when_consumed, - 'effects' => $this->effects->map(function ($effect) { - return $effect->name; - }), - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemContainerResource.php b/app/Http/Resources/SC/Item/ItemContainerResource.php deleted file mode 100644 index ba9f5a72c..000000000 --- a/app/Http/Resources/SC/Item/ItemContainerResource.php +++ /dev/null @@ -1,54 +0,0 @@ -unit) { - 2 => 'cSCU', - 6 => 'µSCU', - default => 'SCU', - }; - - return [ - 'width' => $this->width, - 'height' => $this->height, - 'length' => $this->length, - 'dimension' => $this->dimension, - 'scu' => $this->calculated_scu, - 'scu_converted' => $this->original_converted_scu, - 'unit' => $unit, - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemDescriptionDataResource.php b/app/Http/Resources/SC/Item/ItemDescriptionDataResource.php deleted file mode 100644 index 3dccbb01a..000000000 --- a/app/Http/Resources/SC/Item/ItemDescriptionDataResource.php +++ /dev/null @@ -1,30 +0,0 @@ - $this->name, - 'type' => $this->value, - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemDimensionResource.php b/app/Http/Resources/SC/Item/ItemDimensionResource.php deleted file mode 100644 index 120472982..000000000 --- a/app/Http/Resources/SC/Item/ItemDimensionResource.php +++ /dev/null @@ -1,57 +0,0 @@ -dimension; - $trueDim = $this->true_dimension; - $sumDim = $dim->width + $dim->height + $dim->length; - $sumTrueDim = $trueDim->width + $trueDim->height + $trueDim->length; - - return [ - 'width' => $dim->width, - 'height' => $dim->height, - 'length' => $dim->length, - 'volume' => $dim->volume ?? $trueDim->volume, - $this->mergeWhen($sumDim !== $sumTrueDim, [ - 'true_dimension' => [ - 'width' => $trueDim->width, - 'height' => $trueDim->height, - 'length' => $trueDim->length, - ], - ]), - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemDistortionDataResource.php b/app/Http/Resources/SC/Item/ItemDistortionDataResource.php deleted file mode 100644 index 83ae0c173..000000000 --- a/app/Http/Resources/SC/Item/ItemDistortionDataResource.php +++ /dev/null @@ -1,39 +0,0 @@ - $this->decay_delay, - 'decay_rate' => $this->decay_rate, - 'maximum' => $this->maximum, - 'overload_ratio' => $this->overload_ratio, - 'warning_ratio' => $this->warning_ratio, - 'recovery_ratio' => $this->recovery_ratio, - 'recovery_time' => $this->recovery_time, - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemDurabilityDataResource.php b/app/Http/Resources/SC/Item/ItemDurabilityDataResource.php deleted file mode 100644 index 890101ab0..000000000 --- a/app/Http/Resources/SC/Item/ItemDurabilityDataResource.php +++ /dev/null @@ -1,33 +0,0 @@ - $this->health, - 'max_lifetime' => $this->max_lifetime, - 'repairable' => $this->repairable, - 'salvageable' => $this->salvageable, - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemHeatDataResource.php b/app/Http/Resources/SC/Item/ItemHeatDataResource.php deleted file mode 100644 index c5835737c..000000000 --- a/app/Http/Resources/SC/Item/ItemHeatDataResource.php +++ /dev/null @@ -1,65 +0,0 @@ - $this->temperature_to_ir, - 'ir_temperature_threshold' => $this->ir_temperature_threshold, - 'overpower_heat' => $this->overpower_heat, - 'overclock_threshold_min' => $this->overclock_threshold_min, - 'overclock_threshold_max' => $this->overclock_threshold_max, - 'thermal_energy_base' => $this->thermal_energy_base, - 'thermal_energy_draw' => $this->thermal_energy_draw, - 'thermal_conductivity' => $this->thermal_conductivity, - 'specific_heat_capacity' => $this->specific_heat_capacity, - 'mass' => $this->mass, - 'surface_area' => $this->surface_area, - 'start_cooling_temperature' => $this->start_cooling_temperature, - 'max_cooling_rate' => $this->max_cooling_rate, - 'max_temperature' => $this->max_temperature, - 'min_temperature' => $this->min_temperature, - 'overheat_temperature' => $this->overheat_temperature, - 'recovery_temperature' => $this->recovery_temperature, - 'misfire_min_temperature' => $this->misfire_min_temperature, - 'misfire_max_temperature' => $this->misfire_max_temperature, - 'ir_emission' => $this->infrared_emission, - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemLinkResource.php b/app/Http/Resources/SC/Item/ItemLinkResource.php deleted file mode 100644 index f49b31a9f..000000000 --- a/app/Http/Resources/SC/Item/ItemLinkResource.php +++ /dev/null @@ -1,63 +0,0 @@ - $this->uuid ?? $this->item_uuid, - 'name' => $this->name, - 'type' => $this->type ?? $this->item->type, - 'sub_type' => $this->sub_type ?? $this->item->sub_type, - 'is_base_variant' => $this->base_id === null, - 'manufacturer' => new ManufacturerLinkResource($this->manufacturer ?? $this->item->manufacturer), - 'link' => $this->makeApiUrl(self::ITEMS_SHOW, $this->uuid ?? $this->item_uuid), - $this->mergeWhen($this->base_id !== null, fn () => [ - 'base_variant' => $this->makeApiUrl(self::ITEMS_SHOW, $this->baseVariant->uuid ?? ''), - ]), - 'variants' => self::collection($this->whenLoaded('variants')), - 'shops' => ShopResource::collection($this->whenLoaded('shops')), - - 'updated_at' => $this->updated_at, - 'version' => $this->version ?? $this->item->version, - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemPortResource.php b/app/Http/Resources/SC/Item/ItemPortResource.php deleted file mode 100644 index 28692d56d..000000000 --- a/app/Http/Resources/SC/Item/ItemPortResource.php +++ /dev/null @@ -1,57 +0,0 @@ - $this->name, - 'display_name' => $this->display_name, - 'position' => $this->position, - 'sizes' => [ - 'min' => $this->min_size, - 'max' => $this->max_size, - ], - 'compatible_types' => ItemPortTypeResource::collection($this->compatibleTypes), - 'tags' => $this->defaultTags->pluck('name')->toArray(), - 'required_tags' => $this->requiredTags->pluck('name')->toArray(), - 'equipped_item' => new ItemLinkResource($this->item), - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemPortTypeResource.php b/app/Http/Resources/SC/Item/ItemPortTypeResource.php deleted file mode 100644 index 1263e2507..000000000 --- a/app/Http/Resources/SC/Item/ItemPortTypeResource.php +++ /dev/null @@ -1,34 +0,0 @@ - $this->type, - 'sub_types' => $this->subTypes->pluck('sub_type')->toArray(), - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemPowerDataResource.php b/app/Http/Resources/SC/Item/ItemPowerDataResource.php deleted file mode 100644 index 0cef9951e..000000000 --- a/app/Http/Resources/SC/Item/ItemPowerDataResource.php +++ /dev/null @@ -1,49 +0,0 @@ - $this->power_base, - 'power_draw' => $this->power_draw, - 'throttleable' => $this->throttleable, - 'overclockable' => $this->overclockable, - 'overclock_threshold_min' => $this->overclock_threshold_min, - 'overclock_threshold_max' => $this->overclock_threshold_max, - 'overclock_performance' => $this->overclock_performance, - 'overpower_performance' => $this->overpower_performance, - 'power_to_em' => $this->power_to_em, - 'decay_rate_em' => $this->decay_rate_em, - 'em_min' => $this->min_electromagnetic_emission, - 'em_max' => $this->max_electromagnetic_emission, - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemResource.php b/app/Http/Resources/SC/Item/ItemResource.php deleted file mode 100644 index 6e8c8b25d..000000000 --- a/app/Http/Resources/SC/Item/ItemResource.php +++ /dev/null @@ -1,501 +0,0 @@ -uuid === null) { - return []; - } - - $vehicleItem = $this->vehicleItem; - - // Determine if 'related_items' has been requested via include - $includeParam = $request->query('include'); - $includeValues = []; - if (is_string($includeParam)) { - $includeValues = array_map('trim', explode(',', $includeParam)); - } elseif (is_array($includeParam)) { - $includeValues = $includeParam; - } - $includeRelated = in_array('related_items', $includeValues, true); - - return [ - 'uuid' => $this->uuid, - 'name' => $this->name, - 'class_name' => $this->class_name, - 'description' => $this->getTranslation($this, $request), - 'size' => $this->size, - 'mass' => $this->mass, - 'is_base_variant' => $this->base_id === null, - $this->mergeWhen($vehicleItem !== null || $vehicleItem->exists, [ - 'grade' => $vehicleItem->grade, - 'class' => $vehicleItem->class, - ]), - 'description_data' => ItemDescriptionDataResource::collection($this->whenLoaded('descriptionData')), - 'manufacturer_description' => $this->getDescriptionDatum('Manufacturer'), - 'manufacturer' => new ManufacturerLinkResource($this->manufacturer), - 'type' => $this->cleanType(), - 'sub_type' => $this->sub_type, - $this->mergeWhen(...$this->addAttachmentPosition()), - $this->mergeWhen($this->isTurret(), $this->addTurretData()), - $this->mergeWhen(...$this->addSpecification()), - 'dimension' => new ItemDimensionResource($this), - $this->mergeWhen($this->container->exists, [ - 'inventory' => new ItemContainerResource($this->container), - ]), - 'tags' => $this->defaultTags->pluck('name')->toArray(), - 'required_tags' => $this->requiredTags->pluck('name')->toArray(), - 'entity_tags' => $this->entityTags->pluck('tag')->toArray(), - 'interactions' => $this->interactions->pluck('name')->toArray(), - 'ports' => ItemPortResource::collection($this->whenLoaded('ports')), - $this->mergeWhen($this->relationLoaded('heatData'), [ - 'heat' => new ItemHeatDataResource($this->heatData), - ]), - $this->mergeWhen($this->relationLoaded('powerData'), [ - 'power' => new ItemPowerDataResource($this->powerData), - ]), - $this->mergeWhen($this->relationLoaded('distortionData'), [ - 'distortion' => new ItemDistortionDataResource($this->distortionData), - ]), - $this->mergeWhen($this->relationLoaded('durabilityData'), [ - 'durability' => new ItemDurabilityDataResource($this->durabilityData), - ]), - $this->mergeWhen($this->type === 'WeaponAttachment', [ - 'weapon_modifier' => new ItemWeaponModifierDataResource($this->weaponModifierData), - ]), - 'shops' => ShopResource::collection($this->whenLoaded('shops')), - $this->mergeWhen($this->base_id !== null, [ - 'base_variant' => new ItemLinkResource($this->baseVariant), - ]), - 'variants' => ItemLinkResource::collection($this->whenLoaded('variants')), - $this->mergeWhen($includeRelated, [ - 'related_items' => (new RelatedItemsBuilder)->build($this->resource), - ]), - 'updated_at' => $this->updated_at, - 'version' => $this->version, - ]; - } - - protected function addSpecification(): array - { - $specification = $this?->specification; - if (! $specification?->exists || $specification === null) { - return [false, []]; - } - - return match (true) { - $this->type === 'Armor' => [ - $specification->exists, - fn () => ['emp' => new ArmorResource($specification)], - ], - $this->type === 'Bomb' => [ - $specification->exists, - fn () => ['bomb' => new BombResource($specification)], - ], - $this->type === 'Cooler' => [ - $specification->exists, - fn () => ['cooler' => new CoolerResource($specification)], - ], - str_contains($this->type, 'Char_Clothing'), str_contains($this->type, 'Char_Armor') => [ - $specification->exists, - fn () => ['clothing' => new ClothingResource($specification)], - ], - $this->type === 'EMP' => [ - $specification->exists, - fn () => ['emp' => new EmpResource($specification)], - ], - $this->type === 'Food', $this->type === 'Bottle', $this->type === 'Drink' => [ - $specification->exists, - fn () => ['food' => new FoodResource($specification)], - ], - $this->type === 'FlightController' => [ - $specification->exists, - fn () => ['flight_controller' => new FlightControllerResource($specification)], - ], - $this->type === 'FuelTank', $this->type === 'QuantumFuelTank', $this->type === 'ExternalFuelTank' => [ - $specification->exists, - fn () => ['fuel_tank' => new FuelTankResource($specification)], - ], - $this->type === 'FuelIntake' => [ - $specification->exists, - fn () => ['fuel_intake' => new FuelIntakeResource($specification)], - ], - $this->sub_type === 'Hacking' => [ - $specification->exists, - fn () => ['hacking_chip' => new HackingChipResource($specification)], - ], - $this->type === 'MainThruster', $this->type === 'ManneuverThruster' => [ - $specification->exists, - fn () => ['thruster' => new ThrusterResource($specification)], - ], - $this->sub_type === 'Magazine' => [ - $specification->exists, - fn () => ['personal_weapon_magazine' => new PersonalWeaponMagazineResource($specification)], - ], - $this->type === 'Missile', $this->type === 'Torpedo' => [ - $specification->exists, - fn () => ['missile' => new MissileResource($specification)], - ], - $this->type === 'MiningModifier' => [ - $specification->exists, - fn () => ['mining_module' => new MiningModuleResource($specification)], - ], - $this->type === 'PowerPlant' => [ - $specification->exists, - fn () => ['power_plant' => new PowerPlantResource($specification)], - ], - $this->type === 'QuantumInterdictionGenerator' => [ - $specification->exists, - fn () => ['quantum_interdiction_generator' => new QuantumInterdictionGeneratorResource($specification)], - ], - $this->type === 'QuantumDrive' => [ - $specification->exists, - fn () => ['quantum_drive' => new QuantumDriveResource($specification)], - ], - $this->type === 'SalvageModifier' => [ - $specification->exists, - fn () => ['salvage_modifier' => new SalvageModifierResource($specification)], - ], - $this->type === 'SelfDestruct' => [ - $specification->exists, - fn () => ['self_destruct' => new SelfDestructResource($specification)], - ], - $this->type === 'Shield' => [ - $specification->exists, - fn () => ['shield' => new ShieldResource($specification)], - ], - $this->type === 'TractorBeam' || $this->type === 'TowingBeam' => [ - $specification->exists, - fn () => ['tractor_beam' => new TractorBeamResource($specification)], - ], - $this->type === 'WeaponPersonal' && $this->sub_type === 'Grenade' => [ - $specification->exists, - fn () => ['grenade' => new GrenadeResource($specification)], - ], - $this->type === 'WeaponPersonal' && $this->sub_type === 'Knife' => [ - $specification->exists, - fn () => ['knife' => new KnifeResource($specification)], - ], - $this->type === 'WeaponPersonal' => [ - $specification->exists, - fn () => ['personal_weapon' => new PersonalWeaponResource($specification)], - ], - $this->sub_type === 'IronSight' => [ - $specification->exists, - fn () => ['iron_sight' => new IronSightResource($specification)], - ], - $this->type === 'WeaponAttachment' && in_array($this->sub_type, ['Barrel', 'BottomAttachment', 'Utility'], true) => [ - $specification->exists, - fn () => ['barrel_attach' => new BarrelAttachResource($specification)], - ], - $this->type === 'WeaponGun', $this->type === 'WeaponDefensive' => [ - $specification->exists, - fn () => [($this->type === 'WeaponGun' ? - 'vehicle_weapon' : - 'counter_measure') => new VehicleWeaponResource($specification), ], - ], - $this->type === 'WeaponMining' => [ - $specification->exists, - fn () => ['mining_laser' => new MiningLaserResource($specification)], - ], - default => [false, []], - }; - } - - protected function addTurretData(): array - { - $mountName = 'max_mounts'; - if ($this->type === 'MissileLauncher') { - $mountName = 'max_missiles'; - } elseif ($this->type === 'BombLauncher') { - $mountName = 'max_bombs'; - } - - $ports = $this->ports; - - return [ - $mountName => $ports->count(), - 'min_size' => $ports->min('min_size'), - 'max_size' => $ports->max('max_size'), - ]; - } - - private function addAttachmentPosition(): array - { - if ($this->type !== 'WeaponAttachment' || $this->name === '<= PLACEHOLDER =>') { - return [false, []]; - } - - return [ - true, - fn () => [ - 'position' => match ($this->sub_type) { - 'Magazine' => 'Magazine Well', - 'Barrel' => 'Barrel', - 'IronSight' => 'Optic', - 'Utility' => 'Utility', - 'BottomAttachment' => 'Underbarrel', - default => $this->sub_type, - }, - ], - ]; - } -} diff --git a/app/Http/Resources/SC/Item/ItemWeaponModifierDataResource.php b/app/Http/Resources/SC/Item/ItemWeaponModifierDataResource.php deleted file mode 100644 index 2b551b392..000000000 --- a/app/Http/Resources/SC/Item/ItemWeaponModifierDataResource.php +++ /dev/null @@ -1,135 +0,0 @@ - $this->fire_rate_multiplier, - 'damage_multiplier' => $this->damage_multiplier, - 'damage_over_time_multiplier' => $this->damage_over_time_multiplier, - 'projectile_speed_multiplier' => $this->projectile_speed_multiplier, - 'ammo_cost_multiplier' => $this->ammo_cost_multiplier, - 'heat_generation_multiplier' => $this->heat_generation_multiplier, - 'sound_radius_multiplier' => $this->sound_radius_multiplier, - 'charge_time_multiplier' => $this->charge_time_multiplier, - 'recoil' => array_filter([ - 'decay_multiplier' => $this->recoil_decay_multiplier, - 'end_decay_multiplier' => $this->recoil_end_decay_multiplier, - 'fire_recoil_time_multiplier' => $this->recoil_fire_recoil_time_multiplier, - 'fire_recoil_strength_first_multiplier' => $this->recoil_fire_recoil_strength_first_multiplier, - 'fire_recoil_strength_multiplier' => $this->recoil_fire_recoil_strength_multiplier, - 'angle_recoil_strength_multiplier' => $this->recoil_angle_recoil_strength_multiplier, - 'randomness_multiplier' => $this->recoil_randomness_multiplier, - 'randomness_back_push_multiplier' => $this->recoil_randomness_back_push_multiplier, - 'frontal_oscillation_rotation_multiplier' => $this->recoil_frontal_oscillation_rotation_multiplier, - 'frontal_oscillation_strength_multiplier' => $this->recoil_frontal_oscillation_strength_multiplier, - 'frontal_oscillation_decay_multiplier' => $this->recoil_frontal_oscillation_decay_multiplier, - 'frontal_oscillation_randomness_multiplier' => $this->recoil_frontal_oscillation_randomness_multiplier, - 'animated_recoil_multiplier' => $this->recoil_animated_recoil_multiplier, - ]), - 'spread' => array_filter([ - 'min_multiplier' => $this->spread_min_multiplier, - 'max_multiplier' => $this->spread_max_multiplier, - 'first_attack_multiplier' => $this->spread_first_attack_multiplier, - 'attack_multiplier' => $this->spread_attack_multiplier, - 'decay_multiplier' => $this->spread_decay_multiplier, - 'additive_modifier' => $this->spread_additive_modifier, - ]), - 'aim' => array_filter([ - 'zoom_scale' => $this->aim_zoom_scale, - 'zoom_time_scale' => $this->aim_zoom_time_scale, - ]), - 'salvage' => array_filter([ - 'salvage_speed_multiplier' => $this->spread_salvage_speed_multiplier, - 'radius_multiplier' => $this->salvage_radius_multiplier, - 'extraction_efficiency' => $this->salvage_extraction_efficiency, - ]), - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/ArmorResource.php b/app/Http/Resources/SC/ItemSpecification/ArmorResource.php deleted file mode 100644 index 81ddcb3d8..000000000 --- a/app/Http/Resources/SC/ItemSpecification/ArmorResource.php +++ /dev/null @@ -1,44 +0,0 @@ - $this->signal_infrared, - 'signal_electromagnetic' => $this->signal_electromagnetic, - 'signal_cross_section' => $this->signal_cross_section, - 'damage_physical' => $this->damage_physical, - 'damage_energy' => $this->damage_energy, - 'damage_distortion' => $this->damage_distortion, - 'damage_thermal' => $this->damage_thermal, - 'damage_biochemical' => $this->damage_biochemical, - 'damage_stun' => $this->damage_stun, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/BombResource.php b/app/Http/Resources/SC/ItemSpecification/BombResource.php deleted file mode 100644 index 7a652bace..000000000 --- a/app/Http/Resources/SC/ItemSpecification/BombResource.php +++ /dev/null @@ -1,46 +0,0 @@ - $this->arm_time, - 'ignite_time' => $this->ignite_time, - 'collision_delay_time' => $this->collision_delay_time, - 'explosion_safety_distance' => $this->explosion_safety_distance, - 'explosion_radius_min' => $this->explosion_radius_min, - 'explosion_radius_max' => $this->explosion_radius_max, - 'damage' => $this->damage ?? 0, - 'damages' => WeaponDamageResource::collection($this->whenLoaded('damages')), - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/CoolerResource.php b/app/Http/Resources/SC/ItemSpecification/CoolerResource.php deleted file mode 100644 index 78451f677..000000000 --- a/app/Http/Resources/SC/ItemSpecification/CoolerResource.php +++ /dev/null @@ -1,31 +0,0 @@ - $this->cooling_rate, - 'suppression_ir_factor' => $this->suppression_ir_factor, - 'suppression_heat_factor' => $this->suppression_heat_factor, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/EmpResource.php b/app/Http/Resources/SC/ItemSpecification/EmpResource.php deleted file mode 100644 index f5a100a88..000000000 --- a/app/Http/Resources/SC/ItemSpecification/EmpResource.php +++ /dev/null @@ -1,33 +0,0 @@ - $this->charge_duration, - 'emp_radius' => $this->emp_radius, - 'unleash_duration' => $this->unleash_duration, - 'cooldown_duration' => $this->cooldown_duration, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/FlightControllerResource.php b/app/Http/Resources/SC/ItemSpecification/FlightControllerResource.php deleted file mode 100644 index 127814e80..000000000 --- a/app/Http/Resources/SC/ItemSpecification/FlightControllerResource.php +++ /dev/null @@ -1,35 +0,0 @@ - $this->scm_speed, - 'max_speed' => $this->max_speed, - 'pitch' => $this->pitch, - 'yaw' => $this->yaw, - 'roll' => $this->roll, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/FuelIntakeResource.php b/app/Http/Resources/SC/ItemSpecification/FuelIntakeResource.php deleted file mode 100644 index b4ec834b8..000000000 --- a/app/Http/Resources/SC/ItemSpecification/FuelIntakeResource.php +++ /dev/null @@ -1,29 +0,0 @@ - $this->fuel_push_rate, - 'minimum_rate' => $this->minimum_rate, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/FuelTankResource.php b/app/Http/Resources/SC/ItemSpecification/FuelTankResource.php deleted file mode 100644 index e8463a028..000000000 --- a/app/Http/Resources/SC/ItemSpecification/FuelTankResource.php +++ /dev/null @@ -1,31 +0,0 @@ - $this->fill_rate, - 'drain_rate' => $this->drain_rate, - 'capacity' => $this->capacity, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/HackingChipResource.php b/app/Http/Resources/SC/ItemSpecification/HackingChipResource.php deleted file mode 100644 index ee6343751..000000000 --- a/app/Http/Resources/SC/ItemSpecification/HackingChipResource.php +++ /dev/null @@ -1,31 +0,0 @@ - $this->max_charges, - 'duration_multiplier' => $this->duration_multiplier, - 'error_chance' => $this->error_chance, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/MiningLaser/MiningLaserModifierResource.php b/app/Http/Resources/SC/ItemSpecification/MiningLaser/MiningLaserModifierResource.php deleted file mode 100644 index eaae546ed..000000000 --- a/app/Http/Resources/SC/ItemSpecification/MiningLaser/MiningLaserModifierResource.php +++ /dev/null @@ -1,31 +0,0 @@ - Str::snake($this->name), - 'display_name' => $this->name, - 'value' => $this->value, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/MiningLaser/MiningLaserResource.php b/app/Http/Resources/SC/ItemSpecification/MiningLaser/MiningLaserResource.php deleted file mode 100644 index 13316e266..000000000 --- a/app/Http/Resources/SC/ItemSpecification/MiningLaser/MiningLaserResource.php +++ /dev/null @@ -1,74 +0,0 @@ - $this->power_transfer, - 'optimal_range' => $this->optimal_range, - 'maximum_range' => $this->maximum_range, - 'extraction_throughput' => $this->extraction_throughput, - 'module_slots' => $this->module_slots, - 'extraction_laser_power' => $this->extraction_laser_power, - 'mining_laser_power' => $this->mining_laser_power, - 'modifiers' => MiningLaserModifierResource::collection($this->modifiers), - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/MiningModuleResource.php b/app/Http/Resources/SC/ItemSpecification/MiningModuleResource.php deleted file mode 100644 index edaf7ac3f..000000000 --- a/app/Http/Resources/SC/ItemSpecification/MiningModuleResource.php +++ /dev/null @@ -1,38 +0,0 @@ - $this->getDescriptionDatum('Item Type'), - 'uses' => (int) $this->uses, - 'duration' => $this->duration, - 'modifiers' => MiningLaserModifierResource::collection($this->modifiers), - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/MissileResource.php b/app/Http/Resources/SC/ItemSpecification/MissileResource.php deleted file mode 100644 index b9a91da28..000000000 --- a/app/Http/Resources/SC/ItemSpecification/MissileResource.php +++ /dev/null @@ -1,56 +0,0 @@ - $this->cluster_size, - 'signal_type' => $this->getDescriptionDatum('Tracking Signal'), - 'lock_time' => $this->lock_time, - 'lock_range_max' => $this->lock_range_max, - 'lock_range_min' => $this->lock_range_min, - 'lock_angle' => $this->lock_angle, - 'tracking_signal_min' => $this->tracking_signal_min, - 'speed' => $this->speed, - 'fuel_tank_size' => $this->fuel_tank_size, - 'explosion_radius_min' => $this->explosion_radius_min, - 'explosion_radius_max' => $this->explosion_radius_max, - 'damage_total' => $this->damage ?? 0, - 'damages' => WeaponDamageResource::collection($this->whenLoaded('damages')), - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/PowerPlantResource.php b/app/Http/Resources/SC/ItemSpecification/PowerPlantResource.php deleted file mode 100644 index 724c5fd0c..000000000 --- a/app/Http/Resources/SC/ItemSpecification/PowerPlantResource.php +++ /dev/null @@ -1,27 +0,0 @@ - $this->power_output, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/QuantumDrive/QuantumDriveModeResource.php b/app/Http/Resources/SC/ItemSpecification/QuantumDrive/QuantumDriveModeResource.php deleted file mode 100644 index 0d98831fa..000000000 --- a/app/Http/Resources/SC/ItemSpecification/QuantumDrive/QuantumDriveModeResource.php +++ /dev/null @@ -1,51 +0,0 @@ - sprintf('%s_jump', $this->type), - 'drive_speed' => $this->drive_speed, - 'cooldown_time' => $this->cooldown_time, - 'stage_one_accel_rate' => $this->stage_one_accel_rate, - 'stage_two_accel_rate' => $this->stage_two_accel_rate, - 'engage_speed' => $this->engage_speed, - 'interdiction_effect_time' => $this->interdiction_effect_time, - 'calibration_rate' => $this->calibration_rate, - 'min_calibration_requirement' => $this->min_calibration_requirement, - 'max_calibration_requirement' => $this->max_calibration_requirement, - 'calibration_process_angle_limit' => $this->calibration_process_angle_limit, - 'calibration_warning_angle_limit' => $this->calibration_warning_angle_limit, - 'spool_up_time' => $this->spool_up_time, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/QuantumDrive/QuantumDriveResource.php b/app/Http/Resources/SC/ItemSpecification/QuantumDrive/QuantumDriveResource.php deleted file mode 100644 index dd9075403..000000000 --- a/app/Http/Resources/SC/ItemSpecification/QuantumDrive/QuantumDriveResource.php +++ /dev/null @@ -1,51 +0,0 @@ - $this->quantum_fuel_requirement, - 'jump_range' => $this->jump_range, - 'disconnect_range' => $this->disconnect_range, - 'thermal_energy_draw' => [ - 'pre_ramp_up' => $this->pre_ramp_up_thermal_energy_draw, - 'ramp_up' => $this->ramp_up_thermal_energy_draw, - 'in_flight' => $this->in_flight_thermal_energy_draw, - 'ramp_down' => $this->ramp_down_thermal_energy_draw, - 'post_ramp_down' => $this->post_ramp_down_thermal_energy_draw, - ], - 'modes' => QuantumDriveModeResource::collection($this->modes), - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/QuantumInterdictionGeneratorResource.php b/app/Http/Resources/SC/ItemSpecification/QuantumInterdictionGeneratorResource.php deleted file mode 100644 index d3440f6a5..000000000 --- a/app/Http/Resources/SC/ItemSpecification/QuantumInterdictionGeneratorResource.php +++ /dev/null @@ -1,35 +0,0 @@ - $this->jammer_range, - 'interdiction_range' => $this->interdiction_range, - 'charge_duration' => $this->charge_duration, - 'discharge_duration' => $this->discharge_duration, - 'cooldown_duration' => $this->cooldown_duration, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/SalvageModifierResource.php b/app/Http/Resources/SC/ItemSpecification/SalvageModifierResource.php deleted file mode 100644 index fe9295a4d..000000000 --- a/app/Http/Resources/SC/ItemSpecification/SalvageModifierResource.php +++ /dev/null @@ -1,31 +0,0 @@ - $this->salvage_speed_multiplier, - 'radius_multiplier' => $this->radius_multiplier, - 'extraction_efficiency' => $this->extraction_efficiency, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/SelfDestructResource.php b/app/Http/Resources/SC/ItemSpecification/SelfDestructResource.php deleted file mode 100644 index 807ccb062..000000000 --- a/app/Http/Resources/SC/ItemSpecification/SelfDestructResource.php +++ /dev/null @@ -1,37 +0,0 @@ - $this->damage, - 'radius' => $this->radius, - 'min_radius' => $this->min_radius, - 'phys_radius' => $this->phys_radius, - 'min_phys_radius' => $this->min_phys_radius, - 'time' => $this->time, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/ShieldResource.php b/app/Http/Resources/SC/ItemSpecification/ShieldResource.php deleted file mode 100644 index f9a1d5f6a..000000000 --- a/app/Http/Resources/SC/ItemSpecification/ShieldResource.php +++ /dev/null @@ -1,39 +0,0 @@ - $this->max_shield_health, - 'max_shield_regen' => $this->max_shield_regen, - 'decay_ratio' => $this->decay_ratio, - 'regen_delay' => [ - 'downed' => $this->downed_regen_delay, - 'damage' => $this->damage_regen_delay, - ], - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/ThrusterResource.php b/app/Http/Resources/SC/ItemSpecification/ThrusterResource.php deleted file mode 100644 index 0bdcc4163..000000000 --- a/app/Http/Resources/SC/ItemSpecification/ThrusterResource.php +++ /dev/null @@ -1,33 +0,0 @@ - $this->thrust_capacity, - 'min_health_thrust_multiplier' => $this->min_health_thrust_multiplier, - 'fuel_burn_per_10k_newton' => $this->fuel_burn_per_10k_newton, - 'type' => $this->type, - ]; - } -} diff --git a/app/Http/Resources/SC/ItemSpecification/TractorBeamResource.php b/app/Http/Resources/SC/ItemSpecification/TractorBeamResource.php deleted file mode 100644 index 60fa7041b..000000000 --- a/app/Http/Resources/SC/ItemSpecification/TractorBeamResource.php +++ /dev/null @@ -1,45 +0,0 @@ - $this->min_force, - 'max_force' => $this->max_force, - 'min_distance' => $this->min_distance, - 'max_distance' => $this->max_distance, - 'full_strength_distance' => $this->full_strength_distance, - 'max_angle' => $this->max_angle, - 'max_volume' => $this->max_volume, - 'volume_force_coefficient' => $this->volume_force_coefficient, - 'tether_break_time' => $this->tether_break_time, - 'safe_range_value_factor' => $this->safe_range_value_factor, - ]; - } -} diff --git a/app/Http/Resources/SC/Manufacturer/ManufacturerLinkResource.php b/app/Http/Resources/SC/Manufacturer/ManufacturerLinkResource.php deleted file mode 100644 index 8444db89d..000000000 --- a/app/Http/Resources/SC/Manufacturer/ManufacturerLinkResource.php +++ /dev/null @@ -1,47 +0,0 @@ -get('include', ''); - if (empty($include)) { - $include = ''; - } - - return [ - 'name' => $this->name, - 'code' => $this->code, - 'link' => $this->makeApiUrl(self::MANUFACTURERS_SHOW, urlencode($this->name)), - $this->mergeWhen(str_contains($include, 'counts'), fn () => [ - 'ships_count' => $this->shipsCount(), - 'vehicles_count' => $this->groundVehiclesCount(), - 'items_count' => $this->itemsCount(), - ]), - ]; - } -} diff --git a/app/Http/Resources/SC/Manufacturer/ManufacturerResource.php b/app/Http/Resources/SC/Manufacturer/ManufacturerResource.php deleted file mode 100644 index 1d39dd10c..000000000 --- a/app/Http/Resources/SC/Manufacturer/ManufacturerResource.php +++ /dev/null @@ -1,60 +0,0 @@ - $this->name, - 'code' => $this->code, - 'uuid' => $this->uuid, - 'ships' => VehicleLinkResource::collection($this->ships()), - 'vehicles' => VehicleLinkResource::collection($this->groundVehicles()), - 'items' => ItemLinkResource::collection($this->items()), - ]; - } -} diff --git a/app/Http/Resources/SC/MeleeCombatConfig/MeleeCombatConfigResource.php b/app/Http/Resources/SC/MeleeCombatConfig/MeleeCombatConfigResource.php deleted file mode 100644 index cd4ec45e7..000000000 --- a/app/Http/Resources/SC/MeleeCombatConfig/MeleeCombatConfigResource.php +++ /dev/null @@ -1,49 +0,0 @@ - $this->category, - 'damage' => $this->damage, - 'stun_recovery_modifier' => $this->stun_recovery_modifier, - 'block_stun_reduction_modifier' => $this->block_stun_reduction_modifier, - 'block_stun_stamina_modifier' => $this->block_stun_stamina_modifier, - 'attack_impulse' => $this->attack_impulse, - 'ignore_body_part_impulse_scale' => $this->ignore_body_part_impulse_scale, - 'fullbody_animation' => $this->fullbody_animation, - 'damages' => MeleeDamageResource::collection($this->damages), - ]; - } -} diff --git a/app/Http/Resources/SC/MeleeCombatConfig/MeleeDamageResource.php b/app/Http/Resources/SC/MeleeCombatConfig/MeleeDamageResource.php deleted file mode 100644 index 177115e32..000000000 --- a/app/Http/Resources/SC/MeleeCombatConfig/MeleeDamageResource.php +++ /dev/null @@ -1,30 +0,0 @@ - $this->name, - 'damage' => $this->damage, - ]; - } -} diff --git a/app/Http/Resources/SC/Mission/DeadlineResource.php b/app/Http/Resources/SC/Mission/DeadlineResource.php deleted file mode 100644 index 7101ef89f..000000000 --- a/app/Http/Resources/SC/Mission/DeadlineResource.php +++ /dev/null @@ -1,31 +0,0 @@ - $this->mission_completion_time, - 'mission_auto_end' => $this->mission_auto_end, - 'mission_result_after_timer_end' => $this->mission_result_after_timer_end, - 'mission_end_reason' => $this->mission_end_reason, - ]; - } -} diff --git a/app/Http/Resources/SC/Mission/GiverLinkResource.php b/app/Http/Resources/SC/Mission/GiverLinkResource.php deleted file mode 100644 index 2490df63f..000000000 --- a/app/Http/Resources/SC/Mission/GiverLinkResource.php +++ /dev/null @@ -1,29 +0,0 @@ - $this->uuid, - 'name' => $this->name, - 'link' => $this->makeApiUrl(self::MISSION_GIVERS_SHOW, $this->uuid), - ]; - } -} diff --git a/app/Http/Resources/SC/Mission/GiverResource.php b/app/Http/Resources/SC/Mission/GiverResource.php deleted file mode 100644 index 3e5c30991..000000000 --- a/app/Http/Resources/SC/Mission/GiverResource.php +++ /dev/null @@ -1,58 +0,0 @@ - $this->uuid, - 'name' => $this->name, - 'headquarters' => $this->headquarters, - 'invitation_timeout' => $this->invitation_timeout, - 'visit_timeout' => $this->visit_timeout, - 'short_cooldown' => $this->short_cooldown, - 'medium_cooldown' => $this->medium_cooldown, - 'long_cooldown' => $this->long_cooldown, - 'description' => $this->getTranslation($this, $request), - 'missions' => $this->whenLoaded('missions', fn () => MissionLinkResource::collection($this->missions)), - ]; - } -} diff --git a/app/Http/Resources/SC/Mission/MissionLinkResource.php b/app/Http/Resources/SC/Mission/MissionLinkResource.php deleted file mode 100644 index d6f9258a5..000000000 --- a/app/Http/Resources/SC/Mission/MissionLinkResource.php +++ /dev/null @@ -1,29 +0,0 @@ - $this->uuid, - 'title' => $this->titleHUD ?? $this->title, - 'link' => $this->makeApiUrl(self::MISSIONS_SHOW, $this->uuid), - ]; - } -} diff --git a/app/Http/Resources/SC/Mission/MissionResource.php b/app/Http/Resources/SC/Mission/MissionResource.php deleted file mode 100644 index f38ca6d5f..000000000 --- a/app/Http/Resources/SC/Mission/MissionResource.php +++ /dev/null @@ -1,140 +0,0 @@ - $this->uuid, - 'not_for_release' => $this->not_for_release, - 'title' => $this->title, - 'title_hud' => $this->title_hud, - 'description' => $this->getTranslation($this, $request), - 'mission_giver' => $this->mission_giver, - $this->mergeWhen($this->giver->uuid !== null, [ - 'mission_giver_record' => new GiverResource($this->giver), - ]), - 'comms_channel_name' => $this->comms_channel_name, - 'type' => $this->type->name, - 'locality_available' => $this->locality_available, - 'location_mission_available' => $this->location_mission_available, - 'initially_active' => $this->initially_active, - 'notify_on_available' => $this->notify_on_available, - 'show_as_offer' => $this->show_as_offer, - 'mission_buy_in_amount' => $this->mission_buy_in_amount, - 'refund_buy_in_on_withdraw' => $this->refund_buy_in_on_withdraw, - 'has_complete_button' => $this->has_complete_button, - 'handles_abandon_request' => $this->handles_abandon_request, - 'mission_module_per_player' => $this->mission_module_per_player, - 'max_instances' => $this->max_instances, - 'max_players_per_instance' => $this->max_players_per_instance, - 'max_instances_per_player' => $this->max_instances_per_player, - 'can_be_shared' => $this->can_be_shared, - 'once_only' => $this->once_only, - 'tutorial' => $this->tutorial, - 'display_allied_markers' => $this->display_allied_markers, - 'available_in_prison' => $this->available_in_prison, - 'fail_if_sent_to_prison' => $this->fail_if_sent_to_prison, - 'fail_if_became_criminal' => $this->fail_if_became_criminal, - 'fail_if_leave_prison' => $this->fail_if_leave_prison, - 'request_only' => $this->request_only, - 'respawn_time' => $this->respawn_time, - 'respawn_time_variation' => $this->respawn_time_variation, - 'instance_has_life_time' => $this->instance_has_life_time, - 'show_life_time_in_mobi_glas' => $this->show_life_time_in_mobi_glas, - 'instance_life_time' => $this->instance_life_time, - 'instance_life_time_variation' => $this->instance_life_time_variation, - 'can_reaccept_after_abandoning' => $this->can_reaccept_after_abandoning, - 'abandoned_cooldown_time' => $this->abandoned_cooldown_time, - 'abandoned_cooldown_time_variation' => $this->abandoned_cooldown_time_variation, - 'can_reaccept_after_failing' => $this->can_reaccept_after_failing, - 'has_personal_cooldown' => $this->has_personal_cooldown, - 'personal_cooldown_time' => $this->personal_cooldown_time, - 'personal_cooldown_time_variation' => $this->personal_cooldown_time_variation, - 'module_handles_own_shutdown' => $this->module_handles_own_shutdown, - 'linked_mission' => $this->makeApiUrl(self::MISSIONS_SHOW, $this->linked_mission), - 'lawful_mission' => $this->lawful_mission, - 'deadline' => DeadlineResource::make($this->deadline)->resolve($request), - 'reward' => RewardResource::make($this->reward)->resolve($request), - 'invitation_mission' => $this->invitation_mission, - 'associated_missions' => MissionLinkResource::collection($this->associatedMissions), - 'required_missions' => MissionLinkResource::collection($this->requiredMissions), - 'version' => $this->version, - ]; - } -} diff --git a/app/Http/Resources/SC/Mission/RewardResource.php b/app/Http/Resources/SC/Mission/RewardResource.php deleted file mode 100644 index cdda51108..000000000 --- a/app/Http/Resources/SC/Mission/RewardResource.php +++ /dev/null @@ -1,33 +0,0 @@ - $this->amount, - 'max' => $this->max, - 'plus_bonuses' => $this->plus_bonuses, - 'currency' => $this->currency, - 'reputation_bonus' => $this->reputation_bonus, - ]; - } -} diff --git a/app/Http/Resources/SC/Shop/ShopItemResource.php b/app/Http/Resources/SC/Shop/ShopItemResource.php deleted file mode 100644 index 786983f16..000000000 --- a/app/Http/Resources/SC/Shop/ShopItemResource.php +++ /dev/null @@ -1,85 +0,0 @@ - $this->uuid, - 'name' => $this->name, - 'type' => $this->cleanType(), - 'sub_type' => $this->sub_type, - 'base_price' => $this->shop_data->base_price, - 'price_calculated' => $this->shop_data->offsetted_price, - 'price_range' => $this->shop_data->price_range, - 'base_price_offset' => $this->shop_data->base_price_offset, - 'max_discount' => $this->shop_data->max_discount, - 'max_premium' => $this->shop_data->max_premium, - 'inventory' => $this->shop_data->inventory, - 'optimal_inventory' => $this->shop_data->optimal_inventory, - 'max_inventory' => $this->shop_data->max_inventory, - 'auto_restock' => $this->shop_data->auto_restock, - 'auto_consume' => $this->shop_data->auto_consume, - 'refresh_rate' => $this->shop_data->refresh_rate, - 'buyable' => $this->shop_data->buyable, - 'sellable' => $this->shop_data->sellable, - 'rentable' => $this->shop_data->rentable, - $this->mergeWhen($this->shop_data->rentable === true, [ - 'rental_price_days' => [ - 'duration_1' => $this->shop_data->price1, - 'duration_3' => $this->shop_data->price3, - 'duration_7' => $this->shop_data->price7, - 'duration_30' => $this->shop_data->price30, - ], - 'rental_percent_days' => [ - 'duration_1' => $this->shop_data->rental->percentage_1, - 'duration_3' => $this->shop_data->rental->percentage_3, - 'duration_7' => $this->shop_data->rental->percentage_7, - 'duration_30' => $this->shop_data->rental->percentage_30, - ], - ]), - 'version' => $this->shop_data->version, - ]; - } -} diff --git a/app/Http/Resources/SC/Shop/ShopLinkResource.php b/app/Http/Resources/SC/Shop/ShopLinkResource.php deleted file mode 100644 index 55cf90dea..000000000 --- a/app/Http/Resources/SC/Shop/ShopLinkResource.php +++ /dev/null @@ -1,40 +0,0 @@ - $this->uuid, - 'name_raw' => $this->name_raw, - 'name' => $this->name, - 'position' => $this->position, - 'profit_margin' => $this->profit_margin, - 'link' => $this->makeApiUrl(self::SHOPS_SHOW, $this->uuid), - 'version' => $this->version, - ]; - } -} diff --git a/app/Http/Resources/SC/Shop/ShopResource.php b/app/Http/Resources/SC/Shop/ShopResource.php deleted file mode 100644 index 807552b68..000000000 --- a/app/Http/Resources/SC/Shop/ShopResource.php +++ /dev/null @@ -1,57 +0,0 @@ -whenLoaded('items'); - if (optional($this->shop_data)->exists) { - $items = $this->items()->where('uuid', $this->shop_data->item_uuid)->get(); - } - - return [ - 'uuid' => $this->uuid, - 'name_raw' => $this->name_raw, - 'name' => $this->name, - 'position' => $this->position, - 'profit_margin' => $this->profit_margin, - 'link' => $this->makeApiUrl(self::SHOPS_SHOW, $this->uuid), - 'version' => $this->version, - 'items' => ShopItemResource::collection($items), - ]; - } -} diff --git a/app/Http/Resources/SC/Vehicle/HardpointItemResource.php b/app/Http/Resources/SC/Vehicle/HardpointItemResource.php deleted file mode 100644 index 5fed46816..000000000 --- a/app/Http/Resources/SC/Vehicle/HardpointItemResource.php +++ /dev/null @@ -1,79 +0,0 @@ -uuid === null) { - return []; - } - - $vehicleItem = $this->vehicleItem; - - return [ - 'uuid' => $this->uuid, - 'name' => $this->name, - 'class_name' => $this->class_name, - 'link' => $this->makeApiUrl(self::ITEMS_SHOW, $this->uuid), - 'size' => $this->size, - 'mass' => $this->mass, - 'grade' => $vehicleItem->grade, - 'class' => $vehicleItem->class, - 'manufacturer' => new ManufacturerLinkResource($this->manufacturer), - 'type' => $this->cleanType(), - 'sub_type' => $this->sub_type, - $this->mergeWhen($this->isTurret(), fn () => $this->addTurretData()), - $this->mergeWhen(...$this->addSpecification()), - $this->mergeWhen($this->container->exists, fn () => [ - 'inventory' => new ItemContainerResource($this->container), - ]), - 'ports' => ItemPortResource::collection($this->whenLoaded('ports')), - 'updated_at' => $this->updated_at, - 'version' => $this->version, - ]; - } -} diff --git a/app/Http/Resources/SC/Vehicle/HardpointItemResourceV3.php b/app/Http/Resources/SC/Vehicle/HardpointItemResourceV3.php deleted file mode 100644 index f9a3541e2..000000000 --- a/app/Http/Resources/SC/Vehicle/HardpointItemResourceV3.php +++ /dev/null @@ -1,79 +0,0 @@ -uuid === null) { - return []; - } - - $vehicleItem = $this->vehicleItem; - - return [ - 'uuid' => $this->uuid, - 'name' => $this->name, - 'class_name' => $this->class_name, - 'link' => $this->makeApiUrl(self::ITEMS_SHOW, $this->uuid), - 'size' => $this->size, - 'mass' => $this->mass, - 'grade' => $vehicleItem->grade, - 'class' => $vehicleItem->class, - 'manufacturer' => new ManufacturerLinkResource($this->manufacturer), - 'type' => $this->cleanType(), - 'sub_type' => $this->sub_type, - $this->mergeWhen($this->isTurret(), fn () => $this->addTurretData()), - $this->mergeWhen(...$this->addSpecification()), - $this->mergeWhen($this->container->exists, fn () => [ - 'inventory' => new ItemContainerResource($this->container), - ]), - 'ports' => ItemPortResource::collection($this->whenLoaded('ports')), - 'updated_at' => $this->updated_at, - 'version' => $this->version, - ]; - } -} diff --git a/app/Http/Resources/SC/Vehicle/HardpointResource.php b/app/Http/Resources/SC/Vehicle/HardpointResource.php deleted file mode 100644 index 505ed50e0..000000000 --- a/app/Http/Resources/SC/Vehicle/HardpointResource.php +++ /dev/null @@ -1,101 +0,0 @@ -equipped_item_uuid); - - if ($hasItem) { - $this->load('item'); - } - - $data = [ - 'name' => $this->hardpoint_name, - 'position' => $this->position, - 'min_size' => $this->min_size, - 'max_size' => $this->max_size, - 'class_name' => $this->class_name, - 'health' => $hasItem ? $this->item?->durabilityData?->health : null, - 'type' => $hasItem ? $this->item?->type : null, - 'sub_type' => $hasItem ? $this->item?->sub_type : null, - $this->mergeWhen(...$this->addItem()), - $this->mergeWhen($this->children !== null && $this->children->count() > 0, fn () => [ - 'children' => self::collection($this->children), - ]), - ]; - - if ($hasItem) { - $data += [ - 'type' => $this->item->type, - 'sub_type' => $this->item->sub_type, - ]; - - if ($this->min_size === 0) { - $data['min_size'] = $this->item->size; - $data['max_size'] = $this->item->size; - } - } - - return $data; - } - - private function addItem(): array - { - if (empty($this->equipped_item_uuid)) { - return [false, []]; - } - - if ( - $this->vehicleItem->exists || - ($this->item !== null && ($this->item->exists || $this->item->isTurret() || $this->item->type === 'Cargo')) - ) { - return [true, fn () => ['item' => new HardpointItemResource($this->item)]]; - } - - return [false, []]; - } -} diff --git a/app/Http/Resources/SC/Vehicle/HardpointResourceV3.php b/app/Http/Resources/SC/Vehicle/HardpointResourceV3.php deleted file mode 100644 index 0a2b684c9..000000000 --- a/app/Http/Resources/SC/Vehicle/HardpointResourceV3.php +++ /dev/null @@ -1,103 +0,0 @@ -equipped_item_uuid); - - if ($hasItem) { - $this->load('item'); - } - - $data = [ - 'name' => $this->hardpoint_name, - 'position' => $this->position, - 'sizes' => [ - 'min' => $this->min_size, - 'max' => $this->max_size, - ], - 'class_name' => $this->class_name, - 'health' => $hasItem ? $this->item?->durabilityData?->health : null, - 'compatible_types' => $hasItem ? array_filter([ - array_filter([ - 'type' => $this->item?->type, - 'sub_types' => array_filter([$this->item?->sub_type]), - ]), - ]) : null, - $this->mergeWhen(...$this->addItem()), - $this->mergeWhen($this->children !== null && $this->children->count() > 0, fn () => [ - 'ports' => self::collection($this->children), - ]), - ]; - - if ($hasItem && $this->min_size === 0) { - $data['sizes']['min'] = $this->item->size; - $data['sizes']['max'] = $this->item->size; - } - - return $data; - } - - private function addItem(): array - { - if (empty($this->equipped_item_uuid)) { - return [false, []]; - } - - if ( - $this->vehicleItem->exists || - ($this->item !== null && ($this->item->exists || $this->item->isTurret() || $this->item->type === 'Cargo')) - ) { - return [true, fn () => ['equipped_item' => new HardpointItemResourceV3($this->item)]]; - } - - return [false, []]; - } -} diff --git a/app/Http/Resources/SC/Vehicle/VehicleCargoGrid.php b/app/Http/Resources/SC/Vehicle/VehicleCargoGrid.php deleted file mode 100644 index 1259add78..000000000 --- a/app/Http/Resources/SC/Vehicle/VehicleCargoGrid.php +++ /dev/null @@ -1,69 +0,0 @@ - $this->capacity, - 'capacity_name' => $this->unit_name, - 'is_open' => $this->is_open, - 'is_external' => $this->is_external, - 'is_closed' => $this->is_closed, - 'x' => $this->x, - 'y' => $this->y, - 'z' => $this->z, - 'min_size' => [ - 'x' => $this->min_x, - 'y' => $this->min_y, - 'z' => $this->min_z, - ], - 'max_size' => [ - 'x' => $this->max_x, - 'y' => $this->max_y, - 'z' => $this->max_z, - ], - ]; - } -} diff --git a/app/Http/Resources/SC/Vehicle/VehicleItemLinkResource.php b/app/Http/Resources/SC/Vehicle/VehicleItemLinkResource.php deleted file mode 100644 index aa6d14fc0..000000000 --- a/app/Http/Resources/SC/Vehicle/VehicleItemLinkResource.php +++ /dev/null @@ -1,51 +0,0 @@ - $this->uuid, - 'name' => $this->name, - 'type' => $this->type, - 'grade' => $this->vehicleItem->grade, - 'class' => $this->vehicleItem->class, - 'manufacturer' => new ManufacturerLinkResource($this->manufacturer), - 'link' => $this->makeApiUrl(self::ITEMS_SHOW, $this->uuid), - 'shops' => ShopResource::collection($this->whenLoaded('shops')), - 'updated_at' => $this->updated_at, - 'version' => $this->version, - ]; - } -} diff --git a/app/Http/Resources/SC/Vehicle/VehicleItemResource.php b/app/Http/Resources/SC/Vehicle/VehicleItemResource.php deleted file mode 100644 index ab6342d35..000000000 --- a/app/Http/Resources/SC/Vehicle/VehicleItemResource.php +++ /dev/null @@ -1,155 +0,0 @@ - $this->item_uuid ?? $this->sc?->item_uuid, - 'name' => $this->name, - 'link' => $this->makeApiUrl(self::VEHICLES_SHOW, ($this->item_uuid ?? $this->sc?->item_uuid ?? urlencode($this->name))), - 'updated_at' => $this->updated_at, - 'version' => $this->version ?? $this->sc?->version, - ]; - } -} diff --git a/app/Http/Resources/SC/Vehicle/VehiclePartResource.php b/app/Http/Resources/SC/Vehicle/VehiclePartResource.php deleted file mode 100644 index bdd5b0f4b..000000000 --- a/app/Http/Resources/SC/Vehicle/VehiclePartResource.php +++ /dev/null @@ -1,33 +0,0 @@ - $this->name, - 'display_name' => $this->display_name, - 'damage_max' => $this->damage_max, - 'children' => self::collection($this->whenLoaded('children')), - ]; - } -} diff --git a/app/Http/Resources/SC/Vehicle/VehicleResource.php b/app/Http/Resources/SC/Vehicle/VehicleResource.php deleted file mode 100644 index db74c35c7..000000000 --- a/app/Http/Resources/SC/Vehicle/VehicleResource.php +++ /dev/null @@ -1,599 +0,0 @@ -get('include', '') ?? '')) - ->map('trim') - ->map('strtolower') - ->toArray(); - - $hardpoints = []; - - if (in_array('hardpoints', $includes, true)) { - if ($request->has('filter') && isset($request->get('filter')['hardpoints'])) { - /** @var HasMany $hardpoints */ - $hardpoints = $this->hardpointsWithoutParent(); - - $filters = collect(explode(',', $request->get('filter')['hardpoints'])) - ->map(fn (string $filter) => trim($filter)); - - $remove = $filters - ->filter(fn (string $filter) => str_starts_with($filter, '!')) - ->map(fn (string $filter) => ltrim($filter, '!')) - ->toArray(); - - $include = $filters - ->filter(fn (string $filter) => ! str_starts_with($filter, '!')) - ->toArray(); - - if (! empty($remove)) { - $hardpoints->whereRelation('item', function (Builder $query) use ($remove) { - $query->whereNotIn('type', $remove); - }); - } elseif (! empty($include)) { - $hardpoints->whereRelation('item', function (Builder $query) use ($include) { - $query->whereIn('type', $include); - }); - } - - $hardpoints = $hardpoints->get(); - } else { - $hardpoints = $this->hardpointsWithoutParent; - } - } - - $manufacturer = $this->item->manufacturer->name; - if ($manufacturer === 'Unknown Manufacturer') { - $manufacturer = $this->description_manufacturer; - } - - $cargoGrids = VehicleCargoGrid::collection($this->cargoGrids); - - $data = [ - 'uuid' => $this->item_uuid, - 'name' => $this->name, - 'slug' => Str::slug($this->name), - 'class_name' => $this->class_name, - 'sizes' => [ - 'length' => (float) $this->length, - 'beam' => (float) $this->width, - 'height' => (float) $this->height, - ], - 'emission' => [ - 'ir' => $this->ir_emission, - 'em_idle' => $this->em_emission['min'] ?? null, - 'em_max' => $this->em_emission['max'] ?? null, - ], - 'mass' => $this->mass, - 'cargo_capacity' => $this->cargo_capacity, - 'cargo_grids' => $cargoGrids, - 'cargo_limits' => self::calculateCargoGridSizeLimits(collect($cargoGrids->resolve())), - // 'cargo_capacity_calculated' => $this->scu, - 'vehicle_inventory' => $this->vehicle_inventory_scu, - 'personal_inventory' => $this->personal_inventory_scu, - - 'crew' => [ - 'min' => $this->crew, - 'max' => null, - 'weapon' => $this->weapon_crew, - 'operation' => $this->operation_crew, - ], - 'health' => $this->health, - 'shield_hp' => $this->shield_hp, - 'shield_face_type' => $this->shield_face_type, - 'speed' => [ - 'scm' => $this->flightController?->scm_speed, - 'max' => $this->flightController?->max_speed ?? $this->handling?->max_speed, - - 'scm_boost_forward' => $this->flightController?->scm_boost_forward, - 'scm_boost_backward' => $this->flightController?->scm_boost_backward, - - 'zero_to_scm' => $this->zero_to_scm, - 'zero_to_max' => $this->handling?->zero_to_max ?? $this->zero_to_max, - 'scm_to_zero' => $this->scm_to_zero, - 'max_to_zero' => $this->handling?->max_to_zero ?? $this->max_to_zero, - // Ground Vehicles - $this->mergeWhen($this->handling !== null, fn () => [ - 'reverse' => $this->handling?->reverse_speed, - ]), - ], - 'afterburner' => [ - 'pitch_boost_multiplier' => $this->flightController?->pitch_boost_multiplier, - 'roll_boost_multiplier' => $this->flightController?->roll_boost_multiplier, - 'yaw_boost_multiplier' => $this->flightController?->yaw_boost_multiplier, - 'capacitor' => $this->flightController?->afterburner_capacitor, - 'idle_cost' => $this->flightController?->afterburner_idle_cost, - 'linear_cost' => $this->flightController?->afterburner_linear_cost, - 'angular_cost' => $this->flightController?->afterburner_angular_cost, - 'regen_per_sec' => $this->flightController?->afterburner_regen_per_sec, - 'regen_delay_after_use' => $this->flightController?->afterburner_regen_delay_after_use, - 'pre_delay_time' => $this->flightController?->afterburner_pre_delay_time, - 'ramp_up_time' => $this->flightController?->afterburner_ramp_up_time, - 'ramp_down_time' => $this->flightController?->afterburner_ramp_down_time, - ], - 'fuel' => [ - 'capacity' => $this->fuel_capacity, - 'intake_rate' => $this->fuel_intake_rate, - 'usage' => [ - 'main' => $this->getFuelUsage(), - 'maneuvering' => $this->getFuelUsage('ManneuverThruster'), - 'retro' => $this->getFuelUsage('RetroThruster'), - 'vtol' => $this->getFuelUsage('VtolThruster'), - ], - ], - $this->mergeWhen(...$this->getQuantumDriveData()), - 'agility' => [ - 'pitch' => $this->flightController?->pitch, - 'yaw' => $this->flightController?->yaw, - 'roll' => $this->flightController?->roll, - // Ground Vehicles - $this->mergeWhen($this->handling !== null, fn () => [ - 'v0_steer_max' => $this->handling?->v0_steer_max, - 'kv_steer_max' => $this->handling?->kv_steer_max, - 'vmax_steer_max' => $this->handling?->vmax_steer_max, - 'deceleration' => [ - 'main' => $this->handling?->deceleration, - ], - ]), - 'acceleration' => [ - 'main' => $this->handling?->acceleration ?? $this->acceleration_main, - 'retro' => $this->acceleration_retro, - 'vtol' => $this->acceleration_vtol, - 'maneuvering' => $this->acceleration_maneuvering, - - 'main_g' => $this->acceleration_g_main, - 'retro_g' => $this->acceleration_g_retro, - 'vtol_g' => $this->acceleration_g_vtol, - 'maneuvering_g' => $this->acceleration_g_maneuvering, - ], - ], - $this->mergeWhen($this->armor?->exists, fn () => [ - 'armor' => new ArmorResource($this->armor), - ]), - 'foci' => [ - [Language::ENGLISH => $this->career], - ], - 'type' => [ - Language::ENGLISH => $this->role, - ], - 'description' => TranslationResourceFactory::getTranslationResource($request, $this), - 'size_class' => $this->size, - 'manufacturer' => [ - 'name' => $manufacturer, - 'code' => $this->item->manufacturer->code, - ], - 'insurance' => [ - 'claim_time' => $this->claim_time, - 'expedite_time' => $this->expedite_time, - 'expedite_cost' => $this->expedite_cost, - ], - $this->mergeWhen(in_array('hardpoints', $includes, true), fn () => [ - 'hardpoints' => HardpointResource::collection($hardpoints), - ]), - $this->mergeWhen(in_array('shops', $includes, true), fn () => [ - 'shops' => ShopResource::collection($this->item->shops), - ]), - 'parts' => VehiclePartResource::collection($this->whenLoaded('partsWithoutParent')), - 'updated_at' => $this->updated_at, - 'version' => $this->item->version, - ]; - - $this->loadShipMatrixData($data, $request); - - return $data; - } - - private function getQuantumDriveData(): array - { - $drives = $this->quantumDrives; - - if ($drives->isEmpty()) { - return [false, []]; - } - - $modes = $drives[0]->modes->keyBy('type'); - $normal = $modes['normal']; - - return [ - true, - fn () => [ - 'quantum' => [ - 'quantum_speed' => $normal->drive_speed, - 'quantum_spool_time' => $normal->spool_up_time, - 'quantum_fuel_capacity' => $this->quantum_fuel_capacity, - 'quantum_range' => $this->quantum_fuel_capacity / ($drives[0]->quantum_fuel_requirement / 1e6), - ], - ], - ]; - } - - /** - * Adds ship-matrix information to the output - */ - private function loadShipMatrixData(array &$data, Request $request): void - { - if (! $this->vehicle->exists) { - return; - } - - $matrixVehicle = (new \App\Http\Resources\StarCitizen\Vehicle\VehicleResource($this->vehicle)) - ->resolve($request); - - $toAdd = [ - 'id', - 'chassis_id', - 'name', - 'slug', - 'foci', - 'production_status', - 'production_note', - 'type', - 'description', - 'size', - 'msrp', - 'pledge_url', - 'components', - 'acceleration.x_axis', - 'acceleration.y_axis', - 'acceleration.z_axis', - 'loaner', - 'skus', - ]; - - foreach ($toAdd as $key) { - if (! empty($matrixVehicle[$key])) { - if (str_contains($key, 'acceleration')) { - $key = explode('.', $key)[1]; - $data['acceleration'][$key] = $matrixVehicle['acceleration'][$key]; - } else { - $data[$key] = $matrixVehicle[$key]; - } - } - } - } - - public static function calculateCargoGridSizeLimits(Collection $cargoGrids): array - { - $minVolumeGrid = $cargoGrids - ->filter(fn ($grid) => isset($grid['min_size']['x'], $grid['min_size']['y'], $grid['min_size']['z'])) - ->sortBy(fn ($grid) => $grid['min_size']['x'] * $grid['min_size']['y'] * $grid['min_size']['z']) - ->first(); - - $maxVolumeGrid = $cargoGrids - ->filter(fn ($grid) => isset($grid['max_size']['x'], $grid['max_size']['y'], $grid['max_size']['z'])) - ->sortByDesc(fn ($grid) => $grid['max_size']['x'] * $grid['max_size']['y'] * $grid['max_size']['z']) - ->first(); - - return [ - 'min_size' => $minVolumeGrid['min_size'] ?? null, - 'max_size' => $maxVolumeGrid['max_size'] ?? null, - ]; - } -} diff --git a/app/Http/Resources/SC/Vehicle/VehicleResourceV3.php b/app/Http/Resources/SC/Vehicle/VehicleResourceV3.php deleted file mode 100644 index 7ce4b13df..000000000 --- a/app/Http/Resources/SC/Vehicle/VehicleResourceV3.php +++ /dev/null @@ -1,549 +0,0 @@ -get('include', '') ?? '')) - ->map('trim') - ->map('strtolower') - ->toArray(); - - $hardpoints = []; - - if (in_array('ports', $includes, true)) { - if ($request->has('filter') && isset($request->get('filter')['ports'])) { - /** @var HasMany $hardpoints */ - $hardpoints = $this->hardpointsWithoutParent(); - - $filters = collect(explode(',', $request->get('filter')['ports'])) - ->map(fn (string $filter) => trim($filter)); - - $remove = $filters - ->filter(fn (string $filter) => str_starts_with($filter, '!')) - ->map(fn (string $filter) => ltrim($filter, '!')) - ->toArray(); - - $include = $filters - ->filter(fn (string $filter) => ! str_starts_with($filter, '!')) - ->toArray(); - - if (! empty($remove)) { - $hardpoints->whereRelation('item', function (Builder $query) use ($remove) { - $query->whereNotIn('type', $remove); - }); - } elseif (! empty($include)) { - $hardpoints->whereRelation('item', function (Builder $query) use ($include) { - $query->whereIn('type', $include); - }); - } - - $hardpoints = $hardpoints->get(); - } else { - $hardpoints = $this->hardpointsWithoutParent; - } - } - - $manufacturer = $this->item->manufacturer->name; - if ($manufacturer === 'Unknown Manufacturer') { - $manufacturer = $this->description_manufacturer; - } - - $cargoGrids = VehicleCargoGrid::collection($this->cargoGrids); - - $data = [ - 'uuid' => $this->item_uuid, - 'name' => $this->name, - 'slug' => Str::slug($this->name), - 'class_name' => $this->class_name, - 'sizes' => [ - 'length' => (float) $this->length, - 'beam' => (float) $this->width, - 'height' => (float) $this->height, - ], - 'emission' => [ - 'ir' => $this->ir_emission, - 'em_idle' => $this->em_emission['min'] ?? null, - 'em_max' => $this->em_emission['max'] ?? null, - ], - 'mass' => $this->mass, - 'cargo_capacity' => $this->cargo_capacity, - 'cargo_grids' => $cargoGrids, - 'cargo_limits' => VehicleResource::calculateCargoGridSizeLimits(collect($cargoGrids->resolve())), - // 'cargo_capacity_calculated' => $this->scu, - 'vehicle_inventory' => $this->vehicle_inventory_scu, - 'personal_inventory' => $this->personal_inventory_scu, - - 'crew' => [ - 'min' => $this->crew, - 'max' => null, - 'weapon' => $this->weapon_crew, - 'operation' => $this->operation_crew, - ], - 'health' => $this->health, - 'shield_hp' => $this->shield_hp, - 'shield_face_type' => $this->shield_face_type, - 'fuel' => [ - 'capacity' => $this->fuel_capacity, - 'intake_rate' => $this->fuel_intake_rate, - 'usage' => [ - 'main' => $this->getFuelUsage(), - 'maneuvering' => $this->getFuelUsage('ManneuverThruster'), - 'retro' => $this->getFuelUsage('RetroThruster'), - 'vtol' => $this->getFuelUsage('VtolThruster'), - ], - ], - $this->mergeWhen(...$this->getQuantumDriveData()), - 'agility' => [ - 'pitch' => $this->flightController?->pitch, - 'yaw' => $this->flightController?->yaw, - 'roll' => $this->flightController?->roll, - // Ground Vehicles - $this->mergeWhen($this->handling !== null, [ - 'v0_steer_max' => $this->handling?->v0_steer_max, - 'kv_steer_max' => $this->handling?->kv_steer_max, - 'vmax_steer_max' => $this->handling?->vmax_steer_max, - 'deceleration' => [ - 'main' => $this->handling?->deceleration, - ], - ]), - 'acceleration' => [ - 'main' => $this->handling?->acceleration ?? $this->acceleration_main, - 'retro' => $this->acceleration_retro, - 'vtol' => $this->acceleration_vtol, - 'maneuvering' => $this->acceleration_maneuvering, - - 'main_g' => $this->acceleration_g_main, - 'retro_g' => $this->acceleration_g_retro, - 'vtol_g' => $this->acceleration_g_vtol, - 'maneuvering_g' => $this->acceleration_g_maneuvering, - ], - ], - $this->mergeWhen($this->armor?->exists, [ - 'armor' => new ArmorResource($this->armor), - ]), - 'foci' => [ - [Language::ENGLISH => $this->career], - ], - 'type' => [ - Language::ENGLISH => $this->role, - ], - 'description' => TranslationResourceFactory::getTranslationResource($request, $this), - 'size_class' => $this->size, - 'manufacturer' => [ - 'name' => $manufacturer, - 'code' => $this->item->manufacturer->code, - ], - 'insurance' => [ - 'claim_time' => $this->claim_time, - 'expedite_time' => $this->expedite_time, - 'expedite_cost' => $this->expedite_cost, - ], - $this->mergeWhen(in_array('ports', $includes, true), [ - 'ports' => HardpointResourceV3::collection($hardpoints), - ]), - $this->mergeWhen(in_array('shops', $includes, true), [ - 'shops' => ShopResource::collection($this->item->shops), - ]), - 'parts' => VehiclePartResource::collection($this->whenLoaded('partsWithoutParent')), - 'updated_at' => $this->updated_at, - 'version' => $this->item->version, - ]; - - $this->loadShipMatrixData($data, $request); - - return $data; - } - - private function getQuantumDriveData(): array - { - $drives = $this->quantumDrives; - - if ($drives->isEmpty()) { - return [false, []]; - } - - $modes = $drives[0]->modes->keyBy('type'); - $normal = $modes['normal']; - - return [ - true, - [ - 'quantum' => [ - 'quantum_speed' => $normal->drive_speed, - 'quantum_spool_time' => $normal->spool_up_time, - 'quantum_fuel_capacity' => $this->quantum_fuel_capacity, - 'quantum_range' => $this->quantum_fuel_capacity / ($drives[0]->quantum_fuel_requirement / 1e6), - ], - ], - ]; - } - - /** - * Adds ship-matrix information to the output - */ - private function loadShipMatrixData(array &$data, Request $request): void - { - if (! $this->vehicle->exists) { - return; - } - - $matrixVehicle = (new \App\Http\Resources\StarCitizen\Vehicle\VehicleResource($this->vehicle)) - ->resolve($request); - - $toAdd = [ - 'id', - 'chassis_id', - 'name', - 'slug', - 'foci', - 'production_status', - 'production_note', - 'type', - 'description', - 'size', - 'msrp', - 'pledge_url', - 'components', - 'acceleration.x_axis', - 'acceleration.y_axis', - 'acceleration.z_axis', - 'loaner', - 'skus', - ]; - - foreach ($toAdd as $key) { - if (! empty($matrixVehicle[$key])) { - if (str_contains($key, 'acceleration')) { - $key = explode('.', $key)[1]; - $data['acceleration'][$key] = $matrixVehicle['acceleration'][$key]; - } else { - $data[$key] = $matrixVehicle[$key]; - } - } - } - } -} diff --git a/app/Http/Resources/SC/Vehicle/Weapon/VehicleWeaponRegenResource.php b/app/Http/Resources/SC/Vehicle/Weapon/VehicleWeaponRegenResource.php deleted file mode 100644 index d4028c4f9..000000000 --- a/app/Http/Resources/SC/Vehicle/Weapon/VehicleWeaponRegenResource.php +++ /dev/null @@ -1,33 +0,0 @@ - $this->requested_regen_per_sec, - 'requested_ammo_load' => $this->requested_ammo_load, - 'cooldown' => $this->cooldown, - 'cost_per_bullet' => $this->cost_per_bullet, - ]; - } -} diff --git a/app/Http/Resources/SC/Vehicle/Weapon/VehicleWeaponResource.php b/app/Http/Resources/SC/Vehicle/Weapon/VehicleWeaponResource.php deleted file mode 100644 index 34209a083..000000000 --- a/app/Http/Resources/SC/Vehicle/Weapon/VehicleWeaponResource.php +++ /dev/null @@ -1,81 +0,0 @@ - $this->weapon_class, - 'type' => $this->weapon_type, - 'capacity' => $this->capacity ?? null, - 'range' => $this->ammunition->range ?? null, - 'damage_per_shot' => $this->ammunition->damage ?? null, - 'modes' => WeaponModeResource::collection($this->whenLoaded('modes')), - 'damages' => WeaponDamageResource::collection($this->damages()), - 'regeneration' => new VehicleWeaponRegenResource($this->whenLoaded('regen')), - 'ammunition' => new AmmunitionResource($this->ammunition), - ]; - } -} diff --git a/app/Http/Resources/SC/Weapon/WeaponDamageResource.php b/app/Http/Resources/SC/Weapon/WeaponDamageResource.php deleted file mode 100644 index c64f3db42..000000000 --- a/app/Http/Resources/SC/Weapon/WeaponDamageResource.php +++ /dev/null @@ -1,32 +0,0 @@ - $this->type, - 'name' => $this->name, - 'damage' => $this->damage, - ]; - } -} diff --git a/app/Http/Resources/SC/Weapon/WeaponModeResource.php b/app/Http/Resources/SC/Weapon/WeaponModeResource.php deleted file mode 100644 index 5c28e5142..000000000 --- a/app/Http/Resources/SC/Weapon/WeaponModeResource.php +++ /dev/null @@ -1,38 +0,0 @@ - $this->mode, - 'type' => $this->type, - 'rpm' => $this->rounds_per_minute, - 'ammo_per_shot' => $this->ammo_per_shot, - 'pellets_per_shot' => $this->pellets_per_shot, - 'damage_per_second' => $this->damagePerSecond, - ]; - } -} diff --git a/app/Http/Resources/StarCitizen/Galactapedia/ArticleResource.php b/app/Http/Resources/StarCitizen/Galactapedia/ArticleResource.php index e6e419fd3..ef42fb852 100644 --- a/app/Http/Resources/StarCitizen/Galactapedia/ArticleResource.php +++ b/app/Http/Resources/StarCitizen/Galactapedia/ArticleResource.php @@ -5,12 +5,12 @@ namespace App\Http\Resources\StarCitizen\Galactapedia; use App\Http\Resources\AbstractBaseResource; -use App\Http\Resources\TranslationResourceFactory; +use App\Http\Resources\TranslationResolver; use Illuminate\Http\Request; use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'galactapedia_article_v2', + schema: 'galactapedia_article', title: 'Galactapedia Article', description: 'An article form the Galactapedia', properties: [ @@ -25,22 +25,22 @@ new OA\Property( property: 'categories', type: 'array', - items: new OA\Items(ref: '#/components/schemas/galactapedia_category_v2'), + items: new OA\Items(ref: '#/components/schemas/galactapedia_category'), ), new OA\Property( property: 'tags', type: 'array', - items: new OA\Items(ref: '#/components/schemas/galactapedia_tag_v2'), + items: new OA\Items(ref: '#/components/schemas/galactapedia_tag'), ), new OA\Property( property: 'properties', type: 'array', - items: new OA\Items(ref: '#/components/schemas/galactapedia_property_v2'), + items: new OA\Items(ref: '#/components/schemas/galactapedia_property'), ), new OA\Property( property: 'related_articles', type: 'array', - items: new OA\Items(ref: '#/components/schemas/galactpedia_related_article_v2'), + items: new OA\Items(ref: '#/components/schemas/galactpedia_related_article'), ), new OA\Property( property: 'translations', @@ -48,10 +48,11 @@ new OA\Schema(type: 'string'), new OA\Schema( type: 'array', - items: new OA\Items(ref: '#/components/schemas/translation_v2'), + items: new OA\Items(ref: '#/components/schemas/translation'), ), ], ), + new OA\Property(property: 'created_at_human', type: 'string', example: '1 hour ago'), ], type: 'object' )] @@ -64,34 +65,43 @@ public static function validIncludes(): array 'properties', 'tags', 'related', - 'translations', ]; } - /** - * Transform the resource into an array. - * - * @param Request $request - */ - public function toArray($request): array + public function toArray(Request $request): array { + $template = $this->templates->isEmpty() ? null : $this->templates[0]->template; + $categoryList = $this->categories->pluck('name')->filter()->implode(', '); + $tagList = $this->tags->pluck('name')->filter()->implode(', '); + return [ 'id' => $this->cig_id, 'title' => $this->title, 'slug' => $this->slug, 'thumbnail' => $this->thumbnail, - 'type' => $this->templates->isEmpty() ? null : $this->templates[0]->template, + 'type' => $template, + 'template' => $template, + 'category' => $categoryList !== '' ? $categoryList : null, + 'tag' => $tagList !== '' ? $tagList : null, 'rsi_url' => $this->url, - 'api_url' => $this->makeApiUrl( - self::GALACTAPEDIA_ARTICLE_SHOW, - $this->getRouteKey(), + 'api_url' => route( + 'galactapedia.show', + ['article' => $this->getRouteKey()], + ), + 'web_url' => route( + 'web.galactapedia.show', + ['article' => $this->getRouteKey()], ), 'categories' => CategoryResource::collection($this->whenLoaded('categories')), + 'categories_count' => $this->categories_count, 'tags' => TagResource::collection($this->whenLoaded('tags')), + 'tags_count' => $this->tags_count, 'properties' => PropertyResource::collection($this->whenLoaded('properties')), 'related_articles' => RelatedArticleResource::collection($this->whenLoaded('related')), - 'translations' => TranslationResourceFactory::getTranslationResource($request, $this->whenLoaded('translations')), - 'created_at' => $this->created_at, + 'related_articles_count' => $this->related_articles_count, + 'translations' => TranslationResolver::resolve($this, $request), + 'created_at' => $this->created_at->toIso8601String(), + 'created_at_human' => $this->created_at->diffForHumans(), ]; } } diff --git a/app/Http/Resources/StarCitizen/Galactapedia/CategoryResource.php b/app/Http/Resources/StarCitizen/Galactapedia/CategoryResource.php index 246102dda..8d1348635 100644 --- a/app/Http/Resources/StarCitizen/Galactapedia/CategoryResource.php +++ b/app/Http/Resources/StarCitizen/Galactapedia/CategoryResource.php @@ -9,7 +9,7 @@ use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'galactapedia_category_v2', + schema: 'galactapedia_category', title: 'Galctapedia article category', description: 'Category of an article', properties: [ @@ -20,12 +20,7 @@ )] class CategoryResource extends JsonResource { - /** - * Transform the resource into an array. - * - * @param Request $request - */ - public function toArray($request): array + public function toArray(Request $request): array { return [ 'id' => $this->cig_id, diff --git a/app/Http/Resources/StarCitizen/Galactapedia/PropertyResource.php b/app/Http/Resources/StarCitizen/Galactapedia/PropertyResource.php index 74c5a5171..0ca948b93 100644 --- a/app/Http/Resources/StarCitizen/Galactapedia/PropertyResource.php +++ b/app/Http/Resources/StarCitizen/Galactapedia/PropertyResource.php @@ -9,7 +9,7 @@ use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'galactapedia_property_v2', + schema: 'galactapedia_property', title: 'Galctapedia article property', description: 'Property of an article', properties: [ @@ -20,12 +20,7 @@ )] class PropertyResource extends JsonResource { - /** - * Transform the resource into an array. - * - * @param Request $request - */ - public function toArray($request): array + public function toArray(Request $request): array { return [ 'name' => $this->name, diff --git a/app/Http/Resources/StarCitizen/Galactapedia/RelatedArticleResource.php b/app/Http/Resources/StarCitizen/Galactapedia/RelatedArticleResource.php index aa9cad4ab..6556a8956 100644 --- a/app/Http/Resources/StarCitizen/Galactapedia/RelatedArticleResource.php +++ b/app/Http/Resources/StarCitizen/Galactapedia/RelatedArticleResource.php @@ -9,7 +9,7 @@ use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'galactpedia_related_article_v2', + schema: 'galactpedia_related_article', title: 'Galactapedia related article', description: 'Related article for this galactapedia article', properties: [ @@ -22,20 +22,15 @@ )] class RelatedArticleResource extends AbstractBaseResource { - /** - * Transform the resource into an array. - * - * @param Request $request - */ - public function toArray($request): array + public function toArray(Request $request): array { return [ 'id' => $this->cig_id, 'title' => $this->title, 'url' => $this->url, - 'api_url' => $this->makeApiUrl( - self::GALACTAPEDIA_ARTICLE_SHOW, - $this->getRouteKey(), + 'api_url' => route( + 'galactapedia.show', + ['article' => $this->getRouteKey()], ), ]; } diff --git a/app/Http/Resources/StarCitizen/Galactapedia/TagResource.php b/app/Http/Resources/StarCitizen/Galactapedia/TagResource.php index 4d6e6490a..e1ed08790 100644 --- a/app/Http/Resources/StarCitizen/Galactapedia/TagResource.php +++ b/app/Http/Resources/StarCitizen/Galactapedia/TagResource.php @@ -9,7 +9,7 @@ use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'galactapedia_tag_v2', + schema: 'galactapedia_tag', title: 'Galctapedia article tag', description: 'Tag of an article', properties: [ @@ -20,12 +20,7 @@ )] class TagResource extends JsonResource { - /** - * Transform the resource into an array. - * - * @param Request $request - */ - public function toArray($request): array + public function toArray(Request $request): array { return [ 'id' => $this->cig_id, diff --git a/app/Http/Resources/StarCitizen/Manufacturer/ManufacturerLinkResource.php b/app/Http/Resources/StarCitizen/Manufacturer/ManufacturerLinkResource.php new file mode 100644 index 000000000..cea39fc0c --- /dev/null +++ b/app/Http/Resources/StarCitizen/Manufacturer/ManufacturerLinkResource.php @@ -0,0 +1,31 @@ + $this->cig_id, + 'name' => $this->name, + 'code' => $this->name_short, + ]; + } +} diff --git a/app/Http/Resources/StarCitizen/Starmap/AffiliationResource.php b/app/Http/Resources/StarCitizen/Starmap/AffiliationResource.php index 6facdccaf..11d61cdca 100644 --- a/app/Http/Resources/StarCitizen/Starmap/AffiliationResource.php +++ b/app/Http/Resources/StarCitizen/Starmap/AffiliationResource.php @@ -4,11 +4,12 @@ namespace App\Http\Resources\StarCitizen\Starmap; +use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'affiliation_v2', + schema: 'affiliation', title: 'Affiliation', properties: [ new OA\Property(property: 'id', type: 'string'), @@ -20,7 +21,7 @@ )] class AffiliationResource extends JsonResource { - public function toArray($request): array + public function toArray(Request $request): array { return [ 'id' => $this->cig_id, diff --git a/app/Http/Resources/StarCitizen/Starmap/CelestialObjectResource.php b/app/Http/Resources/StarCitizen/Starmap/CelestialObjectResource.php index 5a17f6b6b..8ab1666f5 100644 --- a/app/Http/Resources/StarCitizen/Starmap/CelestialObjectResource.php +++ b/app/Http/Resources/StarCitizen/Starmap/CelestialObjectResource.php @@ -4,17 +4,19 @@ namespace App\Http\Resources\StarCitizen\Starmap; -use App\Http\Resources\AbstractTranslationResource; +use App\Http\Resources\AbstractBaseResource; +use App\Http\Resources\TranslationResolver; use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'celestial_object_v2', + schema: 'celestial_object', title: 'Celestial Object', properties: [ new OA\Property(property: 'id', type: 'integer'), new OA\Property(property: 'code', type: 'string'), new OA\Property(property: 'system_id', type: 'integer'), new OA\Property(property: 'celestial_object_api_url', type: 'string'), + new OA\Property(property: 'web_url', type: 'string'), new OA\Property(property: 'name', type: 'string'), new OA\Property(property: 'type', type: 'string'), new OA\Property(property: 'age', type: 'integer'), @@ -40,11 +42,21 @@ ), new OA\Property(property: 'size', type: 'float'), new OA\Property(property: 'parent_id', type: 'integer'), + new OA\Property( + property: 'starsystem', + properties: [ + new OA\Property(property: 'id', type: 'integer', nullable: true), + new OA\Property(property: 'code', type: 'string', nullable: true), + new OA\Property(property: 'name', type: 'string', nullable: true), + ], + type: 'object', + nullable: true + ), new OA\Property(property: 'time_modified', type: 'string'), ], type: 'object' )] -class CelestialObjectResource extends AbstractTranslationResource +class CelestialObjectResource extends AbstractBaseResource { public static function validIncludes(): array { @@ -60,10 +72,11 @@ public function toArray($request): array 'id' => $this->cig_id, 'code' => $this->code, 'system_id' => $this->starsystem_id, - 'link' => $this->makeApiUrl( - self::STARMAP_CELESTIAL_OBJECTS_SHOW, - $this->code + 'link' => route( + 'celestial-objects.show', + ['code' => $this->code] ), + 'web_url' => route('web.starmap.celestial-objects.show', ['id' => $this->cig_id]), 'name' => $this->name, 'type' => $this->type, @@ -81,7 +94,7 @@ public function toArray($request): array 'info_url' => $this->info_url, - 'description' => $this->getTranslation($this, $request), + 'description' => TranslationResolver::resolve($this, $request), 'sensor' => [ 'population' => $this->sensor_population, @@ -94,7 +107,13 @@ public function toArray($request): array 'parent_id' => $this->parent_id, 'affiliation' => AffiliationResource::collection($this->whenLoaded('affiliation')), - 'starsystem' => new StarsystemResource($this->whenLoaded('starsystem')), + 'starsystem' => $this->whenLoaded('starsystem', function (): array { + return [ + 'id' => $this->starsystem?->cig_id, + 'code' => $this->starsystem?->code, + 'name' => $this->starsystem?->name, + ]; + }), $this->mergeWhen($this->whenLoaded('subtype'), [ 'sub_type' => [ 'id' => $this->subtype->id, diff --git a/app/Http/Resources/StarCitizen/Starmap/JumppointResource.php b/app/Http/Resources/StarCitizen/Starmap/JumppointResource.php index 6243d9957..67734ecc3 100644 --- a/app/Http/Resources/StarCitizen/Starmap/JumppointResource.php +++ b/app/Http/Resources/StarCitizen/Starmap/JumppointResource.php @@ -4,11 +4,11 @@ namespace App\Http\Resources\StarCitizen\Starmap; -use App\Http\Resources\AbstractTranslationResource; +use App\Http\Resources\AbstractBaseResource; use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'jumppoint_v2', + schema: 'jumppoint', title: 'Jumppoint', description: 'A jumppoint from the starmap', properties: [ @@ -42,7 +42,7 @@ ], type: 'object' )] -class JumppointResource extends AbstractTranslationResource +class JumppointResource extends AbstractBaseResource { private bool $hideCO; @@ -61,13 +61,13 @@ public function toArray($request): array 'entry' => [ 'id' => $this->entry->cig_id, 'system_id' => $this->entry->starsystem_id, - 'system_api_url' => $this->makeApiUrl( - self::STARMAP_STARSYSTEM_SHOW, - $this->entry->starsystem_id + 'system_api_url' => route( + 'starsystems.show', + ['code' => $this->entry->starsystem_id] ), - 'celestial_object_api_url' => $this->makeApiUrl( - self::STARMAP_CELESTIAL_OBJECTS_SHOW, - $this->entry->code + 'celestial_object_api_url' => route( + 'celestial-objects.show', + ['code' => $this->entry->code] ), 'status' => $this->entry_status, 'code' => $this->entry->code, @@ -76,13 +76,13 @@ public function toArray($request): array 'exit' => [ 'id' => $this->exit->cig_id, 'system_id' => $this->exit->starsystem_id, - 'system_api_url' => $this->makeApiUrl( - self::STARMAP_STARSYSTEM_SHOW, - $this->exit->starsystem_id + 'system_api_url' => route( + 'starsystems.show', + ['code' => $this->exit->starsystem_id] ), - 'celestial_object_api_url' => $this->makeApiUrl( - self::STARMAP_CELESTIAL_OBJECTS_SHOW, - $this->exit->code + 'celestial_object_api_url' => route( + 'celestial-objects.show', + ['code' => $this->exit->code] ), 'status' => $this->exit_status, 'code' => $this->exit->code, diff --git a/app/Http/Resources/StarCitizen/Starmap/StarsystemResource.php b/app/Http/Resources/StarCitizen/Starmap/StarsystemResource.php index e8952b8f3..aadfc948e 100644 --- a/app/Http/Resources/StarCitizen/Starmap/StarsystemResource.php +++ b/app/Http/Resources/StarCitizen/Starmap/StarsystemResource.php @@ -4,12 +4,13 @@ namespace App\Http\Resources\StarCitizen\Starmap; -use App\Http\Resources\AbstractTranslationResource; +use App\Http\Resources\AbstractBaseResource; +use App\Http\Resources\TranslationResolver; use Illuminate\Http\Request; use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'starsystem_v2', + schema: 'starsystem', title: 'Starsystem', properties: [ new OA\Property(property: 'id', type: 'integer'), @@ -18,6 +19,7 @@ new OA\Property(property: 'name', type: 'string'), new OA\Property(property: 'status', type: 'string'), new OA\Property(property: 'type', type: 'string'), + new OA\Property(property: 'web_url', type: 'string'), new OA\Property( property: 'position', properties: [ @@ -57,7 +59,7 @@ properties: [ new OA\Property( property: 'data', - ref: '#/components/schemas/celestial_object_v2', + ref: '#/components/schemas/celestial_object', type: 'array', items: new OA\Items, ), @@ -76,7 +78,7 @@ properties: [ new OA\Property( property: 'data', - ref: '#/components/schemas/jumppoint_v2', + ref: '#/components/schemas/jumppoint', type: 'array', items: new OA\Items, ), @@ -90,7 +92,7 @@ ], type: 'object' )] -class StarsystemResource extends AbstractTranslationResource +class StarsystemResource extends AbstractBaseResource { public static function validIncludes(): array { @@ -105,10 +107,11 @@ public function toArray(Request $request): array return [ 'id' => $this->cig_id, 'code' => $this->code, - 'system_api_url' => $this->makeApiUrl(self::STARMAP_STARSYSTEM_SHOW, $this->code), + 'system_api_url' => route('starsystems.show', ['code' => $this->code]), 'name' => $this->name, 'status' => $this->status, 'type' => $this->type, + 'web_url' => route('web.starmap.systems.show', ['id' => $this->cig_id]), 'position' => [ 'x' => $this->position_x, @@ -122,7 +125,7 @@ public function toArray(Request $request): array 'info_url' => $this->info_url, - 'description' => $this->getTranslation($this, $request), + 'description' => TranslationResolver::resolve($this, $request), 'aggregated' => [ 'size' => $this->aggregated_size, diff --git a/app/Http/Resources/StarCitizen/Stat/StatResource.php b/app/Http/Resources/StarCitizen/StatResource.php similarity index 77% rename from app/Http/Resources/StarCitizen/Stat/StatResource.php rename to app/Http/Resources/StarCitizen/StatResource.php index a57629519..66af65f26 100644 --- a/app/Http/Resources/StarCitizen/Stat/StatResource.php +++ b/app/Http/Resources/StarCitizen/StatResource.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace App\Http\Resources\StarCitizen\Stat; +namespace App\Http\Resources\StarCitizen; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'stat_v2', + schema: 'stat', title: 'RSI Stats', description: 'Stats about fans and funds', properties: [ @@ -22,12 +22,7 @@ )] class StatResource extends JsonResource { - /** - * Transform the resource into an array. - * - * @param Request $request - */ - public function toArray($request): array + public function toArray(Request $request): array { return [ 'funds' => $this->funds, diff --git a/app/Http/Resources/StarCitizen/Vehicle/ComponentResource.php b/app/Http/Resources/StarCitizen/Vehicle/ComponentResource.php index 18bd4d36b..aba6c7553 100644 --- a/app/Http/Resources/StarCitizen/Vehicle/ComponentResource.php +++ b/app/Http/Resources/StarCitizen/Vehicle/ComponentResource.php @@ -9,7 +9,7 @@ use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'vehicle_component_v2', + schema: 'vehicle_component', title: 'Vehicle Component', description: 'Components from in-game files', properties: [ @@ -28,12 +28,7 @@ )] class ComponentResource extends JsonResource { - /** - * Transform the resource into an array. - * - * @param Request $request - */ - public function toArray($request): array + public function toArray(Request $request): array { return [ 'type' => $this->type, diff --git a/app/Http/Resources/StarCitizen/Vehicle/VehicleLinkResource.php b/app/Http/Resources/StarCitizen/Vehicle/VehicleLinkResource.php new file mode 100644 index 000000000..05c98cc90 --- /dev/null +++ b/app/Http/Resources/StarCitizen/Vehicle/VehicleLinkResource.php @@ -0,0 +1,51 @@ + $this->cig_id, + 'name' => $this->name, + 'slug' => $this->slug, + 'size' => TranslationResolver::resolve($this->size, $request), + 'type' => TranslationResolver::resolve($this->type, $request), + 'manufacturer' => new ManufacturerLinkResource($this->manufacturer), + 'production_status' => TranslationResolver::resolve($this->productionStatus, $request), + 'link' => route('shipmatrix.vehicles.show', ['vehicle' => $this->slug]), + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Http/Resources/StarCitizen/Vehicle/VehicleLoanerResource.php b/app/Http/Resources/StarCitizen/Vehicle/VehicleLoanerResource.php index 089c4e851..dab4856d8 100644 --- a/app/Http/Resources/StarCitizen/Vehicle/VehicleLoanerResource.php +++ b/app/Http/Resources/StarCitizen/Vehicle/VehicleLoanerResource.php @@ -9,7 +9,7 @@ use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'vehicle_loaner_v2', + schema: 'vehicle_loaner', title: 'Vehicle Loaner', properties: [ new OA\Property(property: 'name', type: 'string'), @@ -20,20 +20,14 @@ )] class VehicleLoanerResource extends AbstractBaseResource { - /** - * Transform the resource into an array. - * - * @param Request $request - */ - public function toArray($request): array + public function toArray(Request $request): array { return [ 'name' => $this->name, - 'link' => $this->makeApiUrl( - self::VEHICLES_SHOW, - $this->sc?->exists ? $this->sc->item_uuid : urlencode($this->name) + 'link' => route( + 'vehicles.show', + ['vehicle' => $this->sc?->exists ? $this->sc->vehicle->uuid : ($this->name ?? '')] ), - 'version' => $this->pivot->version, ]; } diff --git a/app/Http/Resources/StarCitizen/Vehicle/VehicleResource.php b/app/Http/Resources/StarCitizen/Vehicle/VehicleResource.php index 174580f1d..94572ed0d 100644 --- a/app/Http/Resources/StarCitizen/Vehicle/VehicleResource.php +++ b/app/Http/Resources/StarCitizen/Vehicle/VehicleResource.php @@ -5,15 +5,15 @@ namespace App\Http\Resources\StarCitizen\Vehicle; use App\Http\Resources\AbstractBaseResource; -use App\Http\Resources\TranslationResourceFactory; +use App\Http\Resources\TranslationResolver; use Illuminate\Http\Request; use Illuminate\Support\Collection; use OpenApi\Attributes as OA; #[OA\Schema( - schema: 'vehicle_v2', - title: 'Vehicle', - description: 'Ship-Matrix vehicle', + schema: 'ship_matrix_vehicle', + title: 'Ship Matrix Vehicle', + description: 'Ship Matrix vehicle', properties: [ new OA\Property(property: 'id', type: 'integer'), new OA\Property(property: 'chassis_id', type: 'integer'), @@ -26,6 +26,16 @@ new OA\Property(property: 'beam', type: 'float'), new OA\Property(property: 'height', type: 'float'), ], + type: 'object', + deprecated: true + ), + new OA\Property( + property: 'dimension', + properties: [ + new OA\Property(property: 'length', type: 'float'), + new OA\Property(property: 'width', type: 'float'), + new OA\Property(property: 'height', type: 'float'), + ], type: 'object' ), @@ -75,13 +85,13 @@ new OA\Property( property: 'foci', type: 'array', - items: new OA\Items(ref: '#/components/schemas/translation_v2') + items: new OA\Items(ref: '#/components/schemas/translation') ), - new OA\Property(property: 'production_status', ref: '#/components/schemas/translation_v2'), - new OA\Property(property: 'production_note', ref: '#/components/schemas/translation_v2'), - new OA\Property(property: 'type', ref: '#/components/schemas/translation_v2'), - new OA\Property(property: 'description', ref: '#/components/schemas/translation_v2'), - new OA\Property(property: 'size', ref: '#/components/schemas/translation_v2'), + new OA\Property(property: 'production_status', ref: '#/components/schemas/translation'), + new OA\Property(property: 'production_note', ref: '#/components/schemas/translation'), + new OA\Property(property: 'type', ref: '#/components/schemas/translation'), + new OA\Property(property: 'description', ref: '#/components/schemas/translation'), + new OA\Property(property: 'size', ref: '#/components/schemas/translation'), new OA\Property( property: 'msrp', description: 'MSRP imported from the Ship Upgrade tool.', @@ -113,13 +123,15 @@ property: 'components', description: 'Components imported from the Ship-Matrix', type: 'array', - items: new OA\Items(ref: '#/components/schemas/vehicle_component_v2'), + items: new OA\Items(ref: '#/components/schemas/vehicle_component'), ), new OA\Property( property: 'loaner', type: 'array', - items: new OA\Items(ref: '#/components/schemas/vehicle_loaner_v2'), + items: new OA\Items(ref: '#/components/schemas/vehicle_loaner'), ), + new OA\Property(property: 'link', description: 'Link to detail endpoint', type: 'string'), + new OA\Property(property: 'updated_at_human', type: 'string', example: '1 hour ago'), ], type: 'object' )] @@ -132,9 +144,6 @@ public static function validIncludes(): array ]; } - /** - * Transform the resource into an array. - */ public function toArray(Request $request): array { $includes = collect(explode(',', $request->get('include', ''))) @@ -142,15 +151,24 @@ public function toArray(Request $request): array ->map('strtolower') ->toArray(); + $this->addMetadata('deprecated_fields', [ + 'sizes' => 'Use length, width, and height properties from dimension instead', + ]); + return [ 'id' => $this->cig_id, 'chassis_id' => $this->chassis_id, 'name' => $this->name, 'slug' => $this->slug, 'sizes' => [ - 'length' => (float) $this->length, - 'beam' => (float) $this->width, - 'height' => (float) $this->height, + 'length' => (float) ($this->length ?? 0), + 'beam' => (float) ($this->beam ?? 0), + 'height' => (float) ($this->height ?? 0), + ], + 'dimension' => [ + 'length' => (float) ($this->length ?? 0), + 'width' => (float) ($this->beam ?? 0), + 'height' => (float) ($this->height ?? 0), ], 'mass' => $this->mass, 'cargo_capacity' => $this->cargo_capacity, @@ -175,11 +193,11 @@ public function toArray(Request $request): array ], ], 'foci' => $this->getFociTranslations($request), - 'production_status' => TranslationResourceFactory::getTranslationResource($request, $this->productionStatus), - 'production_note' => TranslationResourceFactory::getTranslationResource($request, $this->productionNote), - 'type' => TranslationResourceFactory::getTranslationResource($request, $this->type), - 'description' => TranslationResourceFactory::getTranslationResource($request, $this), - 'size' => TranslationResourceFactory::getTranslationResource($request, $this->size), + 'production_status' => TranslationResolver::resolve($this->productionStatus, $request), + 'production_note' => TranslationResolver::resolve($this->productionNote, $request), + 'type' => TranslationResolver::resolve($this->type, $request), + 'description' => TranslationResolver::resolve($this, $request), + 'size' => TranslationResolver::resolve($this->size, $request), 'msrp' => $this->msrp, $this->mergeWhen($this->pledge_url !== null, [ 'pledge_url' => sprintf('https://robertsspaceindustries.com%s', $this->pledge_url), @@ -190,6 +208,7 @@ public function toArray(Request $request): array 'code' => $this->manufacturer->name_short, 'name' => $this->manufacturer->name, ], + // 'web_url' => route('web.ship-matrix.vehicles.show', ['vehicle' => $this->cig_id]), $this->mergeWhen(in_array('components', $includes, true), [ 'components' => ComponentResource::collection($this->components), @@ -197,6 +216,8 @@ public function toArray(Request $request): array 'loaner' => VehicleLoanerResource::collection($this->loaner), + 'link' => route('shipmatrix.vehicles.show', $this->slug), + 'updated_at' => $this->updated_at, ]; } @@ -209,7 +230,7 @@ private function getFociTranslations(Request $request): array $foci->each( function ($vehicleFocus) use (&$fociTranslations, $request) { - $fociTranslations[] = TranslationResourceFactory::getTranslationResource($request, $vehicleFocus); + $fociTranslations[] = TranslationResolver::resolve($vehicleFocus, $request); } ); diff --git a/app/Http/Resources/StarCitizen/Vehicle/VehicleSkuResource.php b/app/Http/Resources/StarCitizen/Vehicle/VehicleSkuResource.php index bd7622146..cc83bfff9 100644 --- a/app/Http/Resources/StarCitizen/Vehicle/VehicleSkuResource.php +++ b/app/Http/Resources/StarCitizen/Vehicle/VehicleSkuResource.php @@ -19,11 +19,6 @@ )] class VehicleSkuResource extends JsonResource { - /** - * Transform the resource into an array. - * - * @return array - */ public function toArray(Request $request): array { return [ diff --git a/app/Http/Resources/TranslationCollection.php b/app/Http/Resources/TranslationCollection.php deleted file mode 100644 index ee00abcbf..000000000 --- a/app/Http/Resources/TranslationCollection.php +++ /dev/null @@ -1,24 +0,0 @@ -collection as $translation) { - $out[$translation->locale_code] = $translation->translation; - } - - return $out; - } -} diff --git a/app/Http/Resources/TranslationResolver.php b/app/Http/Resources/TranslationResolver.php new file mode 100644 index 000000000..08595d3e7 --- /dev/null +++ b/app/Http/Resources/TranslationResolver.php @@ -0,0 +1,78 @@ +get('locale'); + + if (is_string($locale) && $locale !== '') { + return self::getSingleLocaleTranslation($source, $translationKey, substr($locale, 0, 2)); + } + + return self::getAllLocaleTranslations($source, $translationKey); + } + + private static function getSingleLocaleTranslation( + mixed $source, + string $field, + string $locale + ): ?string { + $value = $source->getTranslation($field, $locale, false); + + if (empty($value) && $locale !== Language::ENGLISH) { + $value = $source->getTranslation($field, Language::ENGLISH, false); + } + + return ! empty($value) ? $value : null; + } + + /** + * @param HasTranslations $source + */ + private static function getAllLocaleTranslations(mixed $source, string $field): ?array + { + $translations = $source->getTranslations($field); + + if (empty($translations)) { + return null; + } + + $english = $translations[Language::ENGLISH] ?? null; + $locales = Language::query()->pluck('code'); + + if ($locales->isEmpty()) { + return array_filter($translations, static fn ($v) => ! empty($v)) ?: null; + } + + $result = $locales->mapWithKeys(function (string $locale) use ($translations, $english): array { + $value = $translations[$locale] ?? $english; + + return [Language::OLD_LANG_MAP[$locale] ?? $locale => $value]; + }) + ->filter(fn ($value) => ! empty($value)) + ->toArray(); + + return ! empty($result) ? $result : null; + } +} diff --git a/app/Http/Resources/TranslationResourceFactory.php b/app/Http/Resources/TranslationResourceFactory.php deleted file mode 100644 index 96950e854..000000000 --- a/app/Http/Resources/TranslationResourceFactory.php +++ /dev/null @@ -1,41 +0,0 @@ -translations); - } else { - return new MissingValue; - } - - $transformed = $collection->toArray($request); - - if ($request->has('locale')) { - return $transformed[$request->get('locale')] ?? $transformed[Language::ENGLISH] ?? new MissingValue; - } - - return $collection; - } -} diff --git a/app/Http/Resources/TranslationSchemas.php b/app/Http/Resources/TranslationSchemas.php new file mode 100644 index 000000000..e3a2e3c34 --- /dev/null +++ b/app/Http/Resources/TranslationSchemas.php @@ -0,0 +1,27 @@ + config('api.rsi_url'), - 'cookies' => self::$cookieJar, - ] - )->timeout(60); - - if ($withTokenHeader === true) { - $client = $client->withHeaders( - [ - 'X-RSI-Token' => self::RSI_TOKEN, - ] - ); - } - - self::$client = $client; - } - - return self::$client; - } - - /** - * Logs a User into the RSI Website. - * - * @return stdClass Response JSON - * - * @throws RuntimeException - */ - protected function getRsiAuthCookie(): stdClass - { - $res = self::$client->post( - 'api/account/signin', - [ - 'form_params' => [ - 'username' => config('services.rsi_account.username'), - 'password' => config('services.rsi_account.password'), - ], - 'cookies' => self::$cookieJar, - ] - ); - - $response = $res->json(); - - if ($response['success'] !== 1 || ! $res->successful()) { - throw new RuntimeException('Login was not successful'); - } - - return $response; - } - - /** - * Add Guzzle Cookies to Goutte. - */ - protected function addGuzzleCookiesToScraper(HttpBrowser $client): HttpBrowser - { - foreach (self::$cookieJar->toArray() as $cookie) { - $client->getCookieJar()->set( - new Cookie($cookie['Name'], $cookie['Value'], null, $cookie['Path'], $cookie['Domain']) - ); - } - - return $client; - } -} diff --git a/app/Jobs/Game/ComputeItemBaseIds.php b/app/Jobs/Game/ComputeItemBaseIds.php new file mode 100644 index 000000000..209de311a --- /dev/null +++ b/app/Jobs/Game/ComputeItemBaseIds.php @@ -0,0 +1,390 @@ +where('game_version_id', $this->gameVersionId) + ->chunkById(250, function (Collection $items) use (&$classBaseCache, &$tagBaseCache): void { + foreach ($items as $itemData) { + $baseId = $this->resolveBaseId($itemData, $classBaseCache, $tagBaseCache); + + if ($baseId !== $itemData->base_id) { + if (! $this->dryRun) { + ItemData::query() + ->whereKey($itemData->id) + ->update(['base_id' => $baseId]); + } + } + } + }); + } + + /** + * @param array $classBaseCache + * @param array $tagBaseCache + */ + private function resolveBaseId(ItemData $itemData, array &$classBaseCache, array &$tagBaseCache): ?int + { + $classBaseId = $this->resolveBaseIdFromClassName($itemData, $classBaseCache); + + if ($classBaseId !== null) { + return $classBaseId === $itemData->id ? null : $classBaseId; + } + + $tagBaseId = $this->resolveBaseIdFromTags($itemData, $tagBaseCache); + + return $tagBaseId === $itemData->id ? null : $tagBaseId; + } + + /** + * @param array $classBaseCache + */ + private function resolveBaseIdFromClassName(ItemData $itemData, array &$classBaseCache): ?int + { + $className = $itemData->class_name ?? ''; + + if ($className === '') { + return null; + } + + [$cacheKey, $baseId] = $this->lookupClassBaseId($itemData, $className, $classBaseCache); + + if ($cacheKey === null) { + return null; + } + + return $baseId; + } + + /** + * @param array $classBaseCache + * @return array{0:?string,1:?int} + */ + private function lookupClassBaseId(ItemData $itemData, string $className, array &$classBaseCache): array + { + $match = []; + if (preg_match('/_0\d/', $className, $match, PREG_OFFSET_CAPTURE) !== 1) { + return [null, null]; + } + + $offset = $match[0][1]; + $prefix = substr($className, 0, $offset + 3); + if ($prefix === '') { + return [null, null]; + } + + $typeKey = $itemData->type ?? ''; + $cacheKey = sprintf('%d|%s|%s', $itemData->game_version_id, $typeKey, $prefix); + + if (array_key_exists($cacheKey, $classBaseCache)) { + return [$cacheKey, $classBaseCache[$cacheKey]]; + } + + $baseQuery = ItemData::query() + ->where('game_version_id', $itemData->game_version_id); + + if ($itemData->type !== null) { + $baseQuery->where('type', $itemData->type); + } + + if ($itemData->type === 'Char_Armor_Backpack') { + $baseClass = substr($className, 0, $offset); + $name = (string) $itemData->name; + + if (str_ends_with($name, 'Backpack') && ! str_contains($name, '"Expo"')) { + $baseClass = '<>'; + } + + if ($baseClass === '<>') { + $classBaseCache[$cacheKey] = null; + + return [$cacheKey, null]; + } + + $baseId = $baseQuery + ->where('class_name', 'LIKE', $baseClass.'%') + ->orderBy('name') + ->value('id'); + + $classBaseCache[$cacheKey] = $baseId; + + return [$cacheKey, $baseId]; + } + + $baseClassChecks = [ + $prefix, + $prefix.'_01', + $prefix.'_01_01', + ]; + + $baseId = $baseQuery + ->whereIn('class_name', $baseClassChecks) + ->orderByRaw( + 'case class_name when ? then 0 when ? then 1 when ? then 2 else 3 end', + $baseClassChecks + ) + ->value('id'); + + $classBaseCache[$cacheKey] = $baseId; + + return [$cacheKey, $baseId]; + } + + /** + * @param array $tagBaseCache + */ + private function resolveBaseIdFromTags(ItemData $itemData, array &$tagBaseCache): ?int + { + $tags = $this->extractStdItemTags($itemData); + $groupTags = $this->resolveVariantGroupTags($tags); + + if ($groupTags === null) { + return null; + } + + $cacheKey = $this->buildTagGroupCacheKey($itemData, $groupTags); + + if (array_key_exists($cacheKey, $tagBaseCache)) { + return $tagBaseCache[$cacheKey]; + } + + $query = ItemData::query() + ->where('game_version_id', $itemData->game_version_id) + ->whereJsonContains('data->stdItem->Tags', $groupTags['series']) + ->whereJsonContains('data->stdItem->Tags', $groupTags['set']); + + $this->applyVariantTypeFilter($query, $itemData); + + $groupItems = $query->get(); + + if ($groupItems->count() <= 1) { + $tagBaseCache[$cacheKey] = null; + + return null; + } + + $baseId = $this->pickBaseIdFromTagGroup($groupItems); + $tagBaseCache[$cacheKey] = $baseId; + + return $baseId; + } + + /** + * @return array + */ + private function extractStdItemTags(ItemData $itemData): array + { + $tags = Arr::get($itemData->data, 'stdItem.Tags', []); + + if (! is_array($tags)) { + $tags = []; + } + + $rawTags = Arr::get($itemData->data, 'tags'); + if (is_string($rawTags)) { + $tags = array_merge($tags, preg_split('/\s+/', trim($rawTags)) ?: []); + } + + $tags = array_filter($tags, fn ($tag) => is_string($tag) && trim($tag) !== ''); + + return array_values(array_unique($tags)); + } + + /** + * @param array $tags + * @return array{series:string,set:string}|null + */ + private function resolveVariantGroupTags(array $tags): ?array + { + $setTag = null; + + foreach ($tags as $tag) { + if (preg_match('/^set_/i', $tag) === 1) { + $setTag = $tag; + break; + } + } + + if ($setTag === null) { + return null; + } + + $seriesTag = null; + foreach ($tags as $tag) { + if (preg_match('/^(set|color)_/i', $tag) === 1) { + continue; + } + + if ($this->isIgnoredVariantTag($tag)) { + continue; + } + + $seriesTag = $tag; + break; + } + + if ($seriesTag === null) { + return null; + } + + return [ + 'series' => $seriesTag, + 'set' => $setTag, + ]; + } + + private function isIgnoredVariantTag(string $tag): bool + { + $lower = strtolower($tag); + + if (str_starts_with($lower, 'sm_')) { + return true; + } + + if (str_starts_with($lower, 'texture_')) { + return true; + } + + if (str_contains($lower, 'armor_mobi')) { + return true; + } + + return in_array($lower, [ + 'helmet', + 'helmetcarryable', + 'backpack', + 'flightready', + 'uneditable', + 'stocked', + 'weaponmountusable', + 'missionquestitem', + 'unifiedhead', + ], true); + } + + /** + * @param array{series:string,set:string} $groupTags + */ + private function buildTagGroupCacheKey(ItemData $itemData, array $groupTags): string + { + if ($itemData->classification !== null) { + return sprintf( + '%d|%s|%s|%s', + $itemData->game_version_id, + strtolower($groupTags['series']), + strtolower($groupTags['set']), + strtolower($itemData->classification) + ); + } + + return sprintf( + '%d|%s|%s|%s|%s', + $itemData->game_version_id, + strtolower($groupTags['series']), + strtolower($groupTags['set']), + strtolower((string) $itemData->type), + strtolower((string) $itemData->sub_type) + ); + } + + /** + * @param Collection $groupItems + */ + private function pickBaseIdFromTagGroup(Collection $groupItems): ?int + { + $sorted = $groupItems->sort(function (ItemData $left, ItemData $right): int { + $leftColor = $this->extractColorIndex($left); + $rightColor = $this->extractColorIndex($right); + + if ($leftColor !== null && $rightColor !== null && $leftColor !== $rightColor) { + return $leftColor <=> $rightColor; + } + + if ($leftColor !== null && $rightColor === null) { + return -1; + } + + if ($leftColor === null && $rightColor !== null) { + return 1; + } + + $leftClass = $left->class_name ?? ''; + $rightClass = $right->class_name ?? ''; + $classCompare = strcmp($leftClass, $rightClass); + if ($classCompare !== 0) { + return $classCompare; + } + + $nameCompare = strcmp($left->name ?? '', $right->name ?? ''); + if ($nameCompare !== 0) { + return $nameCompare; + } + + return $left->id <=> $right->id; + }); + + return $sorted->first()?->id; + } + + private function extractColorIndex(ItemData $itemData): ?int + { + $tags = $this->extractStdItemTags($itemData); + + foreach ($tags as $tag) { + if (preg_match('/^color_(\d+)$/i', $tag, $matches) === 1) { + return (int) $matches[1]; + } + } + + return null; + } + + private function applyVariantTypeFilter(Builder $query, ItemData $itemData): void + { + if ($itemData->classification !== null) { + $query->where('classification', $itemData->classification); + + return; + } + + if ($itemData->type !== null) { + $query->where('type', $itemData->type); + } + + if ($itemData->sub_type !== null) { + $query->where('sub_type', $itemData->sub_type); + } + } +} diff --git a/app/Jobs/Game/ImportItemData.php b/app/Jobs/Game/ImportItemData.php new file mode 100644 index 000000000..4a997690e --- /dev/null +++ b/app/Jobs/Game/ImportItemData.php @@ -0,0 +1,397 @@ +labels = $labels; + } + + /** + * Execute the job. + * + * @throws JsonException + */ + public function handle(): void + { + $payload = $this->readPayload(); + + $itemPayload = $payload['Item'] ?? null; + + if (! is_array($itemPayload)) { + return; + } + + $uuid = $this->extractUuid($itemPayload); + + if ($uuid === null) { + return; + } + + $item = Item::query()->firstOrCreate( + ['uuid' => $uuid], + ['uuid' => $uuid] + ); + + $manufacturerId = $this->resolveManufacturerId($itemPayload, $uuid); + + $itemData = ItemData::query()->updateOrCreate( + [ + 'item_id' => $item->id, + 'game_version_id' => $this->gameVersionId, + ], + $this->mapItemData($itemPayload, $manufacturerId) + ); + + $raw = $payload['Raw'] ?? []; + + $this->syncDescriptionData($item, $itemPayload, $raw); + $this->syncTranslations($item, $itemPayload, $raw); + $this->syncEntityTags($itemData, $itemPayload); + } + + /** + * @throws JsonException + */ + private function readPayload(): array + { + $contents = Storage::disk('scunpacked')->get($this->path); + + return json_decode($contents, true, 512, JSON_THROW_ON_ERROR); + } + + private function extractUuid(array $itemPayload): ?string + { + $uuid = $itemPayload['reference'] ?? $itemPayload['uuid'] ?? null; + + if (! is_string($uuid)) { + return null; + } + + $uuid = trim($uuid); + + return $uuid === '' ? null : $uuid; + } + + private function resolveManufacturerId(array $itemPayload, string $uuid): int + { + $manufacturerUuid = Arr::get($itemPayload, 'stdItem.Manufacturer.UUID'); + + $manufacturer = Manufacturer::query() + ->where('uuid', $manufacturerUuid) + ->first(); + + if ($manufacturer === null) { + throw new RuntimeException(sprintf('Manufacturer with uuid %s does not exist for item %s.', $manufacturerUuid, $uuid)); + } + + return $manufacturer->id; + } + + private function mapItemData(array $itemPayload, int $manufacturerId): array + { + $name = $this->extractName($itemPayload); + + unset($itemPayload['name'], $itemPayload['itemName']); + + $itemClass = Arr::get($itemPayload, 'stdItem.DescriptionData.Class'); + + if (! in_array($itemClass, ['Industrial', 'Civilian', 'Military', 'Stealth', 'Competition'], true)) { + $itemClass = null; + } + + return [ + 'manufacturer_id' => $manufacturerId, + 'name' => $name, + 'class_name' => $itemPayload['className'] ?? null, + 'type' => $itemPayload['type'] ?? null, + 'sub_type' => $itemPayload['subType'] ?? null, + 'classification' => $itemPayload['classification'] ?? null, + 'size' => $this->nullableInt($itemPayload['size'] ?? null), + 'grade' => $this->nullableInt($itemPayload['grade'] ?? null), + 'class' => $itemClass, + 'base_id' => null, + + 'data' => $itemPayload, + ]; + } + + private function extractName(array $itemPayload): string + { + $candidates = [ + $itemPayload['name'] ?? null, + $itemPayload['itemName'] ?? null, + $itemPayload['className'] ?? null, + ]; + + foreach ($candidates as $candidate) { + if (is_string($candidate) && trim($candidate) !== '') { + return $candidate; + } + } + + return 'Unknown Item'; + } + + private function syncDescriptionData(Item $item, array $itemPayload, array $raw): void + { + $descriptionData = Arr::get($itemPayload, 'stdItem.DescriptionData', []); + + if (! is_array($descriptionData) || $descriptionData === []) { + return; + } + + foreach ($descriptionData as $name => $value) { + if (! is_string($name) || $name === '') { + continue; + } + + ItemDescriptionData::query()->updateOrCreate( + [ + 'item_id' => $item->id, + 'name' => $name, + ], + [ + 'value' => is_scalar($value) ? (string) $value : json_encode($value), + ] + ); + } + } + + private function syncTranslations(Item $item, array $itemPayload, array $raw): void + { + $updated = $this->syncEnglishTranslation($item, $raw, $itemPayload); + + $descriptionLabel = $this->extractDescriptionLabel($raw); + + if ($descriptionLabel === null) { + return; + } + + foreach ([Language::CHINESE, Language::GERMAN] as $language) { + try { + $updated = $this->syncLanguageTranslation($item, $descriptionLabel, $language) || $updated; + } catch (\Exception $e) { + \Log::warning("Failed to sync {$language} translation", [ + 'item_id' => $item->id, + 'label' => $descriptionLabel, + 'error' => $e->getMessage(), + ]); + } + } + + if ($updated) { + $item->save(); + } + } + + private function syncEnglishTranslation(Item $item, array $raw, array $itemPayload): bool + { + $english = $this->extractEnglishDescription($raw, $itemPayload); + + if ($english === null || $english === '') { + return false; + } + + $item->setTranslation('translation', Language::ENGLISH, $english); + + return true; + } + + private function syncLanguageTranslation(Item $item, string $label, string $localeCode): bool + { + $translation = $this->getLabels()->getTranslation($localeCode, $label); + + if ($translation === null || $translation === '') { + return false; + } + + $item->setTranslation('translation', $localeCode, $this->getDescriptionText($translation)); + + return true; + } + + private function extractDescriptionLabel(array $raw): ?string + { + $component = Arr::get($raw, 'Entity.Components.SAttachableComponentParams.AttachDef', []); + + $label = $component['Localization__Description'] ?? Arr::get($component, 'Localization.__Description'); + + if (! is_string($label)) { + return null; + } + + $label = trim($label); + + if ($label === '' || mb_strtolower($label) === '@loc_empty') { + return null; + } + + return ltrim($label, '@'); + } + + private function extractEnglishDescription(array $raw, array $itemPayload): ?string + { + $component = Arr::get($raw, 'Entity.Components.SAttachableComponentParams.AttachDef', []); + $localization = $component['Localization'] ?? []; + + $candidates = [ + Arr::get($itemPayload, 'stdItem.DescriptionText'), + Arr::get($itemPayload, 'stdItem.Description'), + Arr::get($localization, 'English.Description'), + Arr::get($localization, 'Description'), + ]; + + foreach ($candidates as $candidate) { + if (is_string($candidate) && trim($candidate) !== '') { + return $candidate; + } + } + + return null; + } + + private function getLabels(): Labels + { + if ($this->labels === null) { + $this->labels = new Labels; + } + + return $this->labels; + } + + private function getEntityTagsLookup(): Collection + { + if ($this->entityTagsLookup === null) { + $this->entityTagsLookup = EntityTag::query() + ->get(['id', 'uuid', 'name']) + ->keyBy('uuid'); + } + + return $this->entityTagsLookup; + } + + private function nullableInt(mixed $value): ?int + { + if ($value === null) { + return null; + } + + if (! is_numeric($value)) { + return null; + } + + return (int) $value; + } + + /** + * Tries to remove the leading part of a description containing data + */ + private function getDescriptionText(string $description): string + { + $description = str_replace('\\n \\n', '\\n\\n', $description); + + $description = trim(str_replace('\n', "\n", $description)); + $description = str_replace(['‘', '’', '`', '´', ' '], ['\'', '\'', '\'', '\'', ' '], $description); + $exploded = explode("\n\n", $description); + + if (count($exploded) === 1) { + $exploded = explode('\n\n', $exploded[0]); + } + + $exploded = array_filter($exploded, static function (string $part) { + return preg_match('/(:|\w:[\s| ])/u', $part) !== 1; + }); + + return trim(implode("\n\n", $exploded)); + } + + private function syncEntityTags(ItemData $itemData, array $itemPayload): void + { + $entityTagMap = $itemPayload['entity_tag_map'] ?? []; + + if (! is_array($entityTagMap) || $entityTagMap === []) { + $itemData->entityTags()->sync([]); + + return; + } + + $validTags = collect($entityTagMap) + ->filter(function ($tagData) { + return isset($tagData['tag'], $tagData['name']) && is_array($tagData) && is_string($tagData['tag']) && is_string($tagData['name']); + }); + + if ($validTags->isEmpty()) { + $itemData->entityTags()->sync([]); + + return; + } + + $lookup = $this->getEntityTagsLookup(); + + $missingTags = $validTags->filter(function ($tagData) use ($lookup) { + return ! $lookup->has($tagData['tag']); + }); + + if ($missingTags->isNotEmpty()) { + $tagsToCreate = $missingTags->map(function ($tagData) { + return [ + 'uuid' => $tagData['tag'], + 'name' => $tagData['name'], + ]; + })->values()->all(); + + EntityTag::query()->upsert( + $tagsToCreate, + ['uuid'], + ['name', 'updated_at'] + ); + + $this->entityTagsLookup = null; + } + + $tagIds = $validTags + ->map(fn ($tagData) => $this->getEntityTagsLookup()->get($tagData['tag'])) + ->filter() + ->pluck('id') + ->all(); + + $itemData->entityTags()->sync($tagIds); + } +} diff --git a/app/Jobs/Game/ImportVehicleData.php b/app/Jobs/Game/ImportVehicleData.php new file mode 100644 index 000000000..49c264539 --- /dev/null +++ b/app/Jobs/Game/ImportVehicleData.php @@ -0,0 +1,249 @@ +readPayload(); + $rawPayload = $this->readRawPayload(); + + if (! isset($payload['UUID'])) { + return; + } + + $vehicle = Vehicle::query()->firstOrCreate( + ['uuid' => $payload['UUID']], + ['uuid' => $payload['UUID']] + ); + + $manufacturerId = $this->resolveManufacturer($payload); + $shipmatrixId = $this->resolveShipmatrixVehicleId($payload); + + VehicleData::query()->updateOrCreate( + [ + 'vehicle_id' => $vehicle->id, + 'game_version_id' => $this->gameVersionId, + ], + $this->mapVehicleData($payload, $manufacturerId, $shipmatrixId) + ); + + $this->importVehicleItem($payload, $rawPayload, $manufacturerId); + } + + /** + * @throws JsonException + */ + private function readPayload(): array + { + $contents = Storage::disk('scunpacked')->get($this->path); + + return json_decode($contents, true, 512, JSON_THROW_ON_ERROR); + } + + /** + * @throws JsonException + */ + private function readRawPayload(): array + { + $rawPath = $this->buildRawPath(); + + if ($rawPath === null || Storage::disk('scunpacked')->missing($rawPath)) { + return []; + } + + $contents = Storage::disk('scunpacked')->get($rawPath); + $payload = json_decode($contents, true, 512, JSON_THROW_ON_ERROR); + + return is_array($payload) ? ($payload['Raw'] ?? []) : []; + } + + private function buildRawPath(): ?string + { + if (! str_ends_with($this->path, '.json')) { + return null; + } + + return preg_replace('/\\.json$/', '-raw.json', $this->path); + } + + private function importVehicleItem(array $payload, array $rawPayload, int $manufacturerId): void + { + $importer = new VehicleItemImporter($this->labels); + $importer->importFromVehiclePayload($this->gameVersionId, $payload, $rawPayload, $manufacturerId); + } + + private function resolveManufacturer(array $payload): int + { + $manufacturer = $payload['Manufacturer'] ?? null; + + if (! is_array($manufacturer)) { + throw new RuntimeException('Manufacturer data missing from payload. UUID: '.$payload['UUID'] ?? 'unknown'); + } + + $uuid = $manufacturer['UUID'] ?? null; + + if (! is_string($uuid) || $uuid === '') { + throw new RuntimeException('Manufacturer UUID missing from payload. UUID: '.$payload['UUID'] ?? 'unknown'); + } + + $record = Manufacturer::query()->where('uuid', $uuid)->first(); + + if ($record === null) { + throw new RuntimeException(sprintf('Manufacturer with UUID %s does not exist.', $uuid)); + } + + return $record->id; + } + + private function mapVehicleData(array $payload, ?int $manufacturerId, ?int $shipmatrixId): array + { + return [ + 'manufacturer_id' => $manufacturerId, + 'shipmatrix_id' => $shipmatrixId, + 'class_name' => $payload['ClassName'] ?? null, + 'name' => $payload['Name'] ?? null, + 'display_name' => $this->generateDisplayName($payload, $manufacturerId), + 'career' => $payload['Career'] ?? null, + 'role' => $payload['Role'] ?? null, + + 'is_vehicle' => (bool) Arr::get($payload, 'IsVehicle', false), + 'is_gravlev' => (bool) Arr::get($payload, 'IsGravlev', false), + 'is_spaceship' => (bool) Arr::get($payload, 'IsSpaceship', false), + + 'size' => Arr::get($payload, 'Size'), + + 'data' => $payload, + ]; + } + + private function resolveShipmatrixVehicleId(array $payload): ?int + { + return ($this->matcher ?? app(VehicleMatchingService::class))->findMatch($payload); + } + + private function normalizeName(string $name): string + { + $name = str_replace('_', ' ', $name); + $name = preg_replace('/\\s+/', ' ', $name ?? ''); + + return trim((string) $name); + } + + private function stripManufacturerPrefix(string $name, string $manufacturer): string + { + $pattern = sprintf('/^%s\\s+/i', preg_quote($manufacturer, '/')); + + return trim((string) preg_replace($pattern, '', $name)); + } + + /** + * Get possible short names for a manufacturer to use for prefix stripping. + * + * Returns an array of candidates to try when stripping manufacturer prefixes, + * ordered from most specific to least specific. + */ + private function getManufacturerShortNames(array $manufacturerData): array + { + $manufacturerName = Arr::get($manufacturerData, 'Name'); + + if ($manufacturerName === null || $manufacturerName === '') { + return []; + } + + $candidates = []; + + $specialCases = [ + 'Roberts Space Industries' => ['RSI'], + 'Consolidated Outland' => ['C.O.'], + 'Musashi Industrial & Starflight Concern' => ['MISC'], + ]; + + if (isset($specialCases[$manufacturerName])) { + $candidates = array_merge($candidates, $specialCases[$manufacturerName]); + } + + $candidates[] = $manufacturerName; + + $parts = explode(' ', $manufacturerName); + if (count($parts) > 0 && $parts[0] !== '') { + $candidates[] = $parts[0]; + } + + return array_unique($candidates); + } + + /** + * Generate a display name by stripping manufacturer prefix from the vehicle name. + * + * Examples: + * - "RSI Constellation Andromeda" > "Constellation Andromeda" + * - "Anvil F7C Hornet" > "F7C Hornet" + * - "F8C Lightning PYAM Exec" > "F8C Lightning PYAM Exec" (no prefix) + */ + private function generateDisplayName(array $payload, ?int $manufacturerId): ?string + { + $rawName = $payload['Name'] ?? null; + + if ($rawName === null || $rawName === '') { + return null; + } + + $normalized = $this->normalizeName($rawName); + + if ($manufacturerId === null) { + return $normalized; + } + + $manufacturerData = Arr::get($payload, 'Manufacturer', []); + + $shortNames = $this->getManufacturerShortNames($manufacturerData); + + foreach ($shortNames as $shortName) { + $stripped = $this->stripManufacturerPrefix($normalized, $shortName); + if ($stripped !== '' && $stripped !== $normalized) { + return $stripped; + } + } + + return $normalized; + } +} diff --git a/app/Jobs/Rsi/CommLink/Download/DownloadCommLink.php b/app/Jobs/Rsi/CommLink/Download/DownloadCommLink.php index e509c6c6e..b1fbd94d0 100644 --- a/app/Jobs/Rsi/CommLink/Download/DownloadCommLink.php +++ b/app/Jobs/Rsi/CommLink/Download/DownloadCommLink.php @@ -4,112 +4,108 @@ namespace App\Jobs\Rsi\CommLink\Download; -use App\Jobs\AbstractBaseDownloadData as BaseDownloadData; -use Carbon\Carbon; -use Illuminate\Bus\Queueable; +use App\Services\RsiDownloadClient; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Http\Client\RequestException; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; +use Illuminate\Foundation\Queue\Queueable; +use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -/** - * Downloads the Whole Page Content. - */ -class DownloadCommLink extends BaseDownloadData implements ShouldQueue +class DownloadCommLink implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; use Queueable; - use SerializesModels; - public const COMM_LINK_BASE_URL = 'https://robertsspaceindustries.com/comm-link'; + private const COMM_LINK_PATH = '/comm-link/SCW/%d-IMPORT'; - public const DISK = 'comm_links'; + private const TOKEN_PATTERN = "/'token'\\s?\\:\\s?'[A-Za-z0-9\\+\\:\\-_\\/]+'/"; - /** - * @var int Post ID - */ - private int $commLinkId = 0; + private const CONTENT_MARKERS = [ + 'id="post"', + 'id="subscribers"', + 'id="layout-system"', + ]; - private bool $skipExisting = false; + public int $timeout = 120; - /** - * Create a new job instance. - */ - public function __construct(int $commLinkId, bool $skipExisting = false) - { - $this->commLinkId = $commLinkId; - $this->skipExisting = $skipExisting; - } + public function __construct( + public readonly int $commLinkId, + public readonly bool $skipExisting = true, + ) {} - /** - * Execute the job. - */ - public function handle(): void + public function handle(RsiDownloadClient $client): void { - if ($this->skipExisting && Storage::disk(self::DISK)->exists($this->commLinkId)) { - app('Log')::debug( - "Skipping existing Comm-Link {$this->commLinkId}", - [ - 'id' => $this->commLinkId, - ] - ); + if ($this->skipExisting && Storage::disk('comm_links')->exists((string) $this->commLinkId)) { + Log::debug('Skipping existing Comm-Link download.', ['id' => $this->commLinkId]); return; } - app('Log')::info( - "Downloading Comm-Link with ID {$this->commLinkId}", - [ + $response = $client->base()->get($this->buildUrl()); + + if ($response->serverError()) { + Log::warning('Comm-Link download failed with server error.', [ 'id' => $this->commLinkId, - ] - ); + 'status' => $response->status(), + ]); - $response = $this->makeClient()->get( - sprintf('%s/%s/%d-IMPORT', self::COMM_LINK_BASE_URL, 'SCW', $this->commLinkId) - ); + $this->release(300); - if (! $response->successful()) { - $this->fail(new RequestException($response)); + return; + } + + if ($response->clientError()) { + Log::info('Comm-Link download failed with client error.', [ + 'id' => $this->commLinkId, + 'status' => $response->status(), + ]); return; } - $content = $this->removeRsiToken($response->body()); + $content = $this->sanitizeContent($response->body()); - if (! Str::contains($content, ['id="post"', 'id="subscribers"', 'id="layout-system"'])) { - app('Log')::info( - "Comm-Link with ID {$this->commLinkId} does not exist", - [ - 'id' => $this->commLinkId, - ] - ); + if (! Str::contains($content, self::CONTENT_MARKERS)) { + Log::info('Comm-Link download skipped due to missing content markers.', [ + 'id' => $this->commLinkId, + ]); return; } - $this->writeFile($content); + Storage::disk('comm_links')->put($this->filePath(), $content); + $this->pruneStoredFiles(); + } + + private function buildUrl(): string + { + return rtrim((string) config('services.rsi_url'), '/').sprintf(self::COMM_LINK_PATH, $this->commLinkId); } - /** - * Strips the X-RSI Token from the Page. - */ - private function removeRsiToken(string $content): string + private function sanitizeContent(string $content): string { - return preg_replace('/\'token\'\s?\:\s?\'[A-Za-z0-9\+\:\-\_\/]+\'/', '\'token\' : \'\'', $content); + return preg_replace(self::TOKEN_PATTERN, "'token' : ''", $content) ?? $content; } - /** - * Write the Comm-Link to disk - */ - private function writeFile(string $content): void + private function filePath(): string { - Storage::disk(self::DISK)->put( - sprintf('%d/%s.html', $this->commLinkId, Carbon::now()->format('Y-m-d_His')), - $content - ); + $filename = Carbon::now()->format('Y-m-d_His'); + + return sprintf('%d/%s.html', $this->commLinkId, $filename); + } + + private function pruneStoredFiles(): void + { + $files = Storage::disk('comm_links')->files((string) $this->commLinkId); + + if (count($files) <= 2) { + return; + } + + sort($files); + + $filesToDelete = array_slice($files, 1, -1); + + Storage::disk('comm_links')->delete($filesToDelete); } } diff --git a/app/Jobs/Rsi/CommLink/Download/DownloadMissingCommLinks.php b/app/Jobs/Rsi/CommLink/Download/DownloadMissingCommLinks.php index 7740d8db5..b271c005d 100644 --- a/app/Jobs/Rsi/CommLink/Download/DownloadMissingCommLinks.php +++ b/app/Jobs/Rsi/CommLink/Download/DownloadMissingCommLinks.php @@ -4,159 +4,98 @@ namespace App\Jobs\Rsi\CommLink\Download; -use App\Jobs\AbstractBaseDownloadData as BaseDownloadData; use App\Models\Rsi\CommLink\CommLink; -use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Database\Eloquent\ModelNotFoundException; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; +use Illuminate\Foundation\Queue\Queueable; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; use Symfony\Component\DomCrawler\Crawler; -/** - * Download all missing Comm-Links based on the last DB entry. - * Extracts the highest Comm-Link-Id from 'https://robertsspaceindustries.com/comm-link' - * And Dispatches download-jobs for ID - DB_ID - * - * If No Comm-Link was found in the DB, the first Comm-Link ID (12663) will be used. - * - * Existing Comm-Links are skipped. - */ -class DownloadMissingCommLinks extends BaseDownloadData implements ShouldQueue +class DownloadMissingCommLinks implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; use Queueable; - use SerializesModels; public const FIRST_COMM_LINK_ID = 12663; - public const COMM_LINK_BASE_URL = 'https://robertsspaceindustries.com/comm-link'; + public int $timeout = 120; - /** - * Execute the job. - */ public function handle(): void { - app('Log')::info('Starting Missing Comm-Links Download Job'); + Log::info('Starting Comm-Link missing download scan.'); - $response = $this->makeClient()->get(self::COMM_LINK_BASE_URL); + $response = Http::timeout(60)->get($this->hubUrl()); + + if ($response->serverError()) { + Log::warning('Comm-Link hub request failed with server error.', [ + 'status' => $response->status(), + ]); - if (! $response->successful()) { - app('Log')::error('Could not connect to RSI, retrying in 5 minutes.'); $this->release(300); return; } + if ($response->clientError()) { + Log::info('Comm-Link hub request failed with client error.', [ + 'status' => $response->status(), + ]); + + return; + } + $postIds = $this->extractPostIds($response->body()); - if (empty($postIds)) { - app('Log')::info('Could not retrieve latest Comm-Link ID, retrying in 1 minute.'); + if ($postIds === []) { + Log::info('Comm-Link hub returned no IDs.'); $this->release(60); return; } $latestPostId = max($postIds); + $latestDbId = CommLink::query()->max('cig_id') ?? self::FIRST_COMM_LINK_ID - 1; - app('Log')::info( - "Latest Comm-Link ID is: {$latestPostId}", - [ - 'id' => $latestPostId, - ] - ); + foreach ($postIds as $postId) { + dispatch(new DownloadCommLink($postId, true)); + } - $this->downloadCommLinks($postIds); + $startId = max(self::FIRST_COMM_LINK_ID, $latestDbId + 1); + for ($id = $startId; $id <= $latestPostId; $id++) { + dispatch(new DownloadCommLink($id, true)); + } } - /** - * Extracts Post ids from html - * - * - * @return array Ids - */ - private function extractPostIds(string $body): array + private function hubUrl(): string { - $postIds = []; - - $crawler = new Crawler; - - $crawler->addHtmlContent($body, 'UTF-8'); - $crawler->filter('#channel .hub-blocks .hub-block') - ->each( - function (Crawler $crawler) use (&$postIds) { - $link = $crawler->filter('a'); - $postIds[] = $this->extractIdFromLink($link); - } - ); - - return $postIds; + return rtrim((string) config('services.rsi_url'), '/').'/comm-link'; } /** - * Extract latest Comm-Link id from Website + * @return array */ - private function extractIdFromLink(Crawler $link): int + private function extractPostIds(string $body): array { - $linkHref = $link->attr('href'); - - if ($linkHref === null) { - return 0; - } + $crawler = new Crawler; + $crawler->addHtmlContent($body, 'UTF-8'); - $linkHref = explode('/', $linkHref); - $linkHref = end($linkHref); - $linkHref = explode('-', $linkHref); + $ids = $crawler->filter('#channel .hub-blocks .hub-block') + ->each(function (Crawler $crawler): int { + $href = $crawler->filter('a')->attr('href'); - return (int) $linkHref[0]; - } + if ($href === null) { + return 0; + } - /** - * Dispatches download jobs for all missing ids - */ - private function downloadCommLinks(array $postIDs): void - { - $latestPostId = max($postIDs); - - try { - $dbIds = CommLink::query() - ->select('cig_id') - ->take(count($postIDs)) - ->orderByDesc('cig_id') - ->get() - ->pluck('cig_id'); - } catch (ModelNotFoundException $e) { - $dbIds = collect([self::FIRST_COMM_LINK_ID - 1]); - } + $segments = explode('/', $href); + $slug = end($segments) ?: ''; + $parts = explode('-', $slug); - $missing = collect($postIDs)->diff($dbIds); - - $missing->each( - function (int $id) { - dispatch(new DownloadCommLink($id, true)); - } - ); - - $dbId = $dbIds->max(); - if ($dbId > 0) { - app('Log')::info( - "Latest DB Comm-Link ID is: {$dbId}", - [ - 'id' => $dbId, - ] - ); - $dbId++; - } else { - app('Log')::info('No Comm-Links in DB found'); - $dbId = self::FIRST_COMM_LINK_ID; - } + return (int) ($parts[0] ?? 0); + }); - for ($id = $dbId; $id <= $latestPostId; $id++) { - if (! $missing->contains($id)) { - dispatch(new DownloadCommLink($id, true)); - } - } + return collect($ids) + ->filter(static fn (int $id) => $id >= self::FIRST_COMM_LINK_ID) + ->values() + ->all(); } } diff --git a/app/Jobs/Rsi/CommLink/Download/Image/DownloadCommLinkImage.php b/app/Jobs/Rsi/CommLink/Download/Image/DownloadCommLinkImage.php deleted file mode 100644 index 30be955ac..000000000 --- a/app/Jobs/Rsi/CommLink/Download/Image/DownloadCommLinkImage.php +++ /dev/null @@ -1,115 +0,0 @@ -image = $image; - } - - /** - * Execute the job. - */ - public function handle(): void - { - app('Log')::info( - "Downloading Comm-Link Image {$this->image->name}", - [ - 'id' => $this->image->id, - 'src' => $this->image->src, - ] - ); - - $localDirName = $this->image->dir ?? $this->generateLocalDirName(); - - if (Storage::disk('comm_link_images')->exists(sprintf('%s/%s', $localDirName, $this->image->name))) { - return; - } - - $response = $this->makeClient()->get($this->image->url); - - if ($response->serverError()) { - app('Log')::critical('Could not connect to RSI Website'); - - $this->fail(new RequestException($response)); - - return; - } - - if ($response->clientError()) { - app('Log')::info( - "Could not download Comm-Link Image {$this->image->name}", - [ - 'url' => $this->image->url, - ] - ); - - $this->image->update( - [ - 'local' => false, - 'dir' => 'NOT_FOUND', - ] - ); - - return; - } - - $this->writeImage($response->body(), $localDirName); - - $this->image->update( - [ - 'local' => true, - 'dir' => $localDirName, - ] - ); - } - - private function generateLocalDirName(): string - { - try { - return bin2hex(random_bytes(7)); - } catch (Exception $e) { - return Str::random(14); - } - } - - /** - * Writes the image data to file - */ - private function writeImage(string $data, string $folder): void - { - Storage::disk('comm_link_images')->put( - sprintf('%s/%s', $folder, $this->image->name), - $data - ); - } -} diff --git a/app/Jobs/Rsi/CommLink/Download/Image/DownloadCommLinkImages.php b/app/Jobs/Rsi/CommLink/Download/Image/DownloadCommLinkImages.php deleted file mode 100644 index 52088ff6e..000000000 --- a/app/Jobs/Rsi/CommLink/Download/Image/DownloadCommLinkImages.php +++ /dev/null @@ -1,45 +0,0 @@ -where('local', false)->chunk( - 100, - function (Collection $images) { - $images->each( - function (Image $image) { - dispatch(new DownloadCommLinkImage($image))->onQueue('comm_link_images'); - } - ); - } - ); - } -} diff --git a/app/Jobs/Rsi/CommLink/Download/ReDownloadDbCommLinks.php b/app/Jobs/Rsi/CommLink/Download/ReDownloadDbCommLinks.php index 0026f798d..067b0ff56 100644 --- a/app/Jobs/Rsi/CommLink/Download/ReDownloadDbCommLinks.php +++ b/app/Jobs/Rsi/CommLink/Download/ReDownloadDbCommLinks.php @@ -4,64 +4,37 @@ namespace App\Jobs\Rsi\CommLink\Download; -use App\Jobs\AbstractBaseDownloadData as BaseDownloadData; use App\Models\Rsi\CommLink\CommLink; -use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; -use InvalidArgumentException; +use Illuminate\Foundation\Queue\Queueable; +use Illuminate\Support\Facades\Log; +use RuntimeException; -/** - * Re-Downloads a new Version of all existing Database Comm-Links - */ -class ReDownloadDbCommLinks extends BaseDownloadData implements ShouldQueue +class ReDownloadDbCommLinks implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; use Queueable; - use SerializesModels; public const FIRST_COMM_LINK_ID = 12663; - private bool $skipExisting; + public int $timeout = 120; - /** - * ReDownloadDbCommLinks constructor. - * - * @param bool $skipExisting Don't download existing Comm-Links - */ - public function __construct(bool $skipExisting = false) - { - $this->skipExisting = $skipExisting; - } + public function __construct(public readonly bool $skipExisting = true) {} - /** - * Execute the job. - */ public function handle(): void { - app('Log')::info('Re-Downloading all DB Comm-Links'); + $latestDbId = CommLink::query()->max('cig_id'); - $latestDbPost = CommLink::query()->orderByDesc('cig_id')->first(); - if ($latestDbPost === null) { - $this->fail(new InvalidArgumentException('No Comm-Links in DB Found')); + if ($latestDbId === null) { + $this->fail(new RuntimeException('No Comm-Links found in database.')); return; } - app('Log')::info( - "Latest DB Comm-Link CIG ID: {$latestDbPost->cig_id}", - [ - 'cig_id' => $latestDbPost->cig_id, - ] - ); - - $this->makeClient(false); + Log::info('Re-downloading Comm-Links from database.', ['max_id' => $latestDbId]); - for ($id = self::FIRST_COMM_LINK_ID; $id <= $latestDbPost->cig_id; $id++) { - dispatch(new DownloadCommLink($id, $this->skipExisting))->delay(($id - self::FIRST_COMM_LINK_ID) * 30); + for ($id = self::FIRST_COMM_LINK_ID; $id <= $latestDbId; $id++) { + dispatch(new DownloadCommLink($id, $this->skipExisting)) + ->delay(($id - self::FIRST_COMM_LINK_ID) * 30); } } } diff --git a/app/Jobs/Rsi/CommLink/Image/ComputeImageHash.php b/app/Jobs/Rsi/CommLink/Image/ComputeImageHash.php new file mode 100644 index 000000000..e761e1cd6 --- /dev/null +++ b/app/Jobs/Rsi/CommLink/Image/ComputeImageHash.php @@ -0,0 +1,140 @@ +onConnection('database'); + $this->onQueue('comm-link-hashes'); + } + + /** + * @return array + */ + public function middleware(): array + { + return [ + new WithoutOverlapping($this->imageId), + ]; + } + + public function handle(PdqHasher $hasher): void + { + $record = ImageHash::query() + ->getConnection() + ->table('comm_link_images') + ->select(['id', 'src', 'local', 'dir']) + ->where('id', $this->imageId) + ->first(); + + if ($record === null) { + return; + } + + $hasHash = ImageHash::query() + ->where('comm_link_image_id', $record->id) + ->whereNotNull('pdq_hash') + ->exists(); + + if ($hasHash) { + return; + } + + $response = Http::get($this->resolveImageUrl($record->src, (bool) $record->local, (string) $record->dir)); + + if ($response->serverError()) { + Log::warning('Comm-link image download failed with server error.', [ + 'image_id' => $record->id, + 'status' => $response->status(), + ]); + + $this->release(300); + + return; + } + + if ($response->clientError()) { + Log::info('Comm-link image download failed with client error.', [ + 'image_id' => $record->id, + 'status' => $response->status(), + ]); + + return; + } + + $contentType = $response->header('Content-Type'); + if ($contentType !== null && ! str_starts_with($contentType, 'image/')) { + Log::info('Comm-link image skipped because content type is not an image.', [ + 'image_id' => $record->id, + 'content_type' => $contentType, + ]); + + return; + } + + try { + $hashResult = $hasher->hashContents($response->body()); + } catch (RuntimeException $exception) { + Log::info('Comm-link image hashing failed.', [ + 'image_id' => $record->id, + 'message' => $exception->getMessage(), + ]); + + return; + } + + ImageHash::query()->updateOrCreate( + [ + 'comm_link_image_id' => $record->id, + ], + [ + 'pdq_hash' => $hashResult->toBitString(), + 'pdq_quality' => $hashResult->quality, + ] + ); + } + + private function resolveImageUrl(string $src, bool $local, string $dir): string + { + if ($local) { + $name = basename($src); + + return asset("storage/comm_link_images/{$dir}/{$name}"); + } + + $prefixes = ['/media', '/rsi', '/layoutscache', '/i/']; + $baseUrl = 'https://media.robertsspaceindustries.com'; + + foreach ($prefixes as $prefix) { + if (str_starts_with($src, $prefix)) { + $baseUrl = config('services.rsi_url'); + break; + } + } + + return $baseUrl.$src; + } +} diff --git a/app/Jobs/Rsi/CommLink/Image/ComputeSimilarImageIds.php b/app/Jobs/Rsi/CommLink/Image/ComputeSimilarImageIds.php index 99664b897..e3a191cd9 100644 --- a/app/Jobs/Rsi/CommLink/Image/ComputeSimilarImageIds.php +++ b/app/Jobs/Rsi/CommLink/Image/ComputeSimilarImageIds.php @@ -18,33 +18,42 @@ class ComputeSimilarImageIds implements ShouldQueue use Queueable; use SerializesModels; - private Image $image; + public int $timeout = 300; - public function __construct(Image $image) + public function __construct(public readonly int $imageId) { - $this->image = $image; + // } - /** - * Execute the job. - */ public function handle(): void { - $this->image->refresh(); - if ($this->image->base_image_id !== null) { + $image = Image::query()->find($this->imageId); + + if ($image === null) { + return; + } + + if ($image->base_image_id !== null) { return; } - $this->image->similarImages(95, 50)->each(function (Image $duplicate) { - unset($duplicate->similarity, $duplicate->similarity_method, $duplicate->pdq_distance); + $similarImages = $image->similarImages(95, 50); - if ($duplicate->base_image_id === $this->image->id) { - return; + foreach ($similarImages as $duplicate) { + // Skip if duplicate already points to this image + if ($duplicate->base_image_id === $image->id) { + continue; } - $duplicate->update([ - 'base_image_id' => $this->image->id, - ]); - }); + unset( + $duplicate->similarity, + $duplicate->similarity_method, + $duplicate->pdq_hash, + $duplicate->pdq_quality, + $duplicate->distance + ); + + $duplicate->update(['base_image_id' => $image->id]); + } } } diff --git a/app/Jobs/Rsi/CommLink/Image/CreateImageHash.php b/app/Jobs/Rsi/CommLink/Image/CreateImageHash.php deleted file mode 100644 index 9be11452b..000000000 --- a/app/Jobs/Rsi/CommLink/Image/CreateImageHash.php +++ /dev/null @@ -1,240 +0,0 @@ -image = $image; - - $this->perceptionHasher = new ImageHash(new PerceptualHash2(32)); - $this->differenceHasher = new ImageHash(new DifferenceHash); - $this->averageHasher = new ImageHash(new AverageHash); - } - - /** - * Delete possible temp files when done - */ - public function __destruct() - { - if ($this->deleteTempFile) { - File::delete($this->tempFileUrl); - } - } - - /** - * Execute the job. - */ - public function handle(): void - { - if (! extension_loaded('gd') && ! extension_loaded('imagick')) { - app('Log')::error('Required extension "GD" or "Imagick" not available.'); - $this->fail('Required extension "GD" or "Imagick" not available.'); - - return; - } - - if ($this->image->hash !== null && $this->image->hash->exists && $this->image->hash->pdq_hash1 !== null) { - $this->delete(); - - return; - } - - if ($this->image->local) { - $fileUrl = $this->image->local_path; - } else { - $fileUrl = $this->image->getLocalOrRemoteUrl(); - } - - if (str_contains($this->image->metadata->mime, 'video')) { - if (! $this->image->local) { - $this->fail('Can\'t extract frame from remote file.'); - - return; - } - - $fileUrl = $this->saveVideoFrame(); - if ($fileUrl === null) { - $this->fail(sprintf('Could not extract frame from video %s', $this->image->name)); - - return; - } - - $this->deleteTempFile = true; - $this->tempFileUrl = $fileUrl; - } - - $pdqFromStream = false; - if (! $this->image->local) { - if (Storage::disk('comm_link_images')->exists("{$this->image->dir}/{$this->image->name}")) { - $this->image->update(['local' => true]); - } else { - $pdqFromStream = true; - $fileUrl = $this->downloadFile($fileUrl); - } - } - - // 4xx Error - if ($fileUrl === null) { - return; - } - - try { - $hash = $this->perceptionHasher->hash($fileUrl); - } catch (NotReadableException $e) { - app('Log')::info("Image $fileUrl is not readable", [$fileUrl]); - $this->fail($e); - - return; - } - - $perception = $hash->toHex(); - $difference = $this->differenceHasher->hash($fileUrl)->toHex(); - $average = $this->averageHasher->hash($fileUrl)->toHex(); - - try { - [$hash, $quality] = PDQHasher::computeHashAndQualityFromFilename( - $fileUrl, - true, - $pdqFromStream - ); - } catch (Exception $e) { - $this->fail($e->getMessage()); - - return; - } - - /** @var PDQHash $hash */ - $hash = $hash->to64BitStrings(); - - if ($perception === '' || $difference === '' || $average === '') { - app('Log')::debug("Hash for $fileUrl is empty.", [$fileUrl]); - - return; - } - - $this->image->hash()->updateOrCreate( - [ - 'perceptual_hash' => hex2bin($perception), - 'difference_hash' => hex2bin($difference), - 'average_hash' => hex2bin($average), - 'pdq_hash1' => hex2bin($hash[0]), - 'pdq_hash2' => hex2bin($hash[1]), - 'pdq_hash3' => hex2bin($hash[2]), - 'pdq_hash4' => hex2bin($hash[3]), - 'pdq_quality' => $quality, - ] - ); - } - - /** - * Downloads a file and returns the content - * - * - * @param bool $selfCall Don't retry indefinitely - */ - private function downloadFile(string $url, bool $selfCall = false): ?string - { - $response = $this->makeClient()->get($url); - - if ($response->serverError()) { - app('Log')::debug( - 'Download of Comm-Link image failed. Retrying in 300 seconds.', - [$url, $response->status()] - ); - - $this->release(300); - - return null; - } - - if ($response->clientError()) { - if (! $selfCall && $response->status() === 404) { - $url = str_replace('/source/', '/post/', $url); - - app('Log')::debug('Retrying download with smaller version.', [$url]); - - // Retry with smaller version - return $this->downloadFile($url, true); - } - - app('Log')::info("Download of Comm-Link image resulted in code {$response->status()}", [$url]); - - return null; - } - - return $response->body(); - } - - /** - * Use FFMPEG to retrieve a frame from second 1 - */ - private function saveVideoFrame(): ?string - { - $fp = tmpfile(); - $path = stream_get_meta_data($fp)['uri']; - fclose($fp); - $pathExt = $path.'.jpg'; - - $proc = new Process([ - '/usr/bin/ffmpeg', - '-i', - $this->image->local_path, - '-an', - '-ss', - '1', - '-y', - '-f', - 'mjpeg', - $pathExt, - ]); - - $proc->setTimeout(120); - - $code = $proc->run(); - - return $code === 0 ? $pathExt : null; - } -} diff --git a/app/Jobs/Rsi/CommLink/Image/CreateImageHashes.php b/app/Jobs/Rsi/CommLink/Image/CreateImageHashes.php deleted file mode 100644 index c9e290fbc..000000000 --- a/app/Jobs/Rsi/CommLink/Image/CreateImageHashes.php +++ /dev/null @@ -1,84 +0,0 @@ -commLinkIds = $commLinkIds; - } - - /** - * Execute the job. - */ - public function handle(): void - { - if (! extension_loaded('gd') && ! extension_loaded('imagick')) { - app('Log')::error('Required extension "GD" or "Imagick" not available.'); - - $this->fail('Required extension "GD" or "Imagick" not available.'); - - return; - } - - $query = Image::query() - ->whereHas( - 'commLinks', - function (Builder $query) { - $query->whereIn('cig_id', $this->commLinkIds); - } - ) - ->whereDoesntHave('hash') - ->whereHas( - 'metadata', - function (Builder $query) { - $query->where('size', '<', 1024 * 1024 * 10); // Max 10MB files - } - ) - ->where( - function (Builder $query) { - $query->orWhereRaw('LOWER(src) LIKE \'%.jpg\'') - ->orWhereRaw('LOWER(src) LIKE \'%.jpeg\'') - ->orWhereRaw('LOWER(src) LIKE \'%.png\'') - ->orWhereRaw('LOWER(src) LIKE \'%.webp\''); - } - ); - - $query->chunk( - 100, - function (Collection $images) { - $images->each( - function (Image $image) { - dispatch(new CreateImageHash($image)); - } - ); - } - ); - } -} diff --git a/app/Jobs/Rsi/CommLink/Image/CreateImageMetadata.php b/app/Jobs/Rsi/CommLink/Image/CreateImageMetadata.php index f14cd4979..92516f9a1 100644 --- a/app/Jobs/Rsi/CommLink/Image/CreateImageMetadata.php +++ b/app/Jobs/Rsi/CommLink/Image/CreateImageMetadata.php @@ -5,57 +5,35 @@ namespace App\Jobs\Rsi\CommLink\Image; use App\Models\Rsi\CommLink\Image\Image; -use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Collection; +use Illuminate\Foundation\Queue\Queueable; class CreateImageMetadata implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; use Queueable; - use SerializesModels; /** - * @var int Comm-Link IDs to operate on + * @param array $commLinkIds */ - private $commLinkIds; + public function __construct(public readonly array $commLinkIds = []) {} - /** - * Create a new job instance. - */ - public function __construct(array $commLinkIds = []) - { - $this->commLinkIds = $commLinkIds; - } - - /** - * Execute the job. - */ public function handle(): void { $query = Image::query() - ->whereHas( - 'commLinks', - function (Builder $query) { - $query->whereIn('cig_id', $this->commLinkIds); - } - ) + ->whereHas('commLinks') ->whereDoesntHave('metadata'); - $query->chunk( - 100, - function (Collection $images) { - $images->each( - function (Image $image) { - dispatch(new CreateImageMetadatum($image)); - } - ); + if ($this->commLinkIds !== []) { + $query->whereHas('commLinks', function (Builder $builder): void { + $builder->whereIn('cig_id', $this->commLinkIds); + }); + } + + $query->orderBy('id')->chunkById(100, function ($images): void { + foreach ($images as $image) { + dispatch(new CreateImageMetadatum($image->id)); } - ); + }); } } diff --git a/app/Jobs/Rsi/CommLink/Image/CreateImageMetadatum.php b/app/Jobs/Rsi/CommLink/Image/CreateImageMetadatum.php index 5c83b3143..48c1358f7 100644 --- a/app/Jobs/Rsi/CommLink/Image/CreateImageMetadatum.php +++ b/app/Jobs/Rsi/CommLink/Image/CreateImageMetadatum.php @@ -4,44 +4,37 @@ namespace App\Jobs\Rsi\CommLink\Image; -use App\Jobs\AbstractBaseDownloadData as BaseDownloadData; use App\Models\Rsi\CommLink\Image\Image; use Carbon\Carbon; -use Illuminate\Bus\Queueable; +use Exception; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Http\Client\Response; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; +use Illuminate\Foundation\Queue\Queueable; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; -class CreateImageMetadatum extends BaseDownloadData implements ShouldQueue +class CreateImageMetadatum implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; use Queueable; - use SerializesModels; - private Image $image; + public int $timeout = 120; - /** - * Create a new job instance. - */ - public function __construct(Image $image) - { - $this->image = $image; - } + public function __construct(public readonly int $imageId) {} - /** - * Execute the job. - */ public function handle(): void { - $url = $this->image->url; + $image = Image::query()->with('metadata')->find($this->imageId); + + if ($image === null) { + return; + } - $response = $this->makeClient()->head($url); + $response = Http::timeout(30)->head($image->url); if ($response->serverError()) { - app('Log')::debug('Header request failed. Retrying in 300 seconds.', [$url, $response->status()]); + Log::warning('Comm-Link image metadata request failed with server error.', [ + 'image_id' => $image->id, + 'status' => $response->status(), + ]); $this->release(300); @@ -49,43 +42,42 @@ public function handle(): void } if ($response->clientError()) { - app('Log')::info("Header request resulted in code {$response->status()}", [$url]); - - if ($this->image->metadata === null) { - $this->image->metadata()->create( - [ - 'mime' => 'undefined', - 'size' => 0, - 'last_modified' => '0001-01-01 00:00:00', - ] - ); + Log::info('Comm-Link image metadata request failed with client error.', [ + 'image_id' => $image->id, + 'status' => $response->status(), + ]); + + if ($image->metadata === null || $image->metadata->mime === 'undefined') { + $image->metadata()->updateOrCreate([ + 'comm_link_image_id' => $image->id, + ], [ + 'mime' => 'undefined', + 'size' => 0, + 'last_modified' => '0001-01-01 00:00:00', + ]); } return; } - $this->saveMetadata($response); - } - - /** - * Saves response data as metadata - */ - private function saveMetadata(Response $response): void - { $data = [ 'mime' => $response->header('content-type'), 'size' => $response->header('content-length'), - 'last_modified' => Carbon::parse($response->header('last-modified'))->toDateTimeString(), + 'last_modified' => $response->header('last-modified'), ]; - foreach ($data as $key => $datum) { - if ($datum === '') { - unset($data[$key]); + if ($data['last_modified'] !== null) { + try { + $data['last_modified'] = Carbon::parse($data['last_modified'])->toDateTimeString(); + } catch (Exception $exception) { + $data['last_modified'] = null; } } - if ($this->image->metadata->mime === 'undefined') { - $this->image->metadata()->create($data); - } + $data = array_filter($data, static fn ($value) => $value !== null && $value !== ''); + + $image->metadata()->updateOrCreate([ + 'comm_link_image_id' => $image->id, + ], $data); } } diff --git a/app/Jobs/Rsi/CommLink/Image/DispatchImageHashes.php b/app/Jobs/Rsi/CommLink/Image/DispatchImageHashes.php new file mode 100644 index 000000000..fa0e811e3 --- /dev/null +++ b/app/Jobs/Rsi/CommLink/Image/DispatchImageHashes.php @@ -0,0 +1,45 @@ + $commLinkIds + */ + public function __construct(public readonly array $commLinkIds = []) {} + + public function handle(): void + { + $query = Image::query() + ->whereHas('commLinks') + ->where(function (Builder $query) { + $query->whereRelation('metadata', 'mime', 'LIKE', 'video%') + ->orWhereRelation('metadata', 'mime', 'LIKE', 'image%'); + }) + ->where('src', 'NOT LIKE', '%.svg') + ->where('src', 'NOT LIKE', '%.tiff') + ->whereDoesntHave('hash'); + + if ($this->commLinkIds !== []) { + $query->whereHas('commLinks', function (Builder $builder): void { + $builder->whereIn('cig_id', $this->commLinkIds); + }); + } + + $query->orderBy('id')->chunkById(100, function ($images): void { + foreach ($images as $image) { + ComputeImageHash::dispatch($image->id); + } + }); + } +} diff --git a/app/Jobs/Rsi/CommLink/Import/ImportCommLink.php b/app/Jobs/Rsi/CommLink/Import/ImportCommLink.php index 7528f4da5..d21a33166 100644 --- a/app/Jobs/Rsi/CommLink/Import/ImportCommLink.php +++ b/app/Jobs/Rsi/CommLink/Import/ImportCommLink.php @@ -4,9 +4,7 @@ namespace App\Jobs\Rsi\CommLink\Import; -use App\Jobs\Rsi\CommLink\Download\DownloadCommLink; use App\Models\Rsi\CommLink\CommLink; -use App\Models\Rsi\CommLink\CommLinksChanged; use App\Models\System\Language; use App\Services\Parser\CommLink\Content; use App\Services\Parser\CommLink\Image; @@ -15,126 +13,68 @@ use Carbon\Carbon; use Carbon\Exceptions\InvalidFormatException; use Exception; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; -use Illuminate\Bus\Queueable; +use Illuminate\Bus\Batchable; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; +use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use RuntimeException; use Symfony\Component\DomCrawler\Crawler; -/** - * Parses the HTML File and extracts all needed Data. - */ class ImportCommLink implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; + use Batchable; use Queueable; - use SerializesModels; - /** - * Comm-Link Post CSS Selector. - */ public const POST_SELECTOR = '#post'; - /** - * Comm-Link Post CSS Selector. - */ public const SUBSCRIBERS_SELECTOR = '#subscribers'; public const SPECIAL_PAGE_SELECTOR = '#layout-system'; - /** - * Alexandria components selector - */ - public const ALEXANDRIA_SELECTOR = 'g-platform-client-component, g-banner-advanced, g-navigation-sales'; - - /** - * @var int Comm-Link ID - */ - private int $commLinkId; - - /** - * @var string File in the Comm-Link ID Folder - */ - private string $file; + private const ALEXANDRIA_SELECTOR = 'g-platform-client-component, g-banner-advanced, g-navigation-sales'; - private ?CommLink $commLinkModel; + private const ALEXANDRIA_URL_PATTERN = 'https://robertsspaceindustries.com/alexandria/html'; - /** - * True if the given file content should be imported into the comm link model. - */ - private bool $forceImport; + public int $timeout = 120; private Crawler $crawler; - private const ALEXANDRIA_URL_PATTERN = 'https://robertsspaceindustries.com/alexandria/html'; + public function __construct( + public readonly int $commLinkId, + public readonly string $file, + public readonly bool $forceImport = false, + ) {} /** - * Create a new job instance. - * - * @param int $id Comm-Link ID - * @param string $file Current File Name - * @param CommLink|null $commLink Optional Comm-Link Model to update - * @param bool $forceImport Flag to Force Import from the current file - */ - public function __construct(int $id, string $file, ?CommLink $commLink = null, bool $forceImport = false) - { - $this->commLinkId = $id; - $this->file = $file; - $this->commLinkModel = $commLink; - $this->forceImport = $forceImport; - } - - /** - * Execute the job. - * - * * @throws FileNotFoundException */ public function handle(): void { - app('Log')::info( - "Parsing Comm-Link with ID {$this->commLinkId}", - [ - 'id' => $this->commLinkId, - 'file' => $this->file, - 'comm_link_already_in_db' => $this->commLinkModel !== null, - 'force_import' => $this->forceImport === true, - ] - ); + Log::info('Parsing Comm-Link import file.', [ + 'id' => $this->commLinkId, + 'file' => $this->file, + 'force_import' => $this->forceImport, + ]); $content = Storage::disk('comm_links')->get($this->filePath()); + if ($content === null) { throw new FileNotFoundException; } - // Check and process dynamic content if needed if ($this->containsDynamicContent($content)) { try { $content = $this->processDynamicContent($content); - // Save the updated content Storage::disk('comm_links')->put($this->filePath(), $content); - app('Log')::info( - "Successfully processed dynamic content for Comm-Link {$this->commLinkId}", - [ - 'file' => $this->file, - ] - ); - } catch (Exception $e) { - app('Log')::error( - "Failed to process dynamic content for Comm-Link {$this->commLinkId}", - [ - 'error' => $e->getMessage(), - 'file' => $this->file, - ] - ); + } catch (Exception $exception) { + Log::warning('Failed to process dynamic Comm-Link content.', [ + 'id' => $this->commLinkId, + 'message' => $exception->getMessage(), + ]); } } @@ -146,144 +86,135 @@ public function handle(): void $specialPage = $this->crawler->filter(self::SPECIAL_PAGE_SELECTOR); $alexandriaComponents = $this->crawler->filter(self::ALEXANDRIA_SELECTOR); - // Check if we have any content to parse if ( $post->count() === 0 && $subscribers->count() === 0 && $specialPage->count() === 0 && $alexandriaComponents->count() === 0 ) { - app('Log')::info("Comm-Link with id {$this->commLinkId} has no content"); + Log::info('Comm-Link import skipped due to empty content.', [ + 'id' => $this->commLinkId, + ]); return; } - if ($this->commLinkModel === null || $this->forceImport) { - $this->createCommLink(); // Updates or Creates - } else { - $this->checkCommLinkForChanges(); + $commLink = CommLink::query()->where('cig_id', $this->commLinkId)->first(); + + if ($commLink === null || $this->forceImport) { + $this->createCommLink(); + + return; } + + $this->updateCommLink($commLink); + } + + private function filePath(): string + { + return sprintf('%d/%s', $this->commLinkId, $this->file); } - /** - * Check if the content contains dynamic content loading - */ private function containsDynamicContent(string $content): bool { return str_contains($content, self::ALEXANDRIA_URL_PATTERN); } /** - * Process and replace dynamic content - * * @throws Exception */ private function processDynamicContent(string $content): string { - - $processedContent = $content; - $crawler = new Crawler; $crawler->addHtmlContent($content, 'UTF-8'); - $toReplace = []; - $crawler->filter('script')->each(function (Crawler $node, $i) use (&$toReplace) { - if (str_contains($node->html(), self::ALEXANDRIA_URL_PATTERN)) { - $toReplace[] = $node->html(); + + $scripts = []; + $crawler->filter('script')->each(function (Crawler $node) use (&$scripts): void { + $html = $node->html(); + + if ($html !== null && str_contains($html, self::ALEXANDRIA_URL_PATTERN)) { + $scripts[] = $html; } }); - $alexandriaContent = ''; + if ($scripts === []) { + return $content; + } - foreach ($toReplace as $scriptTag) { - if (! preg_match('/https:\/\/robertsspaceindustries\.com\/alexandria\/html[^"\'\s]*/i', $scriptTag, $urlMatches)) { - app('Log')::warning( - "Failed to extract Alexandria URL from script tag in Comm-Link {$this->commLinkId}", - [ - 'script_tag' => substr($scriptTag, 0, 150).'...', - ] - ); + $alexandriaContent = ''; + $processedContent = $content; + foreach ($scripts as $scriptTag) { + if (! preg_match('/https:\/\/robertsspaceindustries\.com\/alexandria\/html[^"\'\s]*/i', $scriptTag, $matches)) { continue; } - $alexandriaUrl = $urlMatches[0]; - - try { - $client = new Client([ - 'timeout' => 30, - ]); - - $response = $client->get($alexandriaUrl); - - if ($response->getStatusCode() !== 200) { - throw new RuntimeException('Alexandria endpoint returned status code: '.$response->getStatusCode()); - } + $alexandriaUrl = $matches[0]; - $dynamicContent = $response->getBody()->getContents(); + $response = Http::timeout(30)->get($alexandriaUrl); - if (empty($dynamicContent)) { - throw new RuntimeException('Alexandria endpoint returned empty content'); - } - - $processedContent = str_replace($scriptTag, '', $processedContent); + if (! $response->successful()) { + throw new RuntimeException('Alexandria endpoint returned status code: '.$response->status()); + } - $alexandriaContent .= $dynamicContent; + $dynamicContent = $response->body(); - app('Log')::debug( - "Successfully replaced Alexandria content in Comm-Link {$this->commLinkId}", - [ - 'url' => $alexandriaUrl, - 'content_length' => strlen($dynamicContent), - ] - ); - } catch (GuzzleException $e) { - throw new RuntimeException("Failed to fetch dynamic content from {$alexandriaUrl}: {$e->getMessage()}"); + if ($dynamicContent === '') { + throw new RuntimeException('Alexandria endpoint returned empty content'); } - } - return preg_replace('/
.*?<\/div>/s', sprintf('
%s
', $alexandriaContent), $processedContent); - } + $processedContent = str_replace($scriptTag, '', $processedContent); + $alexandriaContent .= $dynamicContent; + } - /** - * @return string Path to Comm-Link File - */ - private function filePath(): string - { - return "{$this->commLinkId}/{$this->file}"; + return preg_replace( + '/
.*?<\/div>/s', + sprintf('
%s
', $alexandriaContent), + $processedContent + ) ?? $processedContent; } - /** - * Updates or Creates a Comm-Link Model and populates it. - */ private function createCommLink(): void { $data = $this->getCommLinkData(); - // Use the file time for new comm-links - $data['created_at'] = $data['created_at_file']; - - /** @var CommLink $commLink */ - $commLink = CommLink::updateOrCreate( - [ - 'cig_id' => $this->commLinkId, - ], + $data['created_at'] = $data['created_at_file'] ?? $data['created_at']; + + $commLink = CommLink::query()->updateOrCreate( + ['cig_id' => $this->commLinkId], $data ); - $this->addEnglishCommLinkTranslation($commLink); + $this->addEnglishTranslation($commLink); $this->syncImageIds($commLink); $this->syncLinkIds($commLink); + } - CommLinksChanged::create( - [ - 'comm_link_id' => $commLink->id, - 'had_content' => false, - 'type' => 'creation', - ] - ); + private function updateCommLink(CommLink $commLink): void + { + $data = $this->getCommLinkData(); + + if ($this->contentHasChanged($commLink)) { + $this->addEnglishTranslation($commLink); + } else { + unset($data['file']); + } + + $dateMetadata = Carbon::parse($data['created_at']); + $dateMetadataFile = Carbon::parse($data['created_at_file'] ?? $data['created_at']); + $dateMetadataFile->setSecond(0); + $dateMetadataFile->setMinute(0); + + if ($dateMetadata->diffInHours($dateMetadataFile) <= 24 || $data['created_at'] === Metadata::DEFAULT_CREATION_DATE) { + $data['created_at'] = $dateMetadataFile; + } + + $commLink->update($data); + $this->syncImageIds($commLink); + $this->syncLinkIds($commLink); } /** - * Creates the Comm-Link Dara Array from Metadata. + * @return array */ private function getCommLinkData(): array { @@ -291,9 +222,7 @@ private function getCommLinkData(): array $metaData->put( 'created_at_file', - $this->createTimestampFromFile( - $this->getFirstCommLinkFileName() - ) ?? Metadata::DEFAULT_CREATION_DATE + $this->createTimestampFromFile($this->getFirstCommLinkFileName()) ?? Metadata::DEFAULT_CREATION_DATE ); return [ @@ -309,111 +238,65 @@ private function getCommLinkData(): array ]; } - /** - * Adds or Updates the default english Translation to the Comm-Link. - */ - private function addEnglishCommLinkTranslation(CommLink $commLink): void + private function addEnglishTranslation(CommLink $commLink): void { $contentParser = new Content($this->crawler); - $commLink->translations()->updateOrCreate( - [ - 'locale_code' => Language::ENGLISH, - ], - [ - 'translation' => $contentParser->getContent(), - 'proofread' => true, - ] - ); + + $content = $contentParser->getContent(); + + if ($content !== null && $content !== '') { + $commLink->setTranslation('translation', Language::ENGLISH, $content); + $commLink->save(); + } } - /** - * Syncs extracted Comm-Link Image Ids. - */ private function syncImageIds(CommLink $commLink): void { $imageParser = new Image($this->crawler); $commLink->images()->sync($imageParser->getImageIds()); } - /** - * Syncs extracted Comm-Link Link Ids. - */ private function syncLinkIds(CommLink $commLink): void { $linkParser = new Link($this->crawler); $commLink->links()->sync($linkParser->getLinkIds()); } - /** - * Checks if Content of current Comm-Link has Changed - * Updates Metadata. - */ - private function checkCommLinkForChanges(): void + private function contentHasChanged(CommLink $commLink): bool { - $data = $this->getCommLinkData(); - - if ($this->contentHasChanged()) { - $hadContent = true; - if (optional($this->commLinkModel->english())->translation === null) { - $hadContent = false; - } else { - // Don't update the current File if Content has Changed and Translation is not null - unset($data['file']); - } - - $this->addEnglishCommLinkTranslation($this->commLinkModel); + $contentParser = new Content($this->crawler); + $currentTranslation = $commLink->getTranslation('translation', Language::ENGLISH, false); - CommLinksChanged::create( - [ - 'comm_link_id' => $this->commLinkModel->id, - 'had_content' => $hadContent, - 'type' => 'update', - ] - ); - } + return $contentParser->getContent() !== ($currentTranslation ?? ''); + } - $dateMetadata = Carbon::parse($data['created_at']); - $dateMetadataFile = Carbon::parse($data['created_at_file']); - $dateMetadataFile->setSecond(0); - $dateMetadataFile->setMinutes(0); + private function getFirstCommLinkFileName(): ?string + { + $files = Storage::disk('comm_links')->allFiles((string) $this->commLinkId); - if ($dateMetadata->diffInHours($dateMetadataFile) <= 24 || $data['created_at'] === Metadata::DEFAULT_CREATION_DATE) { - $data['created_at'] = $dateMetadataFile; + if ($files === []) { + return null; } - $this->commLinkModel->update($data); - $this->syncImageIds($this->commLinkModel); - $this->syncLinkIds($this->commLinkModel); - } - - /** - * Checks if Local Content is Equal to DB Content. - */ - private function contentHasChanged(): bool - { - $contentParser = new Content($this->crawler); + sort($files); + $filename = Arr::first($files); - return $contentParser->getContent() !== (optional($this->commLinkModel->english())->translation ?? ''); + return $filename === null + ? null + : str_replace(sprintf('%d/', $this->commLinkId), '', $filename); } - /** - * Returns the first file in a comm-link folder or null - */ - private function getFirstCommLinkFileName(): ?string + private function createTimestampFromFile(?string $file): ?string { - $filename = Arr::first(Storage::disk(DownloadCommLink::DISK)->allFiles($this->commLinkId)); + if ($file === null) { + return null; + } - return str_replace(sprintf('%d/', $this->commLinkId), '', $filename); - } + $base = str_replace('.html', '', $file); - /** - * Creates a timestamp from a comm-link filename - */ - private function createTimestampFromFile(string $file): ?string - { try { - return Carbon::createFromFormat('Y-m-d_His\.\h\t\m\l', $file)->format('Y-m-d H:i:s'); - } catch (InvalidFormatException $e) { + return Carbon::createFromFormat('Y-m-d_His', $base)->format('Y-m-d H:i:s'); + } catch (InvalidFormatException $exception) { return null; } } diff --git a/app/Jobs/Rsi/CommLink/Import/ImportCommLinks.php b/app/Jobs/Rsi/CommLink/Import/ImportCommLinks.php index e798ee936..a63248f28 100644 --- a/app/Jobs/Rsi/CommLink/Import/ImportCommLinks.php +++ b/app/Jobs/Rsi/CommLink/Import/ImportCommLinks.php @@ -4,101 +4,105 @@ namespace App\Jobs\Rsi\CommLink\Import; -use App\Jobs\Rsi\CommLink\Image\CreateImageHashes; use App\Jobs\Rsi\CommLink\Image\CreateImageMetadata; -use App\Jobs\Rsi\CommLink\Translate\TranslateCommLinks; -use App\Jobs\Wiki\CommLink\CreateCommLinkWikiPages; -use App\Models\Rsi\CommLink\CommLink; -use App\Traits\Jobs\GetFoldersTrait; -use Illuminate\Bus\Queueable; +use App\Jobs\Rsi\CommLink\Image\DispatchImageHashes; +use Carbon\Carbon; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Arr; -use Illuminate\Support\Facades\Artisan; +use Illuminate\Foundation\Queue\Queueable; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; -/** - * Dispatches a ParseCommLink Job for the newest file in every Comm-Link Folder. - */ class ImportCommLinks implements ShouldQueue { - use Dispatchable; - use GetFoldersTrait; - use InteractsWithQueue; use Queueable; - use SerializesModels; - /** - * @var int Offset to start parsing from - */ - private int $modifiedFolderTime; + public int $timeout = 120; - /** - * Create a new job instance. - * - * @param int $modifiedFolderTime Include folders that were created in the last x minutes. -1 = all - */ - public function __construct(int $modifiedFolderTime = 5) - { - $this->modifiedFolderTime = $modifiedFolderTime; - } + public function __construct(public readonly int $modifiedFolderTime = 5) {} - /** - * Import Comm-Links that where created in the last modifiedFolderTime minutes - * Also create Metadata, Image Hashes, Translations and Wiki Pages for each imported Comm-Link - */ public function handle(): void { - $commLinks = CommLink::query()->get(); - $commLinks = $commLinks->keyBy('cig_id'); + $directories = $this->filterDirectories('comm_links', $this->modifiedFolderTime); - $newCommLinkIds = $this->filterDirectories('comm_links', $this->modifiedFolderTime) - ->each( - function ($commLinkDir) use ($commLinks) { - $file = Arr::last(Storage::disk('comm_links')->files($commLinkDir)); + if ($directories->isEmpty()) { + return; + } - if ($file !== null) { - $file = preg_split('/\/|\\\/', $file); - $commLink = $commLinks->get((int) $commLinkDir, null); + $jobs = []; + $commLinkIds = []; - dispatch(new ImportCommLink((int) $commLinkDir, Arr::last($file), $commLink)); - } - } - ) - ->map( - function ($directory) { - return (int) $directory; + foreach ($directories as $directory) { + $file = $this->latestFile($directory); + + if ($file === null) { + continue; + } + + $commLinkId = (int) $directory; + $jobs[] = new ImportCommLink($commLinkId, $file); + $commLinkIds[] = $commLinkId; + } + + if ($jobs === []) { + return; + } + + Bus::batch($jobs) + ->name('Comm-Link import') + ->allowFailures() + ->then(function () use ($commLinkIds): void { + if ($commLinkIds === []) { + return; } - ) - ->toArray(); - $this->dispatchChain($newCommLinkIds); + CreateImageMetadata::dispatch($commLinkIds); + DispatchImageHashes::dispatch($commLinkIds); + }) + ->dispatch(); } - /** - * Create Metadata, Image Hashes, Translations and Wiki Pages - */ - private function dispatchChain(array $commLinkIds): void + private function latestFile(string $directory): ?string { - CreateImageMetadata::withChain( - [ - new CreateImageHashes($commLinkIds), - ] - )->dispatch($commLinkIds); - - if (config('services.deepl.auth_key', null) !== null) { - dispatch(new TranslateCommLinks($commLinkIds)); + $files = Storage::disk('comm_links')->files($directory); + + if ($files === []) { + return null; } - $clientNotNull = config('services.mediawiki.client_id') !== null; - $apiUrlNotNull = config('mediawiki.api_url') !== null; + sort($files); + $file = end($files); - if ($clientNotNull && $apiUrlNotNull) { - dispatch(new CreateCommLinkWikiPages)->delay(90); + if ($file === false) { + return null; } - Artisan::call('comm-links:compute-similar-image-ids --recent'); + return Str::afterLast($file, '/'); + } + + /** + * Filter folders that where created in the last X minutes on a given disk + * + * @param string $disk The disk name to filter + * @param int $findTimeMinutes Include directories created in the last X minutes or all if -1 + */ + private function filterDirectories(string $disk, int $findTimeMinutes): Collection + { + $now = Carbon::now()->subMinutes($findTimeMinutes); + + return collect(Storage::disk($disk)->directories()) + ->filter( + function (string $dir) use ($disk, $now, $findTimeMinutes) { + $mTime = Carbon::createFromTimestamp(File::lastModified(Storage::disk($disk)->path($dir))); + + if ($findTimeMinutes === -1) { + return true; + } + + return $mTime->greaterThanOrEqualTo($now); + } + ); } } diff --git a/app/Jobs/Rsi/CommLink/SyncImageId.php b/app/Jobs/Rsi/CommLink/SyncImageId.php deleted file mode 100644 index 9d56d98eb..000000000 --- a/app/Jobs/Rsi/CommLink/SyncImageId.php +++ /dev/null @@ -1,81 +0,0 @@ -commLink = $commLink; - } - - /** - * Execute the job. - * - * @throws FileNotFoundException - */ - public function handle(): void - { - app('Log')::info( - "Syncing Image ids for Comm-Link {$this->commLink->cig_id}", - [ - 'id' => $this->commLink->cig_id, - 'file' => $this->commLink->file, - ] - ); - - $content = Storage::disk('comm_links')->get("{$this->commLink->cig_id}/{$this->commLink->file}"); - if ($content === null) { - throw new FileNotFoundException; - } - - $this->crawler = new Crawler; - $this->crawler->addHtmlContent($content, 'UTF-8'); - - $post = $this->crawler->filter(ImportCommLink::POST_SELECTOR); - $subscribers = $this->crawler->filter(ImportCommLink::SUBSCRIBERS_SELECTOR); - - if ($post->count() === 0 && $subscribers->count() === 0) { - app('Log')::info("Comm-Link with id {$this->commLink->cig_id} has no content"); - - return; - } - - $this->syncImages(); - } - - /** - * Syncs extracted Comm-Link Image Ids. - */ - private function syncImages(): void - { - $imageParser = new Image($this->crawler); - $this->commLink->images()->sync($imageParser->getImageIds()); - } -} diff --git a/app/Jobs/Rsi/CommLink/SyncImageIds.php b/app/Jobs/Rsi/CommLink/SyncImageIds.php deleted file mode 100644 index 7a4093e38..000000000 --- a/app/Jobs/Rsi/CommLink/SyncImageIds.php +++ /dev/null @@ -1,54 +0,0 @@ -offset = $offset; - } - - /** - * Execute the job. - */ - public function handle(): void - { - $commLinks = CommLink::query()->where('cig_id', '>=', $this->offset)->get(); - - $commLinks->each( - function ($commLink) { - if (! Storage::disk('comm_links')->exists((string) $commLink->cig_id)) { - return; - } - - dispatch(new SyncImageId($commLink)); - } - ); - } -} diff --git a/app/Jobs/Rsi/CommLink/Translate/TranslateCommLink.php b/app/Jobs/Rsi/CommLink/Translate/TranslateCommLink.php deleted file mode 100644 index 4a9eaf24c..000000000 --- a/app/Jobs/Rsi/CommLink/Translate/TranslateCommLink.php +++ /dev/null @@ -1,106 +0,0 @@ -commLink = $commLink; - } - - /** - * Execute the job. - */ - public function handle(): void - { - app('Log')::info('Translating Comm-Link with ID {$this->commLink->cig_id}'); - $targetLocale = config('services.deepl.target_locale'); - - if (optional($this->commLink->german())->translation !== null) { - $this->delete(); - - return; - } - - $english = $this->commLink->english()->translation; - $formality = 'less'; - - if (in_array($this->commLink->category->name, $this->formalCategories, true)) { - $formality = 'more'; - } - - $translator = new TranslateText($english); - - // phpcs:disable - try { - $translation = $translator->translate(config('services.deepl.target_locale'), $formality); - } catch ( - QuotaException| - CallException| - AuthenticationException| - InvalidArgumentException| - TextLengthException $e - ) { - $this->fail($e); - - return; - } catch (ConnectException|RateLimitedException $e) { - $this->release(60); - - return; - } - // phpcs:enable - - $this->commLink->translations()->updateOrCreate( - [ - 'locale_code' => sprintf('%s_%s', Str::lower($targetLocale), $targetLocale), - ], - [ - 'translation' => trim(TranslateText::runTextReplacements($translation)), - 'proofread' => false, - ] - ); - } -} diff --git a/app/Jobs/Rsi/CommLink/Translate/TranslateCommLinks.php b/app/Jobs/Rsi/CommLink/Translate/TranslateCommLinks.php index a79c6e92d..8ccf93998 100644 --- a/app/Jobs/Rsi/CommLink/Translate/TranslateCommLinks.php +++ b/app/Jobs/Rsi/CommLink/Translate/TranslateCommLinks.php @@ -4,88 +4,102 @@ namespace App\Jobs\Rsi\CommLink\Translate; +use App\Exceptions\Translation\AuthenticationException; +use App\Exceptions\Translation\QuotaExceededException; +use App\Exceptions\Translation\RateLimitException; +use App\Exceptions\Translation\TranslationException; use App\Models\Rsi\CommLink\CommLink; use App\Models\System\Language; -use App\Traits\Jobs\GetCommLinkWikiPageInfoTrait as GetCommLinkWikiPageInfo; -use App\Traits\LoginWikiBotAccountTrait as LoginWikiBotAccount; +use App\Services\Translation\TranslationService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use RuntimeException; +use Illuminate\Support\Collection; /** - * Translate new Comm-Links + * Translate all Comm-Links without German translation */ class TranslateCommLinks implements ShouldQueue { use Dispatchable; - use GetCommLinkWikiPageInfo; use InteractsWithQueue; - use LoginWikiBotAccount; use Queueable; use SerializesModels; /** - * @var int Comm-Link IDs to operate on + * Categories that should be translated with more formal German */ - private $commLinkIds; - - /** - * Create a new job instance. - */ - public function __construct(array $commLinkIds = []) - { - $this->commLinkIds = $commLinkIds; - } + private array $formalCategories = ['Lore', 'Short Stories']; /** * Execute the job. */ - public function handle(): void + public function handle(TranslationService $translator): void { - app('Log')::info('Starting Comm-Link Translations'); + app('Log')::info('Translating Comm-Links'); - $this->loginWikiBotAccount('services.wiki_translations'); + $targetLocale = config('services.deepl.target_locale', 'de'); - CommLink::query()->whereHas( - 'translations', - function (Builder $query) { - $query->where('locale_code', Language::ENGLISH)->whereRaw("translation <> ''"); - } - ) - ->whereIn('cig_id', $this->commLinkIds) + CommLink::query() + ->with(['category']) + ->whereNotNull('translation') ->chunk( - 100, - function (Collection $commLinks) { - try { - $pageInfoCollection = $this->getPageInfoForCommLinks($commLinks, true); - } catch (RuntimeException $e) { - app('Log')::error($e->getMessage()); - - if (str_contains($e->getMessage(), 'Guru Meditation')) { - $this->release(60); - } else { - $this->fail($e); - } + 25, + function (Collection $commLinks) use ($translator, $targetLocale) { + $commLinks->each( + function (CommLink $commLink) use ($translator, $targetLocale) { + $english = $commLink->getTranslation('translation', Language::ENGLISH, false); + $german = $commLink->getTranslation('translation', Language::GERMAN, false); - return; - } + if ($english === null || $english === '') { + return; + } - $commLinks->each( - function (CommLink $commLink) use ($pageInfoCollection) { - $wikiPage = $pageInfoCollection->get($commLink->cig_id, []); + if ($german !== null && $german !== '') { + return; + } - if (isset($wikiPage['missing'])) { - dispatch(new TranslateCommLink($commLink)); + $formality = 'less'; + if ($commLink->category !== null && in_array($commLink->category->name, $this->formalCategories, true)) { + $formality = 'more'; } + + try { + app('Log')::info(sprintf('Translating Comm-Link %d', $commLink->cig_id)); + $translation = $translator->translate($english, $targetLocale, 'en', $formality); + } catch (QuotaExceededException $e) { + app('Log')::warning('DeepL quota exceeded'); + + $this->fail($e); + + return; + } catch (RateLimitException $e) { + app('Log')::info('Got rate limit exception. Trying job again in 60 seconds.'); + + $this->release(60); + + return; + } catch (AuthenticationException $e) { + app('Log')::error('DeepL authentication failed', ['error' => $e->getMessage()]); + + $this->fail($e); + + return; + } catch (TranslationException $e) { + app('Log')::warning('Translation failed', [ + 'comm_link_id' => $commLink->cig_id, + 'error' => $e->getMessage(), + ]); + + return; + } + + $commLink->setTranslation('translation', Language::GERMAN, $translation); + $commLink->save(); } ); - - usleep(100000); } ); } diff --git a/app/Jobs/SC/ComputeItemBaseIds.php b/app/Jobs/SC/ComputeItemBaseIds.php deleted file mode 100644 index 4e8d93604..000000000 --- a/app/Jobs/SC/ComputeItemBaseIds.php +++ /dev/null @@ -1,67 +0,0 @@ -chunk(250, function (Collection $items) { - $items->each(function (Item $item) { - // No '01,02, etc.' found, we currently assume that this means no base variants - if (preg_match('/[a-z-_]+_0\d/i', $item->class_name) === false) { - return; - } - - $idEnd = strpos($item->class_name, '0'); - $class = substr($item->class_name, 0, $idEnd + 2); - $baseClassChecks = [ - $class, - $class.'_01', - $class.'_01_01', - ]; - - $baseModel = Item::query() - ->where('uuid', '<>', $item->uuid) - ->where('type', $item->type); - - // Special backpack handling, they differ in their "set" ids (we assume) but share the same set - if ($item->type === 'Char_Armor_Backpack') { - $baseClass = substr($item->class_name, 0, $idEnd - 1); - - if (str_ends_with($item->name, 'Backpack') && ! str_contains($item->name, '"Expo"')) { - $baseClass = '<>'; // Don't match anything - } - - $baseModel - ->where('class_name', 'LIKE', $baseClass.'%') - ->orderBy('name'); - } else { - $baseModel->whereIn('class_name', $baseClassChecks); - } - - $baseModel = $baseModel->first(); - - $item->update([ - 'base_id' => $baseModel?->id, - ]); - }); - }); - } -} diff --git a/app/Jobs/SC/Import/AbstractItemCreationJob.php b/app/Jobs/SC/Import/AbstractItemCreationJob.php deleted file mode 100644 index 1a8d39cf5..000000000 --- a/app/Jobs/SC/Import/AbstractItemCreationJob.php +++ /dev/null @@ -1,18 +0,0 @@ -labels = new Labels; - } -} diff --git a/app/Jobs/SC/Import/Ammunition.php b/app/Jobs/SC/Import/Ammunition.php deleted file mode 100644 index 3c33e197a..000000000 --- a/app/Jobs/SC/Import/Ammunition.php +++ /dev/null @@ -1,70 +0,0 @@ -withoutGlobalScopes()->updateOrCreate([ - 'uuid' => $this->data['ammunition']['uuid'], - ], [ - 'size' => $this->data['ammunition']['size'], - 'lifetime' => $this->data['ammunition']['lifetime'], - 'speed' => $this->data['ammunition']['speed'], - 'range' => $this->data['ammunition']['range'], - ]); - - collect($this->data['ammunition']['damages'])->each(function ($damageClass) use ($ammunition) { - collect($damageClass)->each(function ($damage) use ($ammunition) { - $ammunition->damages()->updateOrCreate([ - 'type' => $damage['type'], - 'name' => $damage['name'], - ], [ - 'damage' => $damage['damage'], - ]); - }); - }); - - $ammunition->piercability()->updateOrCreate([ - 'ammunition_uuid' => $this->data['ammunition']['uuid'], - ], [ - 'damage_falloff_level_1' => $this->data['ammunition']['piercability']['damage_falloff_level_1'] ?? 0, - 'damage_falloff_level_2' => $this->data['ammunition']['piercability']['damage_falloff_level_2'] ?? 0, - 'damage_falloff_level_3' => $this->data['ammunition']['piercability']['damage_falloff_level_3'] ?? 0, - 'max_penetration_thickness' => $this->data['ammunition']['piercability']['max_penetration_thickness'] ?? 0, - ]); - - collect($this->data['ammunition']['damage_falloffs'])->each(function ($falloff, $key) use ($ammunition) { - $ammunition->damageFalloffs()->updateOrCreate([ - 'type' => $key, - ], [ - 'physical' => $falloff['physical'] ?? 0, - 'energy' => $falloff['energy'] ?? 0, - 'distortion' => $falloff['distortion'] ?? 0, - 'thermal' => $falloff['thermal'] ?? 0, - 'biochemical' => $falloff['biochemical'] ?? 0, - 'stun' => $falloff['stun'] ?? 0, - ]); - }); - } -} diff --git a/app/Jobs/SC/Import/Clothing.php b/app/Jobs/SC/Import/Clothing.php deleted file mode 100644 index 92b22cd32..000000000 --- a/app/Jobs/SC/Import/Clothing.php +++ /dev/null @@ -1,74 +0,0 @@ -loadLabels(); - - try { - $parser = new \App\Services\Parser\SC\Clothing($this->filePath, $this->labels); - } catch (FileNotFoundException|JsonException $e) { - $this->fail($e); - - return; - } - - $item = $parser->getData(); - - try { - /** @var \App\Models\SC\Char\Clothing\Clothing $model */ - $model = \App\Models\SC\Char\Clothing\Clothing::query()->withoutGlobalScopes()->where('uuid', $item['uuid'])->firstOrFail(); - } catch (ModelNotFoundException $e) { - return; - } - - if (isset($item['resistances'])) { - if (! empty($item['damage_reduction'])) { - $model->resistances()->updateOrCreate([ - 'type' => 'damage_reduction', - ], [ - 'multiplier' => str_replace('%', '', $item['damage_reduction']) / 100, - ]); - } - - foreach ($item['resistances'] as $type => $resistance) { - $model->resistances()->updateOrCreate([ - 'type' => $type, - ], [ - 'multiplier' => $resistance['multiplier'] ?? null, - 'threshold' => $resistance['threshold'] ?? null, - ]); - } - } - - if (!empty($item['radiation_resistance'])) { - $model->radiationResistance()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'maximum_radiation_capacity' => $item['radiation_resistance']['maximum_radiation_capacity'], - 'radiation_dissipation_rate' => $item['radiation_resistance']['radiation_dissipation_rate'], - ]); - } - } -} diff --git a/app/Jobs/SC/Import/Food.php b/app/Jobs/SC/Import/Food.php deleted file mode 100644 index fa9ede75f..000000000 --- a/app/Jobs/SC/Import/Food.php +++ /dev/null @@ -1,57 +0,0 @@ -loadLabels(); - try { - $parser = new \App\Services\Parser\SC\Food($this->filePath, $this->labels); - } catch (FileNotFoundException|JsonException $e) { - $this->fail($e); - - return; - } - $item = $parser->getData(); - - /** @var \App\Models\SC\Food\Food $model */ - $model = \App\Models\SC\Food\Food::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'nutritional_density_rating' => $item['nutritional_density_rating'] ?? null, - 'hydration_efficacy_index' => $item['hydration_efficacy_index'] ?? null, - 'container_type' => $item['container_type'] ?? null, - 'one_shot_consume' => $item['one_shot_consume'] ?? null, - 'can_be_reclosed' => $item['can_be_reclosed'] ?? null, - 'discard_when_consumed' => $item['discard_when_consumed'] ?? null, - ]); - - $ids = collect($item['effects'])->map(function (string $effect) { - return FoodEffect::firstOrCreate([ - 'name' => $effect, - ])->id; - }); - - $model->effects()->sync($ids); - } -} diff --git a/app/Jobs/SC/Import/Grenade.php b/app/Jobs/SC/Import/Grenade.php deleted file mode 100644 index 4b29c27c5..000000000 --- a/app/Jobs/SC/Import/Grenade.php +++ /dev/null @@ -1,45 +0,0 @@ -loadLabels(); - try { - $parser = new \App\Services\Parser\SC\Grenade($this->filePath, $this->labels); - } catch (FileNotFoundException|JsonException $e) { - $this->fail($e); - - return; - } - $item = $parser->getData(); - - /** @var \App\Models\SC\Char\Grenade $model */ - \App\Models\SC\Char\Grenade::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'aoe' => $item['aoe'] ?? null, - 'damage_type' => $item['damage_type'] ?? null, - 'damage' => $item['damage'] ?? null, - ]); - } -} diff --git a/app/Jobs/SC/Import/HackingChip.php b/app/Jobs/SC/Import/HackingChip.php deleted file mode 100644 index cea5900e9..000000000 --- a/app/Jobs/SC/Import/HackingChip.php +++ /dev/null @@ -1,45 +0,0 @@ -loadLabels(); - try { - $parser = new \App\Services\Parser\SC\HackingChip($this->filePath, $this->labels); - } catch (FileNotFoundException|JsonException $e) { - $this->fail($e); - - return; - } - $item = $parser->getData(); - - /** @var \App\Models\SC\ItemSpecification\HackingChip $model */ - \App\Models\SC\ItemSpecification\HackingChip::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'max_charges' => $item['max_charges'] ?? null, - 'duration_multiplier' => $item['duration_multiplier'] ?? null, - 'error_chance' => $item['error_chance'] ?? null, - ]); - } -} diff --git a/app/Jobs/SC/Import/Item.php b/app/Jobs/SC/Import/Item.php deleted file mode 100644 index 8498e26ce..000000000 --- a/app/Jobs/SC/Import/Item.php +++ /dev/null @@ -1,352 +0,0 @@ -data = $data; - } - - /** - * Execute the job. - */ - public function handle(): void - { - $manufacturer = ManufacturerFixer::getByName($this->data['manufacturer']['name']); - if ($manufacturer !== null) { - $this->data['manufacturer']['name'] = $manufacturer['name_fix'] ?? $manufacturer['name']; - $this->data['manufacturer']['code'] = $manufacturer['code']; - } - - $manufacturer = Manufacturer::updateOrCreate([ - 'uuid' => $this->data['manufacturer']['uuid'], - ], [ - 'name' => $this->data['manufacturer']['name'], - 'code' => $this->data['manufacturer']['code'], - ]); - - /** @var \App\Models\SC\Item\Item $itemModel */ - $itemModel = \App\Models\SC\Item\Item::query()->withoutGlobalScopes()->updateOrCreate([ - 'uuid' => $this->data['uuid'], - ], [ - 'name' => $this->data['name'], - 'type' => $this->data['type'], - 'sub_type' => $this->data['sub_type'], - 'manufacturer_description' => $this->data['manufacturer_description'], - 'size' => $this->data['size'], - 'class_name' => $this->data['class_name'], - 'mass' => $this->data['mass'], - 'version' => config('api.sc_data_version'), - 'manufacturer_id' => $manufacturer->id, - ]); - - if (! empty($this->data['description'])) { - $itemModel->translations()->updateOrCreate([ - 'locale_code' => Language::ENGLISH, - ], [ - 'translation' => $this->data['description'], - ]); - } - - if (! empty($this->data['description_zh'])) { - $itemModel->translations()->updateOrCreate([ - 'locale_code' => Language::CHINESE, - ], [ - 'translation' => $this->data['description_zh'], - ]); - } - - $data = collect($this->data['description_data'] ?? [])->filter(function ($value, $key) { - return $key !== 'description'; - })->each(function ($value, $key) use ($itemModel) { - $itemModel->descriptionData()->updateOrCreate([ - 'name' => trim($key), - ], [ - 'value' => trim($value), - ]); - })->keys(); - $itemModel->descriptionData()->whereNotIn('name', $data)->delete(); - - $this->createDimensionModel($itemModel); - $this->createContainerModel($itemModel); - $this->createPorts($itemModel); - $this->createPowerModel($itemModel); - $this->createHeatModel($itemModel); - $this->createDistortionModel($itemModel); - $this->createDurabilityModel($itemModel); - $this->addTags($itemModel, $this->data, 'tags'); - $this->addTags($itemModel, $this->data, 'required_tags', true); - $this->addInteractions($itemModel, $this->data); - $this->addEntityTags($itemModel, $this->data); - } - - private function createDimensionModel(\App\Models\SC\Item\Item $itemModel): void - { - $itemModel->dimensions()->updateOrCreate([ - 'item_uuid' => $this->data['uuid'], - 'override' => 0, - ], [ - 'width' => $this->data['dimension']['width'], - 'height' => $this->data['dimension']['height'], - 'length' => $this->data['dimension']['length'], - - 'volume' => $this->data['volume'], - ]); - - if ($this->data['dimension_override']['width'] !== null) { - $itemModel->dimensions()->updateOrCreate([ - 'item_uuid' => $this->data['uuid'], - 'override' => 1, - ], [ - 'width' => $this->data['dimension_override']['width'], - 'height' => $this->data['dimension_override']['height'], - 'length' => $this->data['dimension_override']['length'], - ]); - } - } - - private function createContainerModel(\App\Models\SC\Item\Item $itemModel): void - { - if ($this->data['inventory_container']['scu'] !== null) { - $itemModel->container()->updateOrCreate([ - 'item_uuid' => $this->data['uuid'], - ], [ - 'width' => $this->data['inventory_container']['width'], - 'height' => $this->data['inventory_container']['height'], - 'length' => $this->data['inventory_container']['length'], - 'scu' => $this->data['inventory_container']['scu'], - 'unit' => $this->data['inventory_container']['unit'], - ]); - } - } - - private function createPorts(\App\Models\SC\Item\Item $itemModel): void - { - if (! empty($this->data['ports'])) { - $availablePorts = collect($this->data['ports'])->each(function (array $port) use ($itemModel) { - /** @var ItemPort $port */ - $portModel = $itemModel->ports()->updateOrCreate([ - 'name' => $port['name'], - ], [ - 'display_name' => $port['display_name'], - 'equipped_item_uuid' => $port['equipped_item_uuid'], - 'min_size' => $port['min_size'], - 'max_size' => $port['max_size'], - 'position' => $port['position'], - ]); - - $this->addTags($portModel, $port, 'tags'); - $this->addTags($portModel, $port, 'required_tags', true); - - $types = collect($port['compatible_types']) - ->map(function (array $type) { - /** @var ItemType $typeModel */ - $typeModel = ItemType::query()->firstOrCreate([ - 'type' => $type['type'], - ]); - - $type['id'] = $typeModel->id; - - return $type; - }) - ->each(function (array $type) use ($portModel) { - /** @var ItemPort $portModel */ - $portModelType = $portModel->compatibleTypes()->updateOrCreate([ - 'item_type_id' => $type['id'], - ]); - - $subTypes = collect($type['sub_types']) - ->map(function (string $subType) { - /** @var ItemSubType $typeModel */ - $typeModel = ItemSubType::query()->firstOrCreate([ - 'sub_type' => $subType, - ]); - - return $typeModel->id; - }) - ->each(function (int $id) use ($portModelType) { - /** @var ItemPortType $portModelType */ - $portModelType->subTypes()->updateOrCreate([ - 'sub_type_id' => $id, - ]); - }); - - /** @var ItemPortType $portModelType */ - $portModelType->subTypes()->whereNotIn('sub_type_id', $subTypes)->delete(); - }) - ->pluck('id'); - - /** @var ItemPort $portModel */ - $portModel->compatibleTypes()->whereNotIn('item_type_id', $types)->delete(); - }) - ->pluck('name'); - - // Remove old ports - $itemModel->ports()->whereNotIn('name', $availablePorts)->delete(); - } - } - - private function createPowerModel(\App\Models\SC\Item\Item $itemModel): void - { - if (! empty($this->data['power'])) { - $itemModel->powerData()->updateOrCreate([ - 'item_uuid' => $this->data['uuid'], - ], [ - 'power_base' => $this->data['power']['power_base'] ?? null, - 'power_draw' => $this->data['power']['power_draw'] ?? null, - 'throttleable' => $this->data['power']['throttleable'] ?? null, - 'overclockable' => $this->data['power']['overclockable'] ?? null, - 'overclock_threshold_min' => $this->data['power']['overclock_threshold_min'] ?? null, - 'overclock_threshold_max' => $this->data['power']['overclock_threshold_max'] ?? null, - 'overclock_performance' => $this->data['power']['overclock_performance'] ?? null, - 'overpower_performance' => $this->data['power']['overpower_performance'] ?? null, - 'power_to_em' => $this->data['power']['power_to_em'] ?? null, - 'decay_rate_em' => $this->data['power']['decay_rate_em'] ?? null, - ]); - } - } - - private function createHeatModel(\App\Models\SC\Item\Item $itemModel): void - { - if (! empty($this->data['heat'])) { - $itemModel->heatData()->updateOrCreate([ - 'item_uuid' => $this->data['uuid'], - ], [ - 'temperature_to_ir' => $this->data['heat']['temperature_to_ir'] ?? null, - 'ir_temperature_threshold' => $this->data['heat']['ir_temperature_threshold'] ?? null, - 'overpower_heat' => $this->data['heat']['overpower_heat'] ?? null, - 'overclock_threshold_min' => $this->data['heat']['overclock_threshold_min'] ?? null, - 'overclock_threshold_max' => $this->data['heat']['overclock_threshold_max'] ?? null, - 'thermal_energy_base' => $this->data['heat']['thermal_energy_base'] ?? null, - 'thermal_energy_draw' => $this->data['heat']['thermal_energy_draw'] ?? null, - 'thermal_conductivity' => $this->data['heat']['thermal_conductivity'] ?? null, - 'specific_heat_capacity' => $this->data['heat']['specific_heat_capacity'] ?? null, - 'mass' => $this->data['heat']['mass'] ?? null, - 'surface_area' => $this->data['heat']['surface_area'] ?? null, - 'start_cooling_temperature' => $this->data['heat']['start_cooling_temperature'] ?? null, - 'max_cooling_rate' => $this->data['heat']['max_cooling_rate'] ?? null, - 'max_temperature' => $this->data['heat']['max_temperature'] ?? null, - 'min_temperature' => $this->data['heat']['min_temperature'] ?? null, - 'overheat_temperature' => $this->data['heat']['overheat_temperature'] ?? null, - 'recovery_temperature' => $this->data['heat']['recovery_temperature'] ?? null, - 'misfire_min_temperature' => $this->data['heat']['misfire_min_temperature'] ?? null, - 'misfire_max_temperature' => $this->data['heat']['misfire_max_temperature'] ?? null, - ]); - } - } - - private function createDistortionModel(\App\Models\SC\Item\Item $itemModel): void - { - if (! empty($this->data['distortion'])) { - $itemModel->distortionData()->updateOrCreate([ - 'item_uuid' => $this->data['uuid'], - ], [ - 'decay_delay' => $this->data['distortion']['decay_delay'] ?? null, - 'decay_rate' => $this->data['distortion']['decay_rate'] ?? null, - 'maximum' => $this->data['distortion']['maximum'] ?? null, - 'warning_ratio' => $this->data['distortion']['warning_ratio'] ?? null, - 'overload_ratio' => $this->data['distortion']['overload_ratio'] ?? null, - 'recovery_ratio' => $this->data['distortion']['recovery_ratio'] ?? null, - 'recovery_time' => $this->data['distortion']['recovery_time'] ?? null, - ]); - } - } - - private function createDurabilityModel(\App\Models\SC\Item\Item $itemModel): void - { - if (! empty($this->data['durability'])) { - $itemModel->durabilityData()->updateOrCreate([ - 'item_uuid' => $this->data['uuid'], - ], [ - 'health' => $this->data['durability']['health'] ?? null, - 'max_lifetime' => $this->data['durability']['lifetime'] ?? null, - 'salvageable' => $this->data['durability']['salvageable'] ?? null, - 'repairable' => $this->data['durability']['repairable'] ?? null, - ]); - } - } - - private function addTags($model, $data, string $key, bool $isRequiredTag = false): void - { - if (empty($data[$key])) { - return; - } - - $tags = collect(explode(' ', $data[$key])) - ->map('trim') - ->map('strtolower') - ->map(function ($tag) { - $tag = Tag::query()->firstOrCreate([ - 'name' => $tag, - ]); - - return $tag->id; - }); - - $model->tags()->syncWithPivotValues($tags, ['is_required_tag' => $isRequiredTag]); - } - - private function addInteractions($model, $data): void - { - if (empty($data['interactions'])) { - return; - } - - $interactions = collect($data['interactions']) - ->map(function ($interaction) { - $interaction = Interaction::query()->firstOrCreate([ - 'name' => $interaction, - ]); - - return $interaction->id; - }); - - $model->interactions()->sync($interactions); - } - - private function addEntityTags(\App\Models\SC\Item\Item $model, $data): void - { - if (empty($data['entity_tags'])) { - return; - } - - $tags = collect($data['entity_tags']) - ->map('trim') - ->map(function ($tag) { - $tag = EntityTag::query()->firstOrCreate([ - 'tag' => $tag, - ]); - - return $tag->id; - }); - - $model->entityTags()->sync($tags); - } -} diff --git a/app/Jobs/SC/Import/ItemSpecificationCreator.php b/app/Jobs/SC/Import/ItemSpecificationCreator.php deleted file mode 100644 index 18a21bbfd..000000000 --- a/app/Jobs/SC/Import/ItemSpecificationCreator.php +++ /dev/null @@ -1,97 +0,0 @@ - Grenade::dispatch($filePath), - 'Knife' => Knife::dispatch($filePath), - default => PersonalWeapon::dispatch($filePath), - }; - break; - case $type === 'WeaponAttachment': - WeaponAttachment::dispatch($filePath); - WeaponModifier::dispatch($filePath); - break; - - case $subType === 'Hacking': - HackingChip::dispatch($filePath); - break; - - // Mining - case stripos($type, 'WeaponMining') !== false: - MiningLaser::dispatch($filePath); - break; - - // Vehicle Items - case stripos($type, 'Arm') !== false: - case stripos($type, 'Armor') !== false: - case stripos($type, 'Battery') !== false: - case stripos($type, 'Bomb') !== false: - case stripos($type, 'Cooler') !== false: - case stripos($type, 'EMP') !== false: - case stripos($type, 'ExternalFuelTank') !== false: - case stripos($type, 'FlightController') !== false: - case stripos($type, 'FuelIntake') !== false: - case stripos($type, 'FuelTank') !== false: - case stripos($type, 'MainThruster') !== false: - case stripos($type, 'ManneuverThruster') !== false: - case stripos($type, 'Missile') !== false: - case stripos($type, 'Mount') !== false: - case stripos($type, 'Paints') !== false: - case stripos($type, 'PowerPlant') !== false: - case stripos($type, 'QuantumDrive') !== false: - case stripos($type, 'QuantumFuelTank') !== false: - case stripos($type, 'QuantumInterdictionGenerator') !== false: - case stripos($type, 'Radar') !== false: - case stripos($type, 'SalvageModifier') !== false: - case stripos($type, 'SelfDestruct') !== false: - case stripos($type, 'Shield') !== false: - case stripos($type, 'TowingBeam') !== false: - case stripos($type, 'TractorBeam') !== false: - case stripos($type, 'Turret') !== false: - case stripos($type, 'WeaponDefensive') !== false: - case stripos($type, 'WeaponGun') !== false: - case stripos($type, 'WheeledController') !== false: - case in_array($type, [ - 'BombLauncher', - 'MiningArm', - 'MissileLauncher', - 'ToolArm', - 'Turret', - 'TurretBase', - 'UtilityTurret', - 'WeaponMount', - ]): - VehicleItem::dispatch($filePath); - break; - - default: - break; - } - } -} diff --git a/app/Jobs/SC/Import/Knife.php b/app/Jobs/SC/Import/Knife.php deleted file mode 100644 index 302341eeb..000000000 --- a/app/Jobs/SC/Import/Knife.php +++ /dev/null @@ -1,79 +0,0 @@ -loadLabels(); - try { - $parser = new Weapon($this->filePath, $this->labels); - } catch (FileNotFoundException|JsonException $e) { - $this->fail($e); - - return; - } - $item = $parser->getData(); - if (empty($item['knife'])) { - return; - } - - $this->createConfig($item['knife']); - - \App\Models\SC\Char\PersonalWeapon\Knife::updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'can_be_used_for_take_down' => $item['knife']['can_be_used_for_take_down'] ?? null, - 'can_block' => $item['knife']['can_block'] ?? null, - 'can_be_used_in_prone' => $item['knife']['can_be_used_in_prone'] ?? null, - 'can_dodge' => $item['knife']['can_dodge'] ?? null, - 'melee_combat_config_uuid' => $item['knife']['melee_combat_config'] ?? null, - ]); - } - - private function createConfig(array $data): void - { - collect($data['attack_config'])->each(function (array $config) use ($data) { - /** @var MeleeCombatConfig $configModel */ - $configModel = MeleeCombatConfig::query()->firstOrCreate([ - 'uuid' => $data['melee_combat_config'], - 'category' => $config['actionCategory'], - ], [ - 'stun_recovery_modifier' => $config['stunRecoveryModifier'], - 'block_stun_reduction_modifier' => $config['blockStunReductionModifier'], - 'block_stun_stamina_modifier' => $config['blockStunStaminaModifier'], - 'attack_impulse' => $config['attackImpulse'], - 'ignore_body_part_impulse_scale' => $config['ignoreBodyPartImpulseScale'], - 'fullbody_animation' => $config['fullbodyAnimation'], - ]); - - collect($config['damageInfo'])->each(function (float $damage, string $key) use ($configModel) { - $configModel->damages()->updateOrCreate([ - 'name' => strtolower(str_replace('Damage', '', $key)), - ], [ - 'damage' => $damage, - ]); - }); - }); - } -} diff --git a/app/Jobs/SC/Import/MiningLaser.php b/app/Jobs/SC/Import/MiningLaser.php deleted file mode 100644 index 0e3eccdd3..000000000 --- a/app/Jobs/SC/Import/MiningLaser.php +++ /dev/null @@ -1,47 +0,0 @@ -loadLabels(); - try { - $parser = new \App\Services\Parser\SC\MiningLaser($this->filePath, $this->labels); - } catch (FileNotFoundException|JsonException $e) { - $this->fail($e); - - return; - } - $item = $parser->getData(); - - /** @var \App\Models\SC\ItemSpecification\MiningLaser $model */ - \App\Models\SC\ItemSpecification\MiningLaser::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'power_transfer' => $item['power_transfer'] ?? null, - 'optimal_range' => $item['optimal_range'] ?? null, - 'maximum_range' => $item['maximum_range'] ?? null, - 'extraction_throughput' => $item['extraction_throughput'] ?? null, - 'module_slots' => $item['module_slots'] ?? null, - ]); - } -} diff --git a/app/Jobs/SC/Import/MiningModule.php b/app/Jobs/SC/Import/MiningModule.php deleted file mode 100644 index 4e53901a7..000000000 --- a/app/Jobs/SC/Import/MiningModule.php +++ /dev/null @@ -1,43 +0,0 @@ -loadLabels(); - try { - $parser = new \App\Services\Parser\SC\MiningModule($this->filePath, $this->labels); - } catch (FileNotFoundException|JsonException $e) { - $this->fail($e); - - return; - } - $item = $parser->getData(); - - /** @var \App\Models\SC\ItemSpecification\MiningModule $model */ - \App\Models\SC\ItemSpecification\MiningModule::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'type' => $item['type'] ?? null, - ]); - } -} diff --git a/app/Jobs/SC/Import/PersonalWeapon.php b/app/Jobs/SC/Import/PersonalWeapon.php deleted file mode 100644 index 221bc649d..000000000 --- a/app/Jobs/SC/Import/PersonalWeapon.php +++ /dev/null @@ -1,98 +0,0 @@ -loadLabels(); - try { - $parser = new Weapon($this->filePath, $this->labels); - } catch (JsonException|FileNotFoundException $e) { - $this->fail($e->getMessage()); - - return; - } - - $item = $parser->getData(); - - try { - $itemModel = PersonalWeaponModel::query()->withoutGlobalScopes()->where('uuid', $item['uuid'])->firstOrFail(); - } catch (ModelNotFoundException $e) { - return; - } - - $this->addAmmunition($item); - $this->addModes($item, $itemModel); - $this->addLoadout($item, $itemModel); - } - - private function addAmmunition(array $data): void - { - if (empty($data['ammunition']['uuid']) || empty($data['uuid'])) { - return; - } - - (new Ammunition($data))->handle(); - } - - private function addModes(array $data, PersonalWeaponModel $weapon): void - { - if (empty($data['modes'])) { - return; - } - - collect($data['modes']) - ->filter(fn ($e) => isset($e['type'])) - ->each(function (array $mode) use ($weapon) { - $weapon->modes()->updateOrCreate([ - 'mode' => $mode['mode'], - ], [ - 'localised' => $mode['localised'], - 'type' => $mode['type'], - 'rounds_per_minute' => $mode['rounds_per_minute'] ?? 0, - 'ammo_per_shot' => $mode['ammo_per_shot'] ?? 0, - 'pellets_per_shot' => $mode['pellets_per_shot'] ?? 0, - ]); - }); - } - - private function addLoadout(array $data, PersonalWeaponModel $weapon): void - { - /** @var Collection $ports */ - $ports = $weapon->ports; - - if ($ports === null || $ports->isEmpty()) { - return; - } - - collect($data['attachments'])->each(function (array $attachment) use ($ports) { - $port = $ports->where('name', $attachment['port'])->first(); - $port?->update([ - 'equipped_item_uuid' => $attachment['uuid'], - ]); - }); - } -} diff --git a/app/Jobs/SC/Import/ShopItems.php b/app/Jobs/SC/Import/ShopItems.php deleted file mode 100644 index 2680e4e68..000000000 --- a/app/Jobs/SC/Import/ShopItems.php +++ /dev/null @@ -1,104 +0,0 @@ -manufacturers = (new Manufacturers)->getData(); - - try { - $shops = new Shops; - } catch (\JsonException|FileNotFoundException $e) { - $this->fail($e->getMessage()); - - return; - } - - $shops->getData() - ->each(function ($shop) { - /** @var Shop $shop */ - $shopModel = Shop::updateOrCreate([ - 'uuid' => $shop['shop']['uuid'], - ], [ - 'name_raw' => $shop['shop']['name_raw'], - 'name' => $shop['shop']['name'], - 'position' => $shop['shop']['position'], - 'profit_margin' => $shop['shop']['profit_margin'], - 'version' => config('api.sc_data_version'), - ]); - - $toSync = $shop['inventory'] - //->unique('uuid') - ->mapWithKeys(function ($inventory) use ($shopModel) { - /** @var Item $itemModel */ - $itemModel = Item::query()->where('uuid', $inventory['uuid'])->first(); - - if ($itemModel === null) { - return ['unknown' => null]; - } - - // TODO: Extract - if ($inventory['rentable'] === true && isset($inventory['rental']) && ! empty($inventory['rental'])) { - ShopItemRental::updateOrCreate([ - 'item_uuid' => $itemModel->uuid, - 'shop_uuid' => $shopModel->uuid, - 'node_uuid' => $inventory['node_uuid'], - ], $inventory['rental'] + ['version' => config('api.sc_data_version')]); - } - - return [ - $itemModel->id => [ - 'item_uuid' => $itemModel->uuid, - 'shop_uuid' => $shopModel->uuid, - 'node_uuid' => $inventory['node_uuid'], - 'base_price' => round($inventory['base_price'], 10), - 'base_price_offset' => $inventory['base_price_offset'], - 'max_discount' => $inventory['max_discount'], - 'max_premium' => $inventory['max_premium'], - 'inventory' => $inventory['inventory'], - 'optimal_inventory' => $inventory['optimal_inventory'], - 'max_inventory' => $inventory['max_inventory'], - 'auto_restock' => $inventory['auto_restock'], - 'auto_consume' => $inventory['auto_consume'], - 'refresh_rate' => round($inventory['refresh_rate'], 10), - 'buyable' => $inventory['buyable'], - 'sellable' => $inventory['sellable'], - 'rentable' => $inventory['rentable'], - 'version' => config('api.sc_data_version'), - ], - ]; - }) - ->filter(function ($item) { - return $item !== null; - }); - - $shopModel->items()->sync($toSync); - }); - } -} diff --git a/app/Jobs/SC/Import/Vehicle.php b/app/Jobs/SC/Import/Vehicle.php deleted file mode 100644 index 8994473ee..000000000 --- a/app/Jobs/SC/Import/Vehicle.php +++ /dev/null @@ -1,507 +0,0 @@ -hardpoints = new Collection; - $this->parts = new Collection; - - $this->minPartDamage = 100; - } - - public function handle(): void - { - $manufacturers = (new Manufacturers)->getData(); - - $vehicle = $this->shipData; - - try { - $rawData = File::get($vehicle['filePathRaw']); - - $vehicle['rawData'] = json_decode($rawData, true, 512, JSON_THROW_ON_ERROR); - } catch (FileNotFoundException|JsonException $e) { - $this->fail($e->getMessage()); - } - - if (! isset($vehicle['rawData']['Raw']['Entity']['__ref'])) { - return; - } - - /** @var \App\Models\SC\Vehicle\Vehicle $vehicleModel */ - $vehicleModel = \App\Models\SC\Vehicle\Vehicle::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $vehicle['rawData']['Raw']['Entity']['__ref'], - ], $this->getVehicleModelArray($vehicle) + ['class_name' => $vehicle['ClassName']]); - - if (! $vehicleModel->item === null || ! optional($vehicleModel->item)->exists) { - $itemParser = new \App\Services\Parser\SC\Item( - $vehicle['filePathRaw'], - $manufacturers - ); - - $data = $itemParser->getData(); - - if ($data !== null) { - (new Item($data))->handle(); - } - } else { - $vehicleModel->item->update([ - 'version' => config('api.sc_data_version'), - ]); - } - - // Manually override the Fury Manufacturer - if (in_array($vehicle['rawData']['Raw']['Entity']['__ref'], [ - '96b11061-68ce-4896-9424-fc8804a410ae', - '469d850e-b86b-47fc-9ee2-df81d775ccc8', - ], true) && $vehicleModel->item?->manufacturer_id === 1) { - $mirai = Manufacturer::query()->where('name', 'Mirai')->where('code', '<>', '')->first(); - if ($mirai?->exists ?? false) { - $vehicleModel->item->update([ - 'manufacturer_id' => $mirai->id, - ]); - } - } - - $vehicleModel->refresh(); - if (Arr::get($vehicle, 'Inventory') !== null) { - $vehicleModel->item->container()->updateOrCreate([ - 'item_uuid' => $vehicle['rawData']['Raw']['Entity']['__ref'], - ], [ - 'width' => Arr::get($vehicle, 'Inventory.x'), - 'height' => Arr::get($vehicle, 'Inventory.y'), - 'length' => Arr::get($vehicle, 'Inventory.z'), - 'scu' => Arr::get($vehicle, 'Inventory.SCU'), - 'unit' => Arr::get($vehicle, 'Inventory.unit'), - ]); - } - - $this->createHardpoints($vehicleModel, $vehicle['rawData']); - - $vehicleModel->hardpoints()->whereNotIn('hardpoint_name', $this->hardpoints)->delete(); - $vehicleModel->parts()->whereNotIn('name', $this->parts)->delete(); - $this->createHandlingModel($vehicleModel, $vehicle['rawData']['Raw']); - - if (Arr::get($vehicle, 'CargoGrids') !== null) { - collect(Arr::get($vehicle, 'CargoGrids'))->each(function ($grid) use ($vehicleModel) { - $vehicleModel->cargoGrids()->updateOrCreate([ - 'container_uuid' => $grid['uuid'], - ], [ - 'capacity' => Arr::get($grid, 'SCU'), - 'unit_name' => Arr::get($grid, 'unitName'), - 'x' => Arr::get($grid, 'x'), - 'y' => Arr::get($grid, 'y'), - 'z' => Arr::get($grid, 'z'), - 'min_x' => Arr::get($grid, 'minSize.x'), - 'min_y' => Arr::get($grid, 'minSize.y'), - 'min_z' => Arr::get($grid, 'minSize.z'), - 'max_x' => Arr::get($grid, 'maxSize.x'), - 'max_y' => Arr::get($grid, 'maxSize.y'), - 'max_z' => Arr::get($grid, 'maxSize.z'), - 'is_open' => Arr::get($grid, 'isOpenContainer'), - 'is_external' => Arr::get($grid, 'isExternalContainer'), - 'is_closed' => Arr::get($grid, 'isClosedContainer'), - ]); - }); - } - } - - public function getVehicleModelArray(array $vehicle): array - { - $key = isset($vehicle['FlightCharacteristics']) ? 'FlightCharacteristics' : 'DriveCharacteristics'; - - $data = [ - 'item_uuid' => $vehicle['rawData']['Raw']['Entity']['__ref'], - - 'shipmatrix_id' => $this->tryGetShipmatrixIdForVehicle($vehicle)->id ?? 0, - 'name' => $vehicle['Name'], - 'career' => $vehicle['Career'], - 'role' => $vehicle['Role'], - 'is_ship' => (bool) $vehicle['IsSpaceship'], - 'size' => $vehicle['Size'], - 'width' => $vehicle['Width'] ?? 0, - 'height' => $vehicle['Height'] ?? 0, - 'length' => $vehicle['Length'] ?? 0, - - 'crew' => $vehicle['Crew'] ?? 1, - 'weapon_crew' => $vehicle['WeaponCrew'] ?? 0, - 'operations_crew' => $vehicle['OperationsCrew'] ?? 0, - 'mass' => $vehicle['Mass'], - 'cargo_capacity' => $vehicle['Cargo'], - 'health' => $vehicle['Health'] ?? null, - 'shield_face_type' => $vehicle['ShieldFaceType'] ?? null, - - 'acceleration_main' => $this->numFormat(Arr::get($vehicle, $key.'.Acceleration.Main', 0)), - 'acceleration_retro' => $this->numFormat(Arr::get($vehicle, $key.'.Acceleration.Retro', 0)), - 'acceleration_vtol' => $this->numFormat(Arr::get($vehicle, $key.'.Acceleration.Vtol', 0)), - 'acceleration_maneuvering' => $this->numFormat(Arr::get($vehicle, $key.'.Acceleration.Maneuvering', 0)), - - 'acceleration_g_main' => $this->numFormat(Arr::get($vehicle, $key.'.AccelerationG.Main', 0)), - 'acceleration_g_retro' => $this->numFormat(Arr::get($vehicle, $key.'.AccelerationG.Retro', 0)), - 'acceleration_g_vtol' => $this->numFormat(Arr::get($vehicle, $key.'.AccelerationG.Vtol', 0)), - 'acceleration_g_maneuvering' => $this->numFormat(Arr::get($vehicle, $key.'.AccelerationG.Maneuvering', 0)), - - 'claim_time' => $this->numFormat($vehicle['Insurance']['StandardClaimTime'] ?? 0), - 'expedite_time' => $this->numFormat($vehicle['Insurance']['ExpeditedClaimTime'] ?? 0), - 'expedite_cost' => $this->numFormat($vehicle['Insurance']['ExpeditedCost'] ?? 0), - - 'zero_to_scm' => $this->numFormat($vehicle[$key]['ZeroToScm'] ?? 0), - 'zero_to_max' => $this->numFormat($vehicle[$key]['ZeroToMax'] ?? 0), - - 'scm_to_zero' => $this->numFormat($vehicle[$key]['ScmToZero'] ?? 0), - 'max_to_zero' => $this->numFormat($vehicle[$key]['MaxToZero'] ?? 0), - ]; - - return $data; - } - - /** - * As some in-game ship names differ from the ship matrix, we try to catch this here - * Currently an in-game ship needs to have an accompanying ship-matrix entry - * - * @return Builder|Model|object|null - */ - private function tryGetShipmatrixIdForVehicle(array $vehicle) - { - $name = str_replace('_', ' ', $vehicle['Name']); - $nameFix = explode(' ', $name); - array_shift($nameFix); - $name = implode(' ', $nameFix ?? $vehicle['Name']); - $nameDashed = implode('-', $nameFix ?? $vehicle['Name']); - - $className = explode('_', $vehicle['ClassName']); - array_shift($className); - - switch ($vehicle['Name']) { - case 'Aegis Retaliator': - $name = 'Retaliator Bomber'; - break; - - case 'Anvil C8R Pisces Rescue': - $name = 'C8R Pisces'; - break; - - case 'Anvil F7C-M Hornet Heartseeker': - $name = 'F7C-M Super Hornet Heartseeker'; - break; - - case 'Origin 600i': - $name = '600i Explorer'; - break; - - case 'C.O. Mustang CitizenCon 2948 Edition': - $name = 'Mustang Alpha Vindicator'; - break; - - case 'Crusader A2 Hercules Starlifter': - $name = 'A2 Hercules'; - break; - - case 'Crusader C2 Hercules Starlifter': - $name = 'C2 Hercules'; - break; - - case 'Crusader M2 Hercules Starlifter': - $name = 'M2 Hercules'; - break; - - case 'Crusader Mercury Star Runner': - $name = 'Mercury'; - break; - - case 'Crusader Ares Star Fighter Ion': - $name = 'Ares Ion'; - break; - - case 'Crusader Ares Star Fighter Inferno': - $name = 'Ares Inferno'; - break; - - case 'Drake Dragonfly': - $name = 'Dragonfly Black'; - break; - - case 'Kruger P-72 Archimedes': - $name = 'P-72 Archimedes'; - break; - - case 'Origin M50 Interceptor': - $name = 'M50'; - break; - - case 'Origin 85X Limited': - $name = '85X'; - break; - } - - return $this->queryForName(['name', $name]) ?? - $this->queryForName(['name', 'LIKE', sprintf('%%%s%%', $name)]) ?? - $this->queryForName(['name', $nameDashed]) ?? - $this->queryForName(['name', 'LIKE', sprintf('%%%s%%', $nameDashed)]); - } - - /** - * Just a small query wrapper - */ - private function queryForName(array $config): ?Model - { - return \App\Models\StarCitizen\Vehicle\Vehicle\Vehicle::query()->where(...$config)->first(); - } - - /** - * "Rounds" to a given precision - */ - private function numFormat($data): float|int - { - $num = $data ?? 0; - - if ($num === 'NaN' || $num === 'Infinity') { - return 0; - } - - $negation = ($num < 0) ? (-1) : 1; - $coefficient = 10 ** 3; - - return $negation * floor((abs((float) $num) * $coefficient)) / $coefficient; - } - - private function getItem(string $search): ?\App\Models\SC\Item\Item - { - return \App\Models\SC\Item\Item::query() - ->withoutGlobalScopes() - ->where('uuid', $search) - ->orWhere('class_name', strtolower($search)) - ->first(); - } - - /** - * Creates all hardpoints found on a vehicle - * Iterates through all sup-hardpoints also - */ - private function createHardpoints(\App\Models\SC\Vehicle\Vehicle $vehicle, array $rawData): void - { - $entries = Arr::get( - $rawData, - 'Raw.Entity.Components.SEntityComponentDefaultLoadoutParams.loadout.SItemPortLoadoutManualParams.entries' - ); - - if ($entries === null) { - return; - } - - $hardpoints = []; - $this->mapHardpoints(Arr::get($rawData, 'Vehicle.Parts', []), $hardpoints); - - $hardpoints = array_reverse($hardpoints); - - collect($entries) - ->chunk(5) - ->each(function (Collection $entries) use ($hardpoints, $vehicle) { - $entries - ->each(function ($hardpoint) use ($hardpoints, $vehicle) { - $item = null; - if (! empty($hardpoint['entityClassReference']) && $hardpoint['entityClassReference'] !== '00000000-0000-0000-0000-000000000000') { - $item = $this->getItem($hardpoint['entityClassReference']); - } elseif (! empty($hardpoint['entityClassName'])) { - $item = $this->getItem($hardpoint['entityClassName']); - } - - $itemPortName = strtolower($hardpoint['itemPortName']); - $this->hardpoints->push($hardpoint['itemPortName']); - - /** @var Hardpoint $point */ - $point = $vehicle->hardpoints()->updateOrCreate([ - 'hardpoint_name' => $hardpoint['itemPortName'], - ], [ - 'class_name' => $item?->class_name ?? $hardpoint['entityClassName'] ?? null, - 'equipped_item_uuid' => $item?->uuid ?? $hardpoint['entityClassReference'] ?? null, - 'min_size' => $hardpoints[$itemPortName]['ItemPort']['minSize'] ?? null, - 'max_size' => $hardpoints[$itemPortName]['ItemPort']['maxSize'] ?? null, - ]); - - $this->createSubPoint( - Arr::get($hardpoint, 'loadout.SItemPortLoadoutManualParams.entries', []), - $point, - $vehicle - ); - }); - }); - - // Add Hardpoints only found on the Vehicle.Parts key - collect($hardpoints) - // Create vehicle parts - ->each(function ($hardpoint) use ($vehicle) { - $isBaseBody = $hardpoint['class'] === 'Animated' && isset($hardpoint['scopeContext']); - $isPart = ! empty($hardpoint['name']) && isset($hardpoint['damageMax'])/* && $hardpoint['damageMax'] > $this->minPartDamage*/; - - if ($isBaseBody || $isPart) { - if (! empty($hardpoint['parent'])) { - $hardpoint['parent'] = $vehicle->parts()->where('name', $hardpoint['parent'])->first()->id ?? null; - } - - $vehicle->parts()->updateOrCreate([ - 'name' => $hardpoint['name'], - ], [ - 'parent_id' => $hardpoint['parent'] ?? null, - 'damage_max' => $hardpoint['damageMax'] ?? 0, - ]); - - $this->parts->push($hardpoint['name']); - } - }) - ->whereNotIn('name', $this->hardpoints) - ->filter(function (array $hardpoint) { - return $hardpoint['class'] === 'ItemPort'; - }) - ->filter(function (array $hardpoint) { - return isset($hardpoint['ItemPort']) && ! empty($hardpoint['ItemPort']['flags']) && ($hardpoint['ItemPort']['minSize'] ?? 0) > 0; - }) - ->filter(function (array $hardpoint) { - // Filter out some - return ! Str::contains($hardpoint['name'], [ - '$slot', - '_trail_', - '_SQUIB_', - 'audio', - 'animated', - 'Helper', - 'LandingGear', - 'gameplay', - 'ObjectContainer', - ], true); - }) - ->filter(function (array $hardpoint) { - return ($hardpoint['skipPart'] ?? false) === false; - }) - ->each(function ($hardpoint) use ($vehicle) { - $where = [ - 'hardpoint_name' => $hardpoint['name'], - ]; - - if (str_starts_with($hardpoint['parent'] ?? '', 'hardpoint')) { - $parent = $vehicle->hardpoints()->where('hardpoint_name', $hardpoint['parent'])->first()?->id; - $where['parent_hardpoint_id'] = $parent; - } - - $vehicle->hardpoints()->updateOrCreate($where, [ - 'min_size' => Arr::get($hardpoint, 'ItemPort.minSize'), - 'max_size' => Arr::get($hardpoint, 'ItemPort.maxSize'), - ]); - - $this->hardpoints->push($hardpoint['name']); - }); - } - - /** - * Flat-maps all hardpoints from hardpoint name to hardpoint data - * This is used to get the min and max sizes later on - */ - private function mapHardpoints(array $parts, array &$out, ?string $parent = null): void - { - foreach ($parts as $part) { - if (! isset($part['name']) || $part === 'xmlParts') { - continue; - } - - if ($parent !== null) { - $part['parent'] = $parent; - } - - if (isset($part['Parts'])) { - $this->mapHardpoints($part['Parts'], $out, $part['name']); - unset($part['Parts']); - } - - unset( - $part['ItemPort']['Connections'], - $part['ItemPort']['ControllerDef'], - $part['ItemPort']['Types'], - $part['Effects'], - $part['DamageBehaviors'], - ); - $out[strtolower($part['name'])] = $part; - } - } - - /** - * This runs on each child hardpoint found on a hardpoint recursively - */ - private function createSubPoint(array $entries, Hardpoint $parent, \App\Models\SC\Vehicle\Vehicle $vehicle): void - { - foreach ($entries as $subPoint) { - if (empty($subPoint['entityClassName']) && empty($subPoint['entityClassReference'])) { - continue; - } - - $item = $this->getItem($subPoint['entityClassReference']) ?? $this->getItem($subPoint['entityClassName']); - - $this->hardpoints->push($subPoint['itemPortName']); - $point = $vehicle->hardpoints()->updateOrCreate([ - 'hardpoint_name' => $subPoint['itemPortName'], - 'parent_hardpoint_id' => $parent->id, - ], [ - 'class_name' => $item?->class_name ?? $subPoint['entityClassName'], - 'equipped_item_uuid' => $item?->uuid, - ]); - - $subEntries = Arr::get($subPoint, 'loadout.SItemPortLoadoutManualParams.entries'); - - if (! empty($subEntries)) { - $this->createSubPoint($subEntries, $point, $vehicle); - } - } - } - - private function createHandlingModel(\App\Models\SC\Vehicle\Vehicle $vehicle, array $rawData): void - { - $handlingData = Arr::get($rawData, 'Vehicle.MovementParams.ArcadeWheeled'); - if ($handlingData === null) { - return; - } - - $vehicle->handling()->updateOrCreate([ - 'max_speed' => Arr::get($handlingData, 'Handling.Power.topSpeed'), - 'reverse_speed' => Arr::get($handlingData, 'Handling.Power.reverseSpeed'), - 'acceleration' => Arr::get($handlingData, 'Handling.Power.acceleration'), - 'deceleration' => Arr::get($handlingData, 'Handling.Power.decceleration'), - 'v0_steer_max' => Arr::get($handlingData, 'v0SteerMax'), - 'kv_steer_max' => Arr::get($handlingData, 'kvSteerMax'), - 'vmax_steer_max' => Arr::get($handlingData, 'vMaxSteerMax'), - ]); - } -} diff --git a/app/Jobs/SC/Import/VehicleItem.php b/app/Jobs/SC/Import/VehicleItem.php deleted file mode 100644 index efc0b86d5..000000000 --- a/app/Jobs/SC/Import/VehicleItem.php +++ /dev/null @@ -1,508 +0,0 @@ -loadLabels(); - - try { - $parser = new \App\Services\Parser\SC\VehicleItems\VehicleItem($this->filePath, $this->labels); - } catch (FileNotFoundException|JsonException $e) { - $this->fail($e); - - return; - } - - $item = $parser->getData(); - - try { - $itemModel = \App\Models\SC\Item\Item::query()->withoutGlobalScopes()->where('uuid', $item['uuid'])->firstOrFail(); - } catch (ModelNotFoundException $e) { - return; - } - - $this->createModelSpecification($item, $itemModel); - } - - private function createModelSpecification(array $item, \App\Models\SC\Item\Item $itemModel): void - { - switch ($itemModel->type) { - case 'Armor': - $this->createArmor($item); - break; - - case 'Bomb': - $this->createBomb($item); - break; - - case 'FlightController': - $this->createFlightController($item); - break; - - case 'Cooler': - $this->createCooler($item); - break; - - case 'EMP': - $this->createEmp($item); - break; - - case 'PowerPlant': - $this->createPowerPlant($item); - break; - - case 'Shield': - $this->createShield($item); - break; - - case 'QuantumDrive': - $this->createQuantumDrive($item); - break; - - case 'QuantumInterdictionGenerator': - $this->createQuantumInterdictionGenerator($item); - break; - - case 'FuelTank': - case 'ExternalFuelTank': - case 'QuantumFuelTank': - $this->createFuelTank($item); - break; - - case 'FuelIntake': - $this->createFuelIntake($item); - break; - - case 'WeaponDefensive': - case 'WeaponGun': - $this->createWeapon($item); - break; - - case 'Missile': - $this->createMissile($item); - break; - - case 'MainThruster': - case 'ManneuverThruster': - $this->createThruster($item); - break; - - case 'SelfDestruct': - $this->createSelfDestruct($item); - break; - - case 'TowingBeam': - case 'TractorBeam': - $this->createTractorBeam($item); - break; - - case 'SalvageModifier': - $this->createSalvageModifier($item); - break; - } - } - - private function createArmor(array $item): void - { - Armor::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'signal_infrared' => $item['armor']['signal_infrared'] ?? null, - 'signal_electromagnetic' => $item['armor']['signal_electromagnetic'] ?? null, - 'signal_cross_section' => $item['armor']['signal_cross_section'] ?? null, - 'damage_physical' => $item['armor']['damage_physical'] ?? null, - 'damage_energy' => $item['armor']['damage_energy'] ?? null, - 'damage_distortion' => $item['armor']['damage_distortion'] ?? null, - 'damage_thermal' => $item['armor']['damage_thermal'] ?? null, - 'damage_biochemical' => $item['armor']['damage_biochemical'] ?? null, - 'damage_stun' => $item['armor']['damage_stun'] ?? null, - ]); - } - - private function createFlightController(array $item): void - { - FlightController::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'scm_speed' => $item['flight_controller']['scm_speed'] ?? null, - 'max_speed' => $item['flight_controller']['max_speed'] ?? null, - 'pitch' => $item['flight_controller']['pitch'] ?? null, - 'yaw' => $item['flight_controller']['yaw'] ?? null, - 'roll' => $item['flight_controller']['roll'] ?? null, - - 'scm_boost_forward' => $item['flight_controller']['scm_boost_forward'] ?? null, - 'scm_boost_backward' => $item['flight_controller']['scm_boost_backward'] ?? null, - 'pitch_boost_multiplier' => $item['flight_controller']['pitch_boost_multiplier'] ?? null, - 'roll_boost_multiplier' => $item['flight_controller']['roll_boost_multiplier'] ?? null, - 'yaw_boost_multiplier' => $item['flight_controller']['yaw_boost_multiplier'] ?? null, - 'afterburner_capacitor' => $item['flight_controller']['afterburner_capacitor'] ?? null, - 'afterburner_idle_cost' => $item['flight_controller']['afterburner_idle_cost'] ?? null, - 'afterburner_linear_cost' => $item['flight_controller']['afterburner_linear_cost'] ?? null, - 'afterburner_angular_cost' => $item['flight_controller']['afterburner_angular_cost'] ?? null, - 'afterburner_regen_per_sec' => $item['flight_controller']['afterburner_regen_per_sec'] ?? null, - 'afterburner_regen_delay_after_use' => $item['flight_controller']['afterburner_regen_delay_after_use'] ?? null, - 'afterburner_pre_delay_time' => $item['flight_controller']['afterburner_pre_delay_time'] ?? null, - 'afterburner_ramp_up_time' => $item['flight_controller']['afterburner_ramp_up_time'] ?? null, - 'afterburner_ramp_down_time' => $item['flight_controller']['afterburner_ramp_down_time'] ?? null, - ]); - } - - private function createEmp(array $item): void - { - Emp::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'charge_duration' => $item['emp']['charge_duration'] ?? null, - 'emp_radius' => $item['emp']['emp_radius'] ?? null, - 'unleash_duration' => $item['emp']['unleash_duration'] ?? null, - 'cooldown_duration' => $item['emp']['cooldown_duration'] ?? null, - ]); - } - - private function createQuantumInterdictionGenerator(array $item): void - { - QuantumInterdictionGenerator::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'jammer_range' => $item['qig']['jammer_range'] ?? null, - 'interdiction_range' => $item['qig']['interdiction_range'] ?? null, - 'charge_duration' => $item['qig']['charge_duration'] ?? null, - 'discharge_duration' => $item['qig']['discharge_duration'] ?? null, - 'cooldown_duration' => $item['qig']['cooldown_duration'] ?? null, - ]); - } - - private function createCooler(array $item): void - { - Cooler::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'cooling_rate' => $item['cooler']['cooling_rate'], - 'suppression_ir_factor' => $item['cooler']['suppression_ir_factor'], - 'suppression_heat_factor' => $item['cooler']['suppression_heat_factor'], - ]); - } - - private function createPowerPlant(array $item): void - { - PowerPlant::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'power_output' => $item['power_plant']['power_output'] ?? 0, - ]); - } - - private function createShield(array $item): void - { - /** @var Shield $shield */ - $shield = Shield::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'max_shield_health' => $item['shield']['max_shield_health'], - 'max_shield_regen' => $item['shield']['max_shield_regen'], - 'decay_ratio' => $item['shield']['decay_ratio'], - 'downed_regen_delay' => $item['shield']['downed_regen_delay'], - 'damage_regen_delay' => $item['shield']['damage_regen_delay'], - 'max_reallocation' => 0, //$item['shield']['max_reallocation'], - 'reallocation_rate' => 0, //$item['shield']['reallocation_rate'], - ]); - - // foreach ($item['shield']['absorptions'] as $type => $absorption) { - // $shield->absorptions()->updateOrCreate([ - // 'ship_shield_id' => $shield->id, - // 'type' => $type - // ], [ - // 'min' => $absorption['min'] ?? 0, - // 'max' => $absorption['max'] ?? 0, - // ]); - // } - } - - private function createQuantumDrive(array $item): void - { - /** @var QuantumDrive $drive */ - $drive = QuantumDrive::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'quantum_fuel_requirement' => $item['quantum_drive']['quantum_fuel_requirement'], - 'jump_range' => $item['quantum_drive']['jump_range'], - 'disconnect_range' => $item['quantum_drive']['disconnect_range'], - 'pre_ramp_up_thermal_energy_draw' => $item['quantum_drive']['pre_ramp_up_thermal_energy_draw'], - 'ramp_up_thermal_energy_draw' => $item['quantum_drive']['ramp_up_thermal_energy_draw'], - 'in_flight_thermal_energy_draw' => $item['quantum_drive']['in_flight_thermal_energy_draw'], - 'ramp_down_thermal_energy_draw' => $item['quantum_drive']['ramp_down_thermal_energy_draw'], - 'post_ramp_down_thermal_energy_draw' => $item['quantum_drive']['post_ramp_down_thermal_energy_draw'], - ]); - - foreach ($item['quantum_drive']['modes'] as $type => $mode) { - $drive->modes()->updateOrCreate([ - 'type' => $type, - ], [ - 'drive_speed' => $mode['drive_speed'], - 'cooldown_time' => $mode['cooldown_time'], - 'stage_one_accel_rate' => $mode['stage_one_accel_rate'], - 'stage_two_accel_rate' => $mode['stage_two_accel_rate'], - 'engage_speed' => $mode['engage_speed'], - 'interdiction_effect_time' => $mode['interdiction_effect_time'], - 'calibration_rate' => $mode['calibration_rate'], - 'min_calibration_requirement' => $mode['min_calibration_requirement'], - 'max_calibration_requirement' => $mode['max_calibration_requirement'], - 'calibration_process_angle_limit' => $mode['calibration_process_angle_limit'], - 'calibration_warning_angle_limit' => $mode['calibration_warning_angle_limit'], - 'spool_up_time' => $mode['spool_up_time'], - ]); - } - } - - private function createFuelTank(array $item): void - { - FuelTank::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'fill_rate' => $item['fuel_tank']['fill_rate'] ?? null, - 'drain_rate' => $item['fuel_tank']['drain_rate'] ?? null, - 'capacity' => $item['fuel_tank']['capacity'] ?? null, - ]); - } - - private function createFuelIntake(array $item): void - { - if ($item['fuel_intake'] === null) { - return; - } - - FuelIntake::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'fuel_push_rate' => $item['fuel_intake']['fuel_push_rate'] ?? null, - 'minimum_rate' => $item['fuel_intake']['minimum_rate'] ?? null, - ]); - } - - private function createWeapon(array $item): void - { - if (empty($item['weapon'])) { - return; - } - - /** @var VehicleWeapon $weapon */ - $weapon = VehicleWeapon::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'weapon_type' => Arr::get($item, 'weapon.weapon_type'), - 'weapon_class' => Arr::get($item, 'weapon.weapon_class'), - 'capacity' => Arr::get($item, 'weapon.capacity'), - 'ammunition_uuid' => $item['weapon']['ammunition']['uuid'] ?? null, - ]); - - if (! empty($item['weapon']['ammunition']['uuid'])) { - (new Ammunition($item['weapon']))->handle(); - } - - collect($item['weapon']['modes'])->each(function (array $mode) use ($weapon) { - $weapon->modes()->updateOrCreate([ - 'mode' => $mode['mode'], - ], [ - 'localised' => $mode['localised'] ?? null, - 'type' => $mode['type'] ?? null, - 'rounds_per_minute' => $mode['rounds_per_minute'] ?? null, - 'ammo_per_shot' => $mode['ammo_per_shot'] ?? null, - 'pellets_per_shot' => $mode['pellets_per_shot'] ?? null, - ]); - }); - - if (! empty($item['weapon']['regen_consumption'])) { - $weapon->regen()->updateOrCreate([ - 'weapon_id' => $weapon->id, - ], [ - 'requested_regen_per_sec' => $item['weapon']['regen_consumption']['requested_regen_per_sec'], - 'requested_ammo_load' => $item['weapon']['regen_consumption']['requested_ammo_load'], - 'cooldown' => $item['weapon']['regen_consumption']['cooldown'], - 'cost_per_bullet' => $item['weapon']['regen_consumption']['cost_per_bullet'], - ]); - } - } - - private function createMissile(array $item): void - { - if (! isset($item['missile']['signal_type'])) { - return; - } - - $lockRangeMax = $item['missile']['lock_range_max'] ?? null; - if ($lockRangeMax !== null) { - $lockRangeMax = max(0, $lockRangeMax); - } - $lockRangeMin = $item['missile']['lock_range_min'] ?? null; - if ($lockRangeMin !== null) { - $lockRangeMin = max(0, $lockRangeMin); - } - - $missile = Missile::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'signal_type' => $item['missile']['signal_type'], - 'lock_time' => $item['missile']['lock_time'] ?? null, - 'lock_range_max' => $lockRangeMax, - 'lock_range_min' => $lockRangeMin, - 'lock_angle' => $item['missile']['lock_angle'] ?? null, - 'tracking_signal_min' => $item['missile']['tracking_signal_min'] ?? null, - 'speed' => $item['missile']['speed'] ?? null, - 'fuel_tank_size' => $item['missile']['fuel_tank_size'] ?? null, - 'explosion_radius_min' => $item['missile']['explosion_radius_min'] ?? null, - 'explosion_radius_max' => $item['missile']['explosion_radius_max'] ?? null, - ]); - - if (isset($item['missile']['damages'])) { - foreach ($item['missile']['damages'] as $name => $damage) { - $missile->damages()->updateOrCreate([ - 'missile_id' => $missile->id, - 'name' => $name, - ], [ - 'damage' => $damage, - ]); - } - } - } - - private function createBomb(array $item): void - { - if (! isset($item['bomb']['arm_time'])) { - return; - } - - $bomb = Bomb::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'arm_time' => $item['bomb']['arm_time'] ?? null, - 'ignite_time' => $item['bomb']['ignite_time'] ?? null, - 'collision_delay_time' => $item['bomb']['collision_delay_time'] ?? null, - 'explosion_safety_distance' => $item['bomb']['explosion_safety_distance'] ?? null, - 'explosion_radius_min' => $item['bomb']['explosion_radius_min'] ?? null, - 'explosion_radius_max' => $item['bomb']['explosion_radius_max'] ?? null, - ]); - - if (isset($item['bomb']['damages'])) { - foreach ($item['bomb']['damages'] as $name => $damage) { - $bomb->damages()->updateOrCreate([ - 'bomb_id' => $bomb->id, - 'name' => $name, - ], [ - 'damage' => $damage, - ]); - } - } - } - - private function createThruster(array $item): void - { - if ($item['thruster'] === null) { - return; - } - - Thruster::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'thrust_capacity' => $item['thruster']['thrust_capacity'] ?? null, - 'min_health_thrust_multiplier' => $item['thruster']['min_health_thrust_multiplier'] ?? null, - 'fuel_burn_per_10k_newton' => $item['thruster']['fuel_burn_per_10k_newton'] ?? null, - 'type' => $item['thruster']['type'], - ]); - } - - private function createSelfDestruct(array $item): void - { - SelfDestruct::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'damage' => $item['self_destruct']['damage'] ?? null, - 'radius' => $item['self_destruct']['radius'] ?? null, - 'min_radius' => $item['self_destruct']['min_radius'] ?? null, - 'phys_radius' => $item['self_destruct']['phys_radius'] ?? null, - 'min_phys_radius' => $item['self_destruct']['min_phys_radius'] ?? null, - 'time' => $item['self_destruct']['time'] ?? null, - ]); - } - - private function createRadar(array $item, VehicleItemModel $shipItem): Model - { - return $shipItem->specification()->updateOrCreate([ - 'uuid' => $item['uuid'], - ], [ - 'detection_lifetime' => $item['radar']['detection_lifetime'] ?? 0, - 'altitude_ceiling' => $item['radar']['altitude_ceiling'] ?? 0, - 'enable_cross_section_occlusion' => $item['radar']['enable_cross_section_occlusion'] ?? 0, - 'ship_item_id' => $shipItem->id, - ]); - } - - private function createTractorBeam(array $item): void - { - TractorBeam::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'min_force' => $item['tractor_beam']['min_force'] ?? null, - 'max_force' => $item['tractor_beam']['max_force'] ?? null, - 'min_distance' => $item['tractor_beam']['min_distance'] ?? null, - 'max_distance' => $item['tractor_beam']['max_distance'] ?? null, - 'full_strength_distance' => $item['tractor_beam']['full_strength_distance'] ?? null, - 'max_angle' => $item['tractor_beam']['max_angle'] ?? null, - 'max_volume' => $item['tractor_beam']['max_volume'] ?? null, - 'volume_force_coefficient' => $item['tractor_beam']['volume_force_coefficient'] ?? null, - 'tether_break_time' => $item['tractor_beam']['tether_break_time'] ?? null, - 'safe_range_value_factor' => $item['tractor_beam']['safe_range_value_factor'] ?? null, - ]); - } - - private function createSalvageModifier(array $item): void - { - SalvageModifier::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'salvage_speed_multiplier' => $item['salvage_modifier']['salvage_speed_multiplier'] ?? null, - 'radius_multiplier' => $item['salvage_modifier']['radius_multiplier'] ?? null, - 'extraction_efficiency' => $item['salvage_modifier']['extraction_efficiency'] ?? null, - ]); - } -} diff --git a/app/Jobs/SC/Import/WeaponAttachment.php b/app/Jobs/SC/Import/WeaponAttachment.php deleted file mode 100644 index aa5749b99..000000000 --- a/app/Jobs/SC/Import/WeaponAttachment.php +++ /dev/null @@ -1,67 +0,0 @@ -loadLabels(); - try { - $parser = new \App\Services\Parser\SC\WeaponAttachment($this->filePath, $this->labels); - } catch (FileNotFoundException|JsonException $e) { - $this->fail($e); - - return; - } - - $item = $parser->getData(); - - if ($item === null) { - return; - } - - if (! empty($item['ammo'])) { - PersonalWeaponMagazine::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'initial_ammo_count' => $item['ammo']['initial_ammo_count'] ?? null, - 'max_ammo_count' => $item['ammo']['max_ammo_count'] ?? null, - 'type' => $item['item_type'] ?? null, - 'ammunition_uuid' => $item['ammo']['ammunition_uuid'], - ]); - } - - if (! empty($item['iron_sight'])) { - IronSight::query()->withoutGlobalScopes()->updateOrCreate([ - 'item_uuid' => $item['uuid'], - ], [ - 'default_range' => $item['iron_sight']['default_range'], - 'max_range' => $item['iron_sight']['max_range'], - 'range_increment' => $item['iron_sight']['range_increment'], - 'auto_zeroing_time' => $item['iron_sight']['auto_zeroing_time'], - 'zoom_scale' => $item['iron_sight']['zoom_scale'], - 'zoom_time_scale' => $item['iron_sight']['zoom_time_scale'], - ]); - } - } -} diff --git a/app/Jobs/SC/Import/WeaponModifier.php b/app/Jobs/SC/Import/WeaponModifier.php deleted file mode 100644 index 06f2bcd17..000000000 --- a/app/Jobs/SC/Import/WeaponModifier.php +++ /dev/null @@ -1,83 +0,0 @@ -loadLabels(); - try { - $parser = new \App\Services\Parser\SC\WeaponModifier($this->filePath, $this->labels); - } catch (FileNotFoundException|JsonException $e) { - $this->fail($e); - - return; - } - $data = $parser->getData(); - - if ($data === null) { - return; - } - - /** @var ItemWeaponModifierData $model */ - ItemWeaponModifierData::updateOrCreate([ - 'item_uuid' => $data['uuid'], - ], [ - 'fire_rate_multiplier' => $data['fire_rate_multiplier'] ?? null, - 'damage_multiplier' => $data['damage_multiplier'] ?? null, - 'damage_over_time_multiplier' => $data['damage_over_time_multiplier'] ?? null, - 'projectile_speed_multiplier' => $data['projectile_speed_multiplier'] ?? null, - 'ammo_cost_multiplier' => $data['ammo_cost_multiplier'] ?? null, - 'heat_generation_multiplier' => $data['heat_generation_multiplier'] ?? null, - 'sound_radius_multiplier' => $data['sound_radius_multiplier'] ?? null, - 'charge_time_multiplier' => $data['charge_time_multiplier'] ?? null, - - 'recoil_decay_multiplier' => $data['recoil_decay_multiplier'] ?? null, - 'recoil_end_decay_multiplier' => $data['recoil_end_decay_multiplier'] ?? null, - 'recoil_fire_recoil_time_multiplier' => $data['recoil_fire_recoil_time_multiplier'] ?? null, - 'recoil_fire_recoil_strength_first_multiplier' => $data['recoil_fire_recoil_strength_first_multiplier'] ?? null, - 'recoil_fire_recoil_strength_multiplier' => $data['recoil_fire_recoil_strength_multiplier'] ?? null, - 'recoil_angle_recoil_strength_multiplier' => $data['recoil_angle_recoil_strength_multiplier'] ?? null, - 'recoil_randomness_multiplier' => $data['recoil_randomness_multiplier'] ?? null, - 'recoil_randomness_back_push_multiplier' => $data['recoil_randomness_back_push_multiplier'] ?? null, - 'recoil_frontal_oscillation_rotation_multiplier' => $data['recoil_frontal_oscillation_rotation_multiplier'] ?? null, - 'recoil_frontal_oscillation_strength_multiplier' => $data['recoil_frontal_oscillation_strength_multiplier'] ?? null, - 'recoil_frontal_oscillation_decay_multiplier' => $data['recoil_frontal_oscillation_decay_multiplier'] ?? null, - 'recoil_frontal_oscillation_randomness_multiplier' => $data['recoil_frontal_oscillation_randomness_multiplier'] ?? null, - 'recoil_animated_recoil_multiplier' => $data['recoil_animated_recoil_multiplier'] ?? null, - - 'spread_min_multiplier' => $data['spread_min_multiplier'] ?? null, - 'spread_max_multiplier' => $data['spread_max_multiplier'] ?? null, - 'spread_first_attack_multiplier' => $data['spread_first_attack_multiplier'] ?? null, - 'spread_attack_multiplier' => $data['spread_attack_multiplier'] ?? null, - 'spread_decay_multiplier' => $data['spread_decay_multiplier'] ?? null, - 'spread_additive_modifier' => $data['spread_additive_modifier'] ?? null, - - 'aim_zoom_scale' => $data['aim_zoom_scale'] ?? null, - 'aim_zoom_time_scale' => $data['aim_zoom_time_scale'] ?? null, - - 'salvage_speed_multiplier' => $data['salvage_speed_multiplier'] ?? null, - 'salvage_radius_multiplier' => $data['salvage_radius_multiplier'] ?? null, - 'salvage_extraction_efficiency' => $data['salvage_extraction_efficiency'] ?? null, - ]); - } -} diff --git a/app/Jobs/SC/TranslateItem.php b/app/Jobs/SC/TranslateItem.php deleted file mode 100644 index 33f0405f4..000000000 --- a/app/Jobs/SC/TranslateItem.php +++ /dev/null @@ -1,77 +0,0 @@ -item = $item; - } - - /** - * Execute the job. - */ - public function handle(): void - { - app('Log')::info("Translating Item {$this->item->name}"); - $targetLocale = config('services.deepl.target_locale'); - - $english = optional($this->item->english())->translation; - $german = optional($this->item->german())->translation; - - // Delete job german and english translation length don't differ in length by <= 20% - if (empty($english) || ($german !== null && ((strlen($german) / strlen($english)) > 0.80))) { - $this->delete(); - - return; - } - - $translator = new TranslateText($english); - - try { - $translation = $translator->translate(config('services.deepl.target_locale')); - } catch (ConnectException|RateLimitedException $e) { - $this->release(60); - - return; - } catch (Exception $e) { - $this->fail($e); - - return; - } - - $this->item->translations()->updateOrCreate( - [ - 'locale_code' => sprintf('%s_%s', Str::lower($targetLocale), $targetLocale), - ], - [ - 'translation' => trim(TranslateText::runTextReplacements($translation)), - ] - ); - } -} diff --git a/app/Jobs/StarCitizen/AbstractRSIDownloadData.php b/app/Jobs/StarCitizen/AbstractRSIDownloadData.php deleted file mode 100644 index 2701a2f09..000000000 --- a/app/Jobs/StarCitizen/AbstractRSIDownloadData.php +++ /dev/null @@ -1,36 +0,0 @@ - 0]; - } - - if (($response->success ?? 0) !== 1) { - throw new InvalidDataException( - sprintf('RSI data is not valid. Expected success = 1, got %d', $response->success ?? 0) - ); - } - - return $response; - } -} diff --git a/app/Jobs/StarCitizen/Galactapedia/ImportArticle.php b/app/Jobs/StarCitizen/Galactapedia/ImportArticle.php index 666f749be..1af94cd72 100644 --- a/app/Jobs/StarCitizen/Galactapedia/ImportArticle.php +++ b/app/Jobs/StarCitizen/Galactapedia/ImportArticle.php @@ -4,48 +4,39 @@ namespace App\Jobs\StarCitizen\Galactapedia; -use App\Jobs\AbstractBaseDownloadData; use App\Models\StarCitizen\Galactapedia\Article; use App\Models\StarCitizen\Galactapedia\Category; use App\Models\StarCitizen\Galactapedia\Tag; use App\Models\StarCitizen\Galactapedia\Template; use App\Models\System\Language; -use App\Traits\CreateRelationChangelogTrait; -use Illuminate\Bus\Queueable; +use App\Services\RsiDownloadClient; +use Illuminate\Bus\Batchable; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; +use Illuminate\Foundation\Queue\Queueable; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; -class ImportArticle extends AbstractBaseDownloadData implements ShouldQueue +class ImportArticle implements ShouldQueue { - use CreateRelationChangelogTrait; - use Dispatchable; - use InteractsWithQueue; + use Batchable; use Queueable; - use SerializesModels; - private string $articleId; + public int $timeout = 120; private Article $article; - /** - * Create a new job instance. - */ - public function __construct(string $articleId) - { - $this->articleId = $articleId; - - app('Log')::info(sprintf('Importing Galactapedia Article "%s"', $articleId)); + public function __construct( + public readonly string $articleId, + ) { + Log::info(sprintf('Importing Galactapedia Article "%s"', $articleId)); } /** * Execute the job. */ - public function handle(): void + public function handle(RsiDownloadClient $client): void { - $result = $this->makeClient()->post('galactapedia/graphql', [ + $result = $client->forRsi()->post('galactapedia/graphql', [ 'query' => <<<'QUERY' query ArticleByID($query: ID!) { Article(id: $query) { @@ -107,22 +98,18 @@ public function handle(): void ] ); - $this->article->translations()->updateOrCreate( - [ - 'locale_code' => Language::ENGLISH, - ], - [ - 'translation' => Article::normalizeContent($data['body']), - ] - ); + $translation = Article::normalizeContent($data['body']); + + if ($translation !== '') { + $this->article->setTranslation('translation', Language::ENGLISH, $translation); + $this->article->save(); + } $changes = []; $changes['templates'] = $this->syncTemplates($data['template'] ?? []); $changes['categories'] = $this->syncCategories($data['categories'] ?? []); $changes['tags'] = $this->syncTags($data['tags'] ?? []); $changes['related_articles'] = $this->syncRelatedArticles($data['relatedArticles'] ?? []); - - $this->createRelationChangelog($changes, $this->article); } /** @@ -257,7 +244,7 @@ private function disableDuplicates(array $data): void } if ($article->cig_id !== $data['id']) { - app('Log')::info(sprintf( + Log::info(sprintf( 'Galactapedia Article "%s" (%s) is duplicate, disabling older one.', $article->cleanTitle, $article->cig_id, diff --git a/app/Jobs/StarCitizen/Galactapedia/ImportArticleProperty.php b/app/Jobs/StarCitizen/Galactapedia/ImportArticleProperty.php index be28f2884..efd6700e6 100644 --- a/app/Jobs/StarCitizen/Galactapedia/ImportArticleProperty.php +++ b/app/Jobs/StarCitizen/Galactapedia/ImportArticleProperty.php @@ -4,44 +4,47 @@ namespace App\Jobs\StarCitizen\Galactapedia; -use App\Jobs\AbstractBaseDownloadData; use App\Models\StarCitizen\Galactapedia\Article; -use Illuminate\Bus\Queueable; +use App\Services\RsiDownloadClient; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; +use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; -class ImportArticleProperty extends AbstractBaseDownloadData implements ShouldQueue +class ImportArticleProperty implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; use Queueable; - use SerializesModels; + + public int $timeout = 120; private Article $article; - /** - * Create a new job instance. - */ - public function __construct(Article $article) - { - $this->article = $article; - } + public function __construct( + public readonly int $articleId, + ) {} /** * Execute the job. */ - public function handle(): void + public function handle(RsiDownloadClient $client): void { + $article = Article::query() + ->with('templates') + ->find($this->articleId); + + if ($article === null) { + return; + } + + $this->article = $article; + if ($this->article->templates->isEmpty()) { - app('Log')::info(sprintf('Article "%s" has no Templates, skipping.', $this->article->title)); + Log::info(sprintf('Article "%s" has no Templates, skipping.', $this->article->title)); return; } - $fields = $this->getTemplateFields(); + $fields = $this->getTemplateFields($client); if ($fields === null) { $this->delete(); @@ -51,7 +54,7 @@ public function handle(): void $strFields = implode("\n", $fields->toArray()); - $result = $this->makeClient()->post('galactapedia/graphql', [ + $result = $client->forRsi()->post('galactapedia/graphql', [ 'query' => <<article->cig_id}") { @@ -108,9 +111,9 @@ public function handle(): void }); } - private function getTemplateFields(): ?Collection + private function getTemplateFields(RsiDownloadClient $client): ?Collection { - $result = $this->makeClient()->post('galactapedia/graphql', [ + $result = $client->forRsi()->post('galactapedia/graphql', [ 'query' => <<<'QUERY' query ArticleAfterCursor($type: String!) { template: __type(name: $type) { diff --git a/app/Jobs/StarCitizen/Galactapedia/ImportArticles.php b/app/Jobs/StarCitizen/Galactapedia/ImportArticles.php index ceab3c406..63b433388 100644 --- a/app/Jobs/StarCitizen/Galactapedia/ImportArticles.php +++ b/app/Jobs/StarCitizen/Galactapedia/ImportArticles.php @@ -4,26 +4,22 @@ namespace App\Jobs\StarCitizen\Galactapedia; -use App\Jobs\AbstractBaseDownloadData; -use Illuminate\Bus\Queueable; +use App\Services\RsiDownloadClient; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; +use Illuminate\Foundation\Queue\Queueable; -class ImportArticles extends AbstractBaseDownloadData implements ShouldQueue +class ImportArticles implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; use Queueable; - use SerializesModels; + + public int $timeout = 120; /** * Execute the job. */ - public function handle(): void + public function handle(RsiDownloadClient $client): void { - $result = $this->makeClient()->post('galactapedia/graphql', [ + $result = $client->forRsi()->post('galactapedia/graphql', [ 'query' => <<<'QUERY' query GetArticles { allArticle { diff --git a/app/Jobs/StarCitizen/Galactapedia/ImportArticlesFromCategory.php b/app/Jobs/StarCitizen/Galactapedia/ImportArticlesFromCategory.php index 1312b94d0..17efe1107 100644 --- a/app/Jobs/StarCitizen/Galactapedia/ImportArticlesFromCategory.php +++ b/app/Jobs/StarCitizen/Galactapedia/ImportArticlesFromCategory.php @@ -4,37 +4,27 @@ namespace App\Jobs\StarCitizen\Galactapedia; -use App\Jobs\AbstractBaseDownloadData; use App\Models\StarCitizen\Galactapedia\Category; -use Illuminate\Bus\Queueable; +use App\Services\RsiDownloadClient; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; +use Illuminate\Foundation\Queue\Queueable; -class ImportArticlesFromCategory extends AbstractBaseDownloadData implements ShouldQueue +class ImportArticlesFromCategory implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; use Queueable; - use SerializesModels; - private Category $category; + public int $timeout = 120; - /** - * Create a new job instance. - */ - public function __construct(Category $category) - { - $this->category = $category; - } + public function __construct( + public readonly Category $category, + ) {} /** * Execute the job. */ - public function handle(): void + public function handle(RsiDownloadClient $client): void { - $result = $this->makeClient()->post('galactapedia/graphql', [ + $result = $client->forRsi()->post('galactapedia/graphql', [ 'query' => <<<'QUERY' query ArticleByCategory($query: String) { allArticle(where: {categories: {contains: $query}}) { diff --git a/app/Jobs/StarCitizen/Galactapedia/ImportCategories.php b/app/Jobs/StarCitizen/Galactapedia/ImportCategories.php index 597224b30..c8c255d8f 100644 --- a/app/Jobs/StarCitizen/Galactapedia/ImportCategories.php +++ b/app/Jobs/StarCitizen/Galactapedia/ImportCategories.php @@ -4,30 +4,27 @@ namespace App\Jobs\StarCitizen\Galactapedia; -use App\Jobs\AbstractBaseDownloadData; use App\Models\StarCitizen\Galactapedia\Category; -use Illuminate\Bus\Queueable; +use App\Services\RsiDownloadClient; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; +use Illuminate\Foundation\Queue\Queueable; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; -class ImportCategories extends AbstractBaseDownloadData implements ShouldQueue +class ImportCategories implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; use Queueable; - use SerializesModels; + + public int $timeout = 120; /** * Execute the job. */ - public function handle(): void + public function handle(RsiDownloadClient $client): void { - app('Log')::info('Importing Galactapedia categories.'); + Log::info('Importing Galactapedia categories.'); - $result = $this->makeClient()->post('galactapedia/graphql', [ + $result = $client->forRsi()->post('galactapedia/graphql', [ 'query' => <<<'QUERY' query GetCategories { allCategory { diff --git a/app/Jobs/StarCitizen/Galactapedia/Sync/DispatchArticleProperties.php b/app/Jobs/StarCitizen/Galactapedia/Sync/DispatchArticleProperties.php new file mode 100644 index 000000000..69ad84ca5 --- /dev/null +++ b/app/Jobs/StarCitizen/Galactapedia/Sync/DispatchArticleProperties.php @@ -0,0 +1,34 @@ +select('id') + ->chunkById(self::CHUNK_SIZE, function (Collection $articles): void { + $articles->each(function (Article $article): void { + ImportArticleProperty::dispatch($article->id); + }); + }); + } +} diff --git a/app/Jobs/StarCitizen/Galactapedia/Sync/SyncGalactapedia.php b/app/Jobs/StarCitizen/Galactapedia/Sync/SyncGalactapedia.php new file mode 100644 index 000000000..c39199bdd --- /dev/null +++ b/app/Jobs/StarCitizen/Galactapedia/Sync/SyncGalactapedia.php @@ -0,0 +1,68 @@ +forRsi()->post('galactapedia/graphql', [ + 'query' => <<<'QUERY' +query GetArticles { + allArticle { + edges { + node { + id + title + slug + } + } + } +} +QUERY, + ]); + + $result = $result->json() ?? []; + + if (! isset($result['data']['allArticle']['edges'])) { + return; + } + + $jobs = collect($result['data']['allArticle']['edges']) + ->map(function (array $edge) { + return $edge['node'] ?? null; + }) + ->filter() + ->map(function (array $node) { + return new ImportArticle($node['id']); + }); + + if ($jobs->isEmpty()) { + return; + } + + Bus::batch($jobs->values()) + ->then(function () { + DispatchArticleProperties::dispatch(); + }) + ->dispatch(); + } +} diff --git a/app/Jobs/StarCitizen/Galactapedia/TranslateArticle.php b/app/Jobs/StarCitizen/Galactapedia/TranslateArticle.php index 7c9749f19..8e96eab0b 100644 --- a/app/Jobs/StarCitizen/Galactapedia/TranslateArticle.php +++ b/app/Jobs/StarCitizen/Galactapedia/TranslateArticle.php @@ -4,17 +4,17 @@ namespace App\Jobs\StarCitizen\Galactapedia; +use App\Exceptions\Translation\QuotaExceededException; +use App\Exceptions\Translation\RateLimitException; +use App\Exceptions\Translation\TranslationException; use App\Models\StarCitizen\Galactapedia\Article; -use App\Services\TranslateText; -use Exception; -use GuzzleHttp\Exception\ConnectException; +use App\Models\System\Language; +use App\Services\Translation\TranslationService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Str; -use Octfx\DeepLy\Exceptions\RateLimitedException; class TranslateArticle implements ShouldQueue { @@ -36,13 +36,17 @@ public function __construct(Article $article) /** * Execute the job. */ - public function handle(): void + public function handle(TranslationService $translator): void { app('Log')::info("Translating Galactapedia Article {$this->article->cig_id}"); - $targetLocale = config('services.deepl.target_locale'); + $targetLocale = config('services.deepl.target_locale', 'de'); - $english = $this->article->english()->translation; - $german = optional($this->article->german())->translation; + $english = $this->article->getTranslation('translation', Language::ENGLISH, false); + $german = $this->article->getTranslation('translation', Language::GERMAN, false); + + if ($english === null || $english === '') { + return; + } // Delete job german and english translation length don't differ in length by <= 20% if ($german !== null && ((strlen($german) / strlen($english)) > 0.80)) { @@ -51,28 +55,23 @@ public function handle(): void return; } - $translator = new TranslateText($english); - try { - $translation = $translator->translate(config('services.deepl.target_locale')); - } catch (ConnectException|RateLimitedException $e) { + $translation = $translator->translate($english, $targetLocale); + } catch (RateLimitException $e) { $this->release(60); return; - } catch (Exception $e) { + } catch (QuotaExceededException $e) { + $this->fail($e); + + return; + } catch (TranslationException $e) { $this->fail($e); return; } - $this->article->translations()->updateOrCreate( - [ - 'locale_code' => sprintf('%s_%s', Str::lower($targetLocale), $targetLocale), - ], - [ - 'translation' => trim(TranslateText::runTextReplacements($translation)), - 'proofread' => false, - ] - ); + $this->article->setTranslation('translation', Language::GERMAN, $translation); + $this->article->save(); } } diff --git a/app/Jobs/StarCitizen/Starmap/Download/DownloadStarmap.php b/app/Jobs/StarCitizen/Starmap/Download/DownloadStarmap.php deleted file mode 100644 index e5a7e1df3..000000000 --- a/app/Jobs/StarCitizen/Starmap/Download/DownloadStarmap.php +++ /dev/null @@ -1,169 +0,0 @@ -force = $force; - $this->timestamp = now()->format('Y-m-d'); - } - - /** - * Execute the job. - */ - public function handle(): void - { - app('Log')::info('Starting Starmap Download'); - - if ($this->force || ! Storage::disk(self::STARSYSTEM_DISK)->exists($this->timestamp)) { - $this->downloadBootup(); - $this->writeBootupDataToDisk(); - - $this->dispatchStarSystemJobs(); - } - } - - /** - * Download the bootup data - */ - private function downloadBootup(): void - { - try { - $this->response = $this->makeClient()->post(self::STARSYSTEM_BOOTUP_ENDPOINT)->throw(); - } catch (RequestException $e) { - app('Log')::error( - 'Could not connect to RSI Starmap Bootup', - [ - 'message' => $e->getMessage(), - ] - ); - - $this->release(300); - } - } - - /** - * Write bootup data to star systems disk - */ - private function writeBootupDataToDisk(): void - { - try { - $bootupData = json_decode( - $this->response->body(), - true, - 512, - JSON_THROW_ON_ERROR - ); - } catch (JsonException $e) { - $this->fail($e); - - return; - } - - $this->checkBootupStructure($bootupData); - - try { - Storage::disk(self::STARSYSTEM_DISK)->put( - sprintf('%s/%s', $this->timestamp, self::STARMAP_BOOTUP_FILENAME), - json_encode($bootupData, JSON_THROW_ON_ERROR) - ); - } catch (JsonException $e) { - $this->fail($e); - } - } - - private function checkBootupStructure(array $bootupData): void - { - if (! $this->checkDataStructureIsValid($bootupData, static::BOOTUP_CHECKLIST)) { - app('Log')::error('Can not read Star-Systems from RSI'); - - $this->fail('Can not read Star-Systems from RSI'); - - return; - } - - $this->systems = collect($bootupData['data']['systems']['resultset']); - } - - /** - * Download each star system - */ - private function dispatchStarSystemJobs(): void - { - $this->systems - ->each( - function (array $system) { - DownloadStarsystem::dispatch($system['code'], $this->timestamp, new Collection($system)); - } - ); - } -} diff --git a/app/Jobs/StarCitizen/Starmap/Download/DownloadStarsystem.php b/app/Jobs/StarCitizen/Starmap/Download/DownloadStarsystem.php index 4361ca5ad..1d0ee65b1 100644 --- a/app/Jobs/StarCitizen/Starmap/Download/DownloadStarsystem.php +++ b/app/Jobs/StarCitizen/Starmap/Download/DownloadStarsystem.php @@ -4,88 +4,96 @@ namespace App\Jobs\StarCitizen\Starmap\Download; -use App\Jobs\StarCitizen\AbstractRSIDownloadData; -use Illuminate\Bus\Queueable; +use App\Jobs\StarCitizen\Starmap\Import\ImportStarsystem; +use App\Services\RsiDownloadClient; +use Illuminate\Bus\Batchable; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Http\Client\RequestException; +use Illuminate\Foundation\Queue\Queueable; use Illuminate\Http\Client\Response; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use JsonException; -class DownloadStarsystem extends AbstractRSIDownloadData implements ShouldQueue +class DownloadStarsystem implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; + use Batchable; use Queueable; - use SerializesModels; private const STARSYSTEM_ENDPOINT = '/api/starmap/star-systems/'; private const STRUCTURE_CHECKLIST = ['data', 'resultset', 0, 'celestial_objects', 0]; - private string $systemCode; + private const STARSYSTEM_DISK = 'starmap'; - private array $systemData; - - private string $folder; + public int $timeout = 120; - private ?Collection $bootupData; + private array $systemData; private Response $response; - /** - * Create a new job instance. - */ - public function __construct(string $systemCode, string $folder, ?Collection $bootupData = null) - { - $this->systemCode = $systemCode; - $this->folder = $folder; - $this->bootupData = $bootupData; - $this->makeClient(); - } + public function __construct( + public readonly string $systemCode, + public readonly string $folder, + public readonly ?Collection $bootupData = null, + ) {} /** * Execute the job. */ - public function handle(): void + public function handle(RsiDownloadClient $client): void { - $this->downloadStarSystem(); - $this->checkStructure(); - $this->saveSystemToDisk(); + $this->downloadStarSystem($client); + + if (! isset($this->response)) { + return; + } + + if (! $this->checkStructure()) { + return; + } + + $system = $this->buildSystemPayload(); + + $this->saveSystemToDisk($system); + + ImportStarsystem::dispatch($system); } /** - * Downloads the star system and saves the response + * Downloads the star system and saves the response. */ - private function downloadStarSystem(): void + private function downloadStarSystem(RsiDownloadClient $client): void { - try { - $this->response = $this->makeClient() - ->post(sprintf('%s%s', self::STARSYSTEM_ENDPOINT, $this->systemCode)) - ->throw(); - } catch (RequestException $e) { - app('Log')::error( - sprintf('Could not connect to RSI Starmap %s', $this->systemCode), - [ - 'message' => $e->getMessage(), - ] - ); + $response = $client->forRsi() + ->post(sprintf('%s%s', self::STARSYSTEM_ENDPOINT, $this->systemCode)); + + if ($response->serverError()) { + Log::error(sprintf('Could not connect to RSI Starmap %s', $this->systemCode), [ + 'status' => $response->status(), + ]); $this->release(300); return; } + + if ($response->clientError()) { + Log::warning(sprintf('Starmap request failed for %s', $this->systemCode), [ + 'status' => $response->status(), + ]); + + return; + } + + $this->response = $response; } /** - * Checks the downloaded structure + * Checks the downloaded structure. */ - private function checkStructure(): void + private function checkStructure(): bool { try { $this->systemData = json_decode( @@ -95,34 +103,64 @@ private function checkStructure(): void JSON_THROW_ON_ERROR ); } catch (JsonException $e) { - app('Log')::error(sprintf('Can\'t decode %s.', $this->systemCode)); + Log::error(sprintf('Can\'t decode %s.', $this->systemCode)); $this->fail($e); + + return false; } - if (! $this->checkDataStructureIsValid($this->systemData, static::STRUCTURE_CHECKLIST)) { + if (! $this->validateDataStructure($this->systemData, self::STRUCTURE_CHECKLIST)) { $this->fail('Starsystem data can\'t be processed.'); + + return false; } + + return true; } /** - * Writes the system json to disk + * Validates data structure. */ - private function saveSystemToDisk(): void + private function validateDataStructure(array $data, array $keys): bool + { + if (! isset($data['success']) || $data['success'] !== 1) { + return false; + } + + foreach ($keys as $key) { + if (! array_key_exists($key, $data)) { + return false; + } + $data = $data[$key]; + } + + return true; + } + + private function buildSystemPayload(): array { $system = $this->systemData['data']['resultset'][0]; if ($this->bootupData !== null) { - $system = $this->bootupData->merge($system); + $system = $this->bootupData->merge($system)->toArray(); } + return $system; + } + + /** + * Writes the system json to disk. + */ + private function saveSystemToDisk(array $system): void + { try { - Storage::disk(DownloadStarmap::STARSYSTEM_DISK)->put( + Storage::disk(self::STARSYSTEM_DISK)->put( sprintf('%s/%s_system.json', $this->folder, Str::slug($this->systemCode)), json_encode($system, JSON_THROW_ON_ERROR) ); } catch (JsonException $e) { - app('Log')::error(sprintf('Can\'t encode %s to json.', $this->systemCode)); + Log::error(sprintf('Can\'t encode %s to json.', $this->systemCode)); $this->fail($e); } diff --git a/app/Jobs/StarCitizen/Starmap/Import/ImportCelestialObject.php b/app/Jobs/StarCitizen/Starmap/Import/ImportCelestialObject.php index 9cf3b2d10..09a799cfd 100644 --- a/app/Jobs/StarCitizen/Starmap/Import/ImportCelestialObject.php +++ b/app/Jobs/StarCitizen/Starmap/Import/ImportCelestialObject.php @@ -4,15 +4,16 @@ namespace App\Jobs\StarCitizen\Starmap\Import; -use App\Models\StarCitizen\Starmap\CelestialObject\CelestialObject as CelestialObjectModel; +use App\Models\StarCitizen\Starmap\Affiliation; +use App\Models\StarCitizen\Starmap\CelestialObject as CelestialObjectModel; +use App\Models\StarCitizen\Starmap\CelestialObjectSubtype; use App\Models\System\Language; -use App\Services\Parser\Starmap\Affiliation; -use App\Services\Parser\Starmap\CelestialSubtype; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; /** @@ -48,10 +49,6 @@ public function __construct($rawData, int $starsystemId) */ public function handle(): void { - if (empty($this->rawData['subtype'])) { - app('Log')::debug('Parse Celestial Object: empty=true'); - } - $data = $this->getData(); $description = $data->pull('description'); @@ -64,16 +61,9 @@ public function handle(): void $data->toArray() ); - if ($description !== null) { - $celestialObject->translations()->updateOrCreate( - [ - 'celestial_object_id' => $celestialObject->id, - 'locale_code' => Language::ENGLISH, - ], - [ - 'translation' => $description, - ] - ); + if ($description !== null && $description !== '') { + $celestialObject->setTranslation('translation', Language::ENGLISH, $description); + $celestialObject->save(); } $celestialObject->affiliation()->sync($this->getAffiliationIds($this->rawData->pull('affiliation'))); @@ -120,9 +110,16 @@ public function getData(): Collection */ private function getCelestialSubtypeId() { - $parser = new CelestialSubtype($this->rawData['subtype']); + if (empty(Arr::get($this->rawData, 'subtype.id'))) { + return null; + } - return optional($parser->getCelestialSubtype())->id; + return CelestialObjectSubtype::query()->updateOrCreate([ + 'id' => Arr::get($this->rawData, 'subtype.id'), + ], [ + 'name' => Arr::get($this->rawData, 'subtype.name'), + 'type' => Arr::get($this->rawData, 'subtype.type'), + ])->id; } private function getAffiliationIds(array $affiliations): array @@ -135,11 +132,16 @@ function ($affiliation) { ) ->map( function ($affiliationData) { - return (new Affiliation($affiliationData))->getAffiliation(); + return Affiliation::query()->updateOrCreate(['cig_id' => $affiliationData['id']], [ + 'name' => Arr::get($affiliationData, 'name'), + 'code' => Arr::get($affiliationData, 'code'), + 'color' => Arr::get($affiliationData, 'color'), + 'membership_id' => Arr::get($affiliationData, 'membership.id', null), + ]); } ) ->map( - function (\App\Models\StarCitizen\Starmap\Affiliation $affiliation) { + function (Affiliation $affiliation) { return $affiliation->id; } ) diff --git a/app/Jobs/StarCitizen/Starmap/Import/ImportJumppoint.php b/app/Jobs/StarCitizen/Starmap/Import/ImportJumppoint.php index 75963bb68..e53a87cdf 100644 --- a/app/Jobs/StarCitizen/Starmap/Import/ImportJumppoint.php +++ b/app/Jobs/StarCitizen/Starmap/Import/ImportJumppoint.php @@ -4,7 +4,7 @@ namespace App\Jobs\StarCitizen\Starmap\Import; -use App\Models\StarCitizen\Starmap\Jumppoint\Jumppoint; +use App\Models\StarCitizen\Starmap\Jumppoint; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; diff --git a/app/Jobs/StarCitizen/Starmap/Import/ImportStarmap.php b/app/Jobs/StarCitizen/Starmap/Import/ImportStarmap.php deleted file mode 100644 index c42982467..000000000 --- a/app/Jobs/StarCitizen/Starmap/Import/ImportStarmap.php +++ /dev/null @@ -1,150 +0,0 @@ -timestamp = $timestamp; - } - - /** - * Execute the job. - */ - public function handle(): void - { - if ($this->timestamp === null) { - $this->starmapFolder = $this->getNewestStarmapFolder(); - } else { - $this->starmapFolder = Storage::disk(self::STARSYSTEM_DISK)->path($this->timestamp); - } - - app('Log')::info('Parsing Starmap Download', [$this->starmapFolder]); - - $this->dispatchStarsystemJobs(); - $this->dispatchJumppointJobs(); - } - - private function getNewestStarmapFolder(): string - { - $diskPath = Storage::disk(self::STARSYSTEM_DISK)->path(''); - - $folders = collect(scandir($diskPath, SCANDIR_SORT_DESCENDING))->reject( - function ($folder) { - return Str::startsWith($folder, '.'); - } - )->toArray(); - - if ($folders === false || empty($folders)) { - throw new RuntimeException(sprintf('%s is not a directory.', $diskPath)); - } - - return Storage::disk(self::STARSYSTEM_DISK)->path(array_shift($folders)); - } - - private function dispatchStarsystemJobs(): void - { - $files = scandir($this->starmapFolder); - - if (empty($files)) { - app('Log')::error('Starmap disk is empty'); - - $this->fail('Starmap disk is empty'); - } - - collect($files)->filter( - function (string $path) { - return Str::contains($path, 'system'); - } - ) - ->map( - function (string $systemPath) { - try { - return File::get(sprintf('%s/%s', $this->starmapFolder, $systemPath)); - } catch (FileNotFoundException $e) { - $this->fail($e); - - return ''; - } - } - ) - ->filter( - function ($systemData) { - // Should not happen - return ! empty($systemData) && $systemData !== ''; - } - ) - ->map( - function (string $systemData) { - return json_decode($systemData, true, 512, JSON_THROW_ON_ERROR); - } - )->each( - function (array $system) { - ImportStarsystem::dispatch($system); - } - ); - } - - private function dispatchJumppointJobs(): void - { - try { - $bootupData = File::get(sprintf('%s/%s', $this->starmapFolder, DownloadStarmap::STARMAP_BOOTUP_FILENAME)); - } catch (FileNotFoundException $e) { - $this->fail($e); - } - - try { - $bootupData = json_decode($bootupData, true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { - $this->fail($e); - } - - if (! $this->checkDataStructureIsValid($bootupData, ['data', 'tunnels', 'resultset', 0])) { - $this->fail('Bootup tunnel data not valid.'); - } - - collect($bootupData['data']['tunnels']['resultset'])->each( - function ($tunnel) { - ImportJumppoint::dispatch($tunnel); - } - ); - } -} diff --git a/app/Jobs/StarCitizen/Starmap/Import/ImportStarsystem.php b/app/Jobs/StarCitizen/Starmap/Import/ImportStarsystem.php index 988b0a586..b072827f3 100644 --- a/app/Jobs/StarCitizen/Starmap/Import/ImportStarsystem.php +++ b/app/Jobs/StarCitizen/Starmap/Import/ImportStarsystem.php @@ -4,14 +4,15 @@ namespace App\Jobs\StarCitizen\Starmap\Import; -use App\Models\StarCitizen\Starmap\Starsystem\Starsystem; +use App\Models\StarCitizen\Starmap\Affiliation; +use App\Models\StarCitizen\Starmap\Starsystem; use App\Models\System\Language; -use App\Services\Parser\Starmap\Affiliation; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; /** @@ -53,15 +54,10 @@ public function handle(): void $systemData->toArray() ); - $starsystem->translations()->updateOrCreate( - [ - 'starsystem_id' => $starsystem->id, - 'locale_code' => Language::ENGLISH, - ], - [ - 'translation' => $description, - ] - ); + if ($description !== null && $description !== '') { + $starsystem->setTranslation('translation', Language::ENGLISH, $description); + $starsystem->save(); + } $starsystem->affiliation()->sync($this->getAffiliationIds($affiliation)); @@ -113,11 +109,16 @@ function ($affiliation) { ) ->map( function ($affiliationData) { - return (new Affiliation($affiliationData))->getAffiliation(); + return Affiliation::query()->updateOrCreate(['cig_id' => $affiliationData['id']], [ + 'name' => Arr::get($affiliationData, 'name'), + 'code' => Arr::get($affiliationData, 'code'), + 'color' => Arr::get($affiliationData, 'color'), + 'membership_id' => Arr::get($affiliationData, 'membership.id', null), + ]); } ) ->map( - function (\App\Models\StarCitizen\Starmap\Affiliation $affiliation) { + function (Affiliation $affiliation) { return $affiliation->id; } ) diff --git a/app/Jobs/StarCitizen/Starmap/Sync/SyncStarmap.php b/app/Jobs/StarCitizen/Starmap/Sync/SyncStarmap.php new file mode 100644 index 000000000..544348a84 --- /dev/null +++ b/app/Jobs/StarCitizen/Starmap/Sync/SyncStarmap.php @@ -0,0 +1,218 @@ +timestamp = now()->format('Y-m-d'); + } + + /** + * Execute the job. + */ + public function handle(RsiDownloadClient $client): void + { + $bootupData = $this->loadBootupFromDisk(); + + if ($bootupData === null) { + $this->downloadBootup($client); + + if (! isset($this->response)) { + return; + } + + $bootupData = $this->decodeBootup(); + + if ($bootupData === null) { + return; + } + + $this->writeBootupDataToDisk($bootupData); + } + + $this->dispatchJumppointJobs(); + $this->dispatchStarsystemJobs(); + } + + /** + * Download the bootup data. + */ + private function downloadBootup(RsiDownloadClient $client): void + { + $response = $client->forRsi()->post(self::STARSYSTEM_BOOTUP_ENDPOINT); + + if ($response->serverError()) { + Log::error('Could not connect to RSI Starmap Bootup', [ + 'status' => $response->status(), + ]); + + $this->release(300); + + return; + } + + if ($response->clientError()) { + Log::warning('Starmap bootup request failed', [ + 'status' => $response->status(), + ]); + + return; + } + + $this->response = $response; + } + + private function decodeBootup(): ?array + { + try { + $bootupData = json_decode( + $this->response->body(), + true, + 512, + JSON_THROW_ON_ERROR + ); + } catch (JsonException $e) { + $this->fail($e); + + return null; + } + + return $this->parseBootupData($bootupData); + } + + private function parseBootupData(array $bootupData): ?array + { + if (Arr::get($bootupData, 'data.systems.resultset.0') === null) { + $this->fail('Can not read Star-Systems from RSI'); + + return null; + } + + if (Arr::get($bootupData, 'data.tunnels.resultset.0') === null) { + $this->fail('Bootup tunnel data not valid.'); + + return null; + } + + $this->systems = collect($bootupData['data']['systems']['resultset']); + $this->tunnels = collect($bootupData['data']['tunnels']['resultset']); + + return $bootupData; + } + + private function loadBootupFromDisk(): ?array + { + $bootupPath = sprintf('%s/%s', $this->timestamp, self::STARMAP_BOOTUP_FILENAME); + + if (! Storage::disk(self::STARSYSTEM_DISK)->exists($bootupPath)) { + return null; + } + + $payload = Storage::disk(self::STARSYSTEM_DISK)->get($bootupPath); + + try { + $bootupData = json_decode($payload, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + $this->fail($e); + + return null; + } + + return $this->parseBootupData($bootupData); + } + + private function writeBootupDataToDisk(array $bootupData): void + { + Storage::disk(self::STARSYSTEM_DISK)->makeDirectory($this->timestamp); + + try { + Storage::disk(self::STARSYSTEM_DISK)->put( + sprintf('%s/%s', $this->timestamp, self::STARMAP_BOOTUP_FILENAME), + json_encode($bootupData, JSON_THROW_ON_ERROR) + ); + } catch (JsonException $e) { + $this->fail($e); + } + } + + private function dispatchJumppointJobs(): void + { + $this->tunnels->each( + function (array $tunnel): void { + ImportJumppoint::dispatch($tunnel); + } + ); + } + + /** + * Download each star system. + */ + private function dispatchStarsystemJobs(): void + { + $jobs = $this->systems + ->reject(fn (array $system): bool => $this->hasStarsystemData($system['code'])) + ->map( + function (array $system): DownloadStarsystem { + return new DownloadStarsystem($system['code'], $this->timestamp, new Collection($system)); + } + ); + + if ($jobs->isEmpty()) { + return; + } + + Bus::batch($jobs)->dispatch(); + } + + private function hasStarsystemData(string $systemCode): bool + { + $path = sprintf('%s/%s_system.json', $this->timestamp, Str::slug($systemCode)); + + return Storage::disk(self::STARSYSTEM_DISK)->exists($path); + } +} diff --git a/app/Jobs/StarCitizen/Starmap/Translate/TranslateSystems.php b/app/Jobs/StarCitizen/Starmap/Translate/TranslateSystems.php index 4f6c54cbd..9f4ac4bc7 100644 --- a/app/Jobs/StarCitizen/Starmap/Translate/TranslateSystems.php +++ b/app/Jobs/StarCitizen/Starmap/Translate/TranslateSystems.php @@ -4,22 +4,19 @@ namespace App\Jobs\StarCitizen\Starmap\Translate; -use App\Models\StarCitizen\Starmap\Starsystem\Starsystem; +use App\Exceptions\Translation\AuthenticationException; +use App\Exceptions\Translation\QuotaExceededException; +use App\Exceptions\Translation\RateLimitException; +use App\Exceptions\Translation\TranslationException; +use App\Models\StarCitizen\Starmap\Starsystem; use App\Models\System\Language; +use App\Services\Translation\TranslationService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Collection; -use InvalidArgumentException; -use Octfx\DeepLy\Exceptions\AuthenticationException; -use Octfx\DeepLy\Exceptions\QuotaException; -use Octfx\DeepLy\Exceptions\RateLimitedException; -use Octfx\DeepLy\Exceptions\TextLengthException; -use Octfx\DeepLy\HttpClient\CallException; -use Octfx\DeepLy\Integrations\Laravel\DeepLyFacade; /** * Translate all systems @@ -34,67 +31,65 @@ class TranslateSystems implements ShouldQueue /** * Execute the job. */ - public function handle(): void + public function handle(TranslationService $translator): void { app('Log')::info('Translating Systems'); - Starsystem::query()->whereHas( - 'translations', - function (Builder $query) { - $query->where('locale_code', Language::ENGLISH)->whereRaw("translation <> ''"); - } - ) + $targetLocale = config('services.deepl.target_locale', 'de'); + + Starsystem::query() + ->whereNotNull('translation') ->chunk( 25, - function (Collection $systems) { + function (Collection $systems) use ($translator, $targetLocale) { $systems->each( - function (Starsystem $starsystem) { - if (optional($starsystem->german())->translation !== null) { + function (Starsystem $starsystem) use ($translator, $targetLocale) { + $english = $starsystem->getTranslation('translation', Language::ENGLISH, false); + $german = $starsystem->getTranslation('translation', Language::GERMAN, false); + + if ($english === null || $english === '') { + return; + } + + if ($german !== null && $german !== '') { return; } try { app('Log')::info(sprintf('Translating system %s', $starsystem->name)); - $translation = DeepLyFacade::translate( - $starsystem->english()->translation, - config('services.deepl.target_locale'), - 'EN', - 'more' + $translation = $translator->translate( + $english, + $targetLocale ); - } catch (QuotaException $e) { - app('Log')::warning('Deepl Quote exceeded!'); + } catch (QuotaExceededException $e) { + app('Log')::warning('DeepL quota exceeded'); $this->fail($e); return; - } catch (RateLimitedException $e) { + } catch (RateLimitException $e) { app('Log')::info('Got rate limit exception. Trying job again in 60 seconds.'); $this->release(60); return; - } catch (TextLengthException $e) { - app('Log')::warning($e->getMessage()); - - return; - } catch (CallException|AuthenticationException|InvalidArgumentException $e) { - app('Log')::warning( - sprintf('%s: %s', 'Translation failed with Message', $e->getMessage()) - ); + } catch (AuthenticationException $e) { + app('Log')::error('DeepL authentication failed', ['error' => $e->getMessage()]); $this->fail($e); + return; + } catch (TranslationException $e) { + app('Log')::warning('Translation failed', [ + 'system' => $starsystem->name, + 'error' => $e->getMessage(), + ]); + return; } - $starsystem->translations()->updateOrCreate( - [ - 'locale_code' => 'de_DE', - ], - [ - 'translation' => trim($translation), - ] - ); + $starsystem->setTranslation('translation', Language::GERMAN, $translation); + $starsystem->save(); } ); } diff --git a/app/Jobs/StarCitizen/Stat/DownloadStats.php b/app/Jobs/StarCitizen/Stat/DownloadStats.php index 47b52d279..14b0bf635 100644 --- a/app/Jobs/StarCitizen/Stat/DownloadStats.php +++ b/app/Jobs/StarCitizen/Stat/DownloadStats.php @@ -4,99 +4,111 @@ namespace App\Jobs\StarCitizen\Stat; -use App\Exceptions\InvalidDataException; -use App\Jobs\StarCitizen\AbstractRSIDownloadData as RSIDownloadData; -use Illuminate\Bus\Queueable; +use App\Services\RsiDownloadClient; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Http\Client\RequestException; +use Illuminate\Foundation\Queue\Queueable; use Illuminate\Http\Client\Response; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use JsonException; +use RuntimeException; +use stdClass; -/** - * Class DownloadStats - */ -class DownloadStats extends RSIDownloadData implements ShouldQueue +class DownloadStats implements ShouldQueue { - use Dispatchable; - use InteractsWithQueue; use Queueable; - use SerializesModels; private const STATS_ENDPOINT = '/api/stats/getCrowdfundStats'; private const STATS_DISK = 'stats'; - private bool $force = false; + public int $timeout = 120; - /** - * DownloadShipMatrix constructor. - * - * @param bool $force Set to true do force download even if file already exists - */ - public function __construct($force = false) - { - $this->force = $force; + private string $statFileName; + + private int $year; + + public function __construct( + ?string $statFileName = null, + ?int $year = null, + public readonly bool $force = false, + ) { + $this->statFileName = $statFileName ?? sprintf('stats_%s.json', now()->format('Y-m-d')); + $this->year = $year ?? $this->inferYear($this->statFileName); } /** * Execute the job. - * - * @throws JsonException */ - public function handle(): void + public function handle(RsiDownloadClient $client): void { - app('Log')::info('Starting Stats Download Job.'); + Log::info('Starting Stats Download Job.'); - $path = sprintf('%d/stats_%s.json', now()->year, now()->format('Y-m-d')); + $path = sprintf('%d/%s', $this->year, $this->statFileName); if (! $this->force && Storage::disk(self::STATS_DISK)->exists($path)) { return; } - try { - $response = $this->makeClient() - ->asForm() - ->post( - self::STATS_ENDPOINT, - [ - 'fans' => true, - 'fleet' => true, - 'funds' => true, - ] - )->throw(); - } catch (RequestException $e) { - app('Log')::critical( - 'Could not connect to RSI Stats Endpoint', + $response = $client->forRsi() + ->asForm() + ->post( + self::STATS_ENDPOINT, [ - 'message' => $e->getMessage(), + 'fans' => true, + 'fleet' => true, + 'funds' => true, ] ); - $this->fail($e); + if ($response->serverError()) { + Log::critical('Could not connect to RSI Stats Endpoint', [ + 'status' => $response->status(), + ]); + + $this->release(300); + + return; + } + + if ($response->clientError()) { + Log::warning('Stats request failed with client error', [ + 'status' => $response->status(), + ]); return; } $this->saveStats($response, $path); - app('Log')::info('Stat Download finished'); + Log::info('Stat Download finished'); } - private function saveStats(Response $response, string $path): void + private function validateRsiResponse(string $body): stdClass { try { - $response = $this->parseResponseBody($response->body()); - } catch (InvalidDataException $e) { - app('Log')::error( - 'Stats data is not valid json', - [ - 'message' => $e->getMessage(), - ] + $response = json_decode($body, false, 512, JSON_THROW_ON_ERROR); + } catch (JsonException) { + throw new RuntimeException('Invalid JSON response from RSI'); + } + + if (($response->success ?? 0) !== 1) { + throw new RuntimeException( + sprintf('RSI API returned failure. Expected success=1, got %d', $response->success ?? 0) ); + } + + return $response; + } + + private function saveStats(Response $response, string $path): void + { + try { + $validated = $this->validateRsiResponse($response->body()); + } catch (RuntimeException $e) { + Log::error('Stats data is not valid', [ + 'message' => $e->getMessage(), + ]); $this->fail($e); @@ -105,7 +117,16 @@ private function saveStats(Response $response, string $path): void Storage::disk(self::STATS_DISK)->put( $path, - json_encode($response->data, JSON_THROW_ON_ERROR) + json_encode($validated->data, JSON_THROW_ON_ERROR) ); } + + private function inferYear(string $statFileName): int + { + if (preg_match('/^stats_(\d{4})-\d{2}-\d{2}\.json$/', $statFileName, $matches) === 1) { + return (int) $matches[1]; + } + + return now()->year; + } } diff --git a/app/Jobs/StarCitizen/Stat/Import/ImportStat.php b/app/Jobs/StarCitizen/Stat/ImportStat.php similarity index 76% rename from app/Jobs/StarCitizen/Stat/Import/ImportStat.php rename to app/Jobs/StarCitizen/Stat/ImportStat.php index 1a7095d97..8a4697789 100644 --- a/app/Jobs/StarCitizen/Stat/Import/ImportStat.php +++ b/app/Jobs/StarCitizen/Stat/ImportStat.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\Jobs\StarCitizen\Stat\Import; +namespace App\Jobs\StarCitizen\Stat; -use App\Models\StarCitizen\Stat\Stat; +use App\Models\StarCitizen\Stat; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Contracts\Queue\ShouldQueue; @@ -26,19 +26,17 @@ class ImportStat implements ShouldQueue private const STATS_DISK = 'stats'; - private $statFileName; + private string $statFileName; + + private int $year; /** * Create a new job instance. */ - public function __construct(?string $statFileName = null) + public function __construct(?string $statFileName = null, ?int $year = null) { - if ($statFileName === null) { - $timestamp = now()->format('Y-m-d'); - $statFileName = "stats_{$timestamp}.json"; - } - - $this->statFileName = $statFileName; + $this->statFileName = $statFileName ?? sprintf('stats_%s.json', now()->format('Y-m-d')); + $this->year = $year ?? $this->inferYear($this->statFileName); } /** @@ -47,10 +45,9 @@ public function __construct(?string $statFileName = null) public function handle(): void { app('Log')::info('Parsing Stat Download'); - $year = now()->year; try { - $content = Storage::disk(self::STATS_DISK)->get(sprintf('%d/%s', $year, $this->statFileName)); + $content = Storage::disk(self::STATS_DISK)->get(sprintf('%d/%s', $this->year, $this->statFileName)); if ($content === null) { throw new FileNotFoundException; } @@ -96,4 +93,13 @@ public function handle(): void ] ); } + + private function inferYear(string $statFileName): int + { + if (preg_match('/^stats_(\d{4})-\d{2}-\d{2}\.json$/', $statFileName, $matches) === 1) { + return (int) $matches[1]; + } + + return now()->year; + } } diff --git a/app/Jobs/StarCitizen/Stat/SyncStats.php b/app/Jobs/StarCitizen/Stat/SyncStats.php new file mode 100644 index 000000000..669700be2 --- /dev/null +++ b/app/Jobs/StarCitizen/Stat/SyncStats.php @@ -0,0 +1,29 @@ +format('Y-m-d')); + $year = $date->year; + + DownloadStats::withChain([ + new ImportStat($statFileName, $year), + ])->dispatch($statFileName, $year); + } +} diff --git a/app/Jobs/StarCitizen/Vehicle/CheckShipMatrixStructure.php b/app/Jobs/StarCitizen/Vehicle/CheckShipMatrixStructure.php deleted file mode 100644 index 32141219c..000000000 --- a/app/Jobs/StarCitizen/Vehicle/CheckShipMatrixStructure.php +++ /dev/null @@ -1,64 +0,0 @@ -shipMatrix = $this->getNewestShipMatrixFilename(); - $this->groundTruth = File::get(storage_path('framework/testing/shipmatrix/aurora_es.json')); - - $this->groundTruth = collect(json_decode($this->groundTruth, true, 512, JSON_THROW_ON_ERROR)); - } catch (FileNotFoundException|RuntimeException|JsonException $e) { - $this->fail($e); - } - - $vehicles = json_decode(Storage::disk('vehicles')->get($this->shipMatrix), true, 512, JSON_THROW_ON_ERROR); - - $diff = $this->groundTruth->diffKeys($vehicles[0]); - - if ($diff->count() !== 0) { - $keys = $diff->keys(); - - app('Log')::error('Ship Matrix structure changed, aborting job. Missing keys:', $keys->toArray()); - ShipMatrixStructureChanged::dispatch(); - - $this->fail('Ship Matrix structure changed. Missing keys: '.$keys->implode(', ')); - } - } -} diff --git a/app/Jobs/StarCitizen/Vehicle/DownloadShipMatrix.php b/app/Jobs/StarCitizen/Vehicle/DownloadShipMatrix.php deleted file mode 100644 index 67ec4f392..000000000 --- a/app/Jobs/StarCitizen/Vehicle/DownloadShipMatrix.php +++ /dev/null @@ -1,103 +0,0 @@ -force = $force; - } - - /** - * Execute the job. - */ - public function handle(): void - { - app('Log')::info('Starting Ship Matrix Download Job'); - - if (! $this->force && Storage::disk(self::VEHICLES_DISK)->exists($this->getPath())) { - return; - } - - try { - $response = $this->makeClient()->get(self::SHIPS_ENDPOINT)->throw(); - } catch (RequestException $e) { - app('Log')::critical( - 'Could not connect to RSI Ship Matrix', - [ - 'message' => $e->getMessage(), - ] - ); - - $this->fail($e); - - return; - } - - try { - $response = $this->parseResponseBody($response->body()); - } catch (InvalidDataException $e) { - app('Log')::error( - 'Ship Matrix data is not valid json', - [ - 'message' => $e->getMessage(), - ] - ); - - $this->fail($e); - - return; - } - - // Exception will not happen - $responseJsonData = json_encode($response->data, JSON_THROW_ON_ERROR); - - Storage::disk(self::VEHICLES_DISK)->put($this->getPath(), $responseJsonData); - - app('Log')::info('Ship Matrix Download finished'); - } - - /** - * Generates the Shipmatrix Filename - */ - private function getPath(): string - { - $dirName = now()->format('Y-m-d'); - $fileTimeStamp = now()->format('Y-m-d_H-i'); - $filename = "shipmatrix_{$fileTimeStamp}.json"; - - return "{$dirName}/{$filename}"; - } -} diff --git a/app/Jobs/StarCitizen/Vehicle/Import/ImportMsrp.php b/app/Jobs/StarCitizen/Vehicle/Import/ImportMsrp.php deleted file mode 100644 index fefd98833..000000000 --- a/app/Jobs/StarCitizen/Vehicle/Import/ImportMsrp.php +++ /dev/null @@ -1,95 +0,0 @@ -makeClient(); - - $query = <<<'QUERY' -{ - ships { - id - name - msrp - link - skus { - id - title - available - price - } - } -} -QUERY; - - self::$client->post('https://robertsspaceindustries.com/api/account/v2/setAuthToken'); - self::$client->post('https://robertsspaceindustries.com/api/ship-upgrades/setContextToken'); - $response = self::$client->post( - 'https://robertsspaceindustries.com/pledge-store/api/upgrade', - [ - 'query' => $query, - ] - ); - - if (! $response->ok()) { - app('Log')::error('Could not connect to RSI Pledge Store API, retrying in 5 minutes.'); - - $this->release(300); - } - - collect($response->json('data.ships', [])) - ->each( - function (array $vehicle) { - /** @var Vehicle $model */ - $model = Vehicle::query()->where('cig_id', $vehicle['id'])->first(); - - if ($model === null) { - return; - } - - if ($vehicle['msrp'] !== null) { - $model->update( - [ - 'msrp' => substr($vehicle['msrp'], 0, -2), - 'pledge_url' => $vehicle['link'], - ] - ); - } - - if (! empty($vehicle['skus'])) { - collect($vehicle['skus'])->each(function (array $sku) use ($model) { - $model->skus()->updateOrCreate([ - 'cig_id' => $sku['id'], - ], [ - 'title' => $sku['title'], - 'price' => substr($sku['price'], 0, -2), - 'available' => $sku['available'], - ]); - }); - } - } - ); - } -} diff --git a/app/Jobs/StarCitizen/Vehicle/Import/ImportShipMatrix.php b/app/Jobs/StarCitizen/Vehicle/Import/ImportShipMatrix.php deleted file mode 100644 index 3d224b90c..000000000 --- a/app/Jobs/StarCitizen/Vehicle/Import/ImportShipMatrix.php +++ /dev/null @@ -1,97 +0,0 @@ -shipMatrixFileName = $shipMatrixFileName; - } else { - try { - $this->shipMatrixFileName = $this->getNewestShipMatrixFilename(); - } catch (RuntimeException $e) { - $this->fail($e); - } - } - } - - /** - * Execute the job. - */ - public function handle(): void - { - app('Log')::info('Parsing Ship Matrix Download'); - - try { - $content = Storage::disk('vehicles')->get($this->shipMatrixFileName ?? 'HowCanThisBeNull??'); - if ($content === null) { - throw new FileNotFoundException; - } - - $vehicles = json_decode( - $content, - true, - 512, - JSON_THROW_ON_ERROR - ); - } catch (FileNotFoundException $e) { - app('Log')::error( - "File {$this->shipMatrixFileName} not found on Disk vehicles", - [ - 'message' => $e->getMessage(), - ] - ); - - $this->fail($e); - - return; - } catch (JsonException $e) { - app('Log')::error( - "File {$this->shipMatrixFileName} does not contain valid JSON", - [ - 'message' => $e->getMessage(), - ] - ); - - $this->delete(); - - return; - } - - collect($vehicles)->each( - function ($vehicle) { - dispatch(new ImportVehicle(new Collection($vehicle))); - } - ); - } -} diff --git a/app/Jobs/StarCitizen/Vehicle/Import/ImportLoaner.php b/app/Jobs/StarCitizen/Vehicle/ImportLoaner.php similarity index 98% rename from app/Jobs/StarCitizen/Vehicle/Import/ImportLoaner.php rename to app/Jobs/StarCitizen/Vehicle/ImportLoaner.php index 08069ac57..b1a6e7ce4 100644 --- a/app/Jobs/StarCitizen/Vehicle/Import/ImportLoaner.php +++ b/app/Jobs/StarCitizen/Vehicle/ImportLoaner.php @@ -1,8 +1,8 @@ cookieJar = new CookieJar; + + $client = $rsiClient->base()->withOptions([ + 'cookies' => $this->cookieJar, + ]); + + $query = <<<'QUERY' +{ + ships { + id + name + msrp + link + skus { + id + title + available + price + } + } +} +QUERY; + + try { + $client->post('https://robertsspaceindustries.com/api/account/v2/setAuthToken')->throw(); + $client->post('https://robertsspaceindustries.com/api/ship-upgrades/setContextToken')->throw(); + $response = $client->post( + 'https://robertsspaceindustries.com/pledge-store/api/upgrade', + [ + 'query' => $query, + ] + )->throw(); + } catch (RequestException $e) { + app('Log')::critical('Could not connect to RSI Pledge Store API', [ + 'message' => $e->getMessage(), + ]); + + $this->fail($e); + + return; + } + + collect($response->json('data.ships', [])) + ->each( + function (array $vehicle) { + /** @var Vehicle $model */ + $model = Vehicle::query()->where('cig_id', $vehicle['id'])->first(); + + if ($model === null) { + return; + } + + if ($vehicle['msrp'] !== null) { + $model->update( + [ + 'msrp' => substr((string) $vehicle['msrp'], 0, -2), + 'pledge_url' => $vehicle['link'], + ] + ); + } + + if (! empty($vehicle['skus'])) { + collect($vehicle['skus'])->each(function (array $sku) use ($model) { + $model->skus()->updateOrCreate([ + 'cig_id' => $sku['id'], + ], [ + 'title' => $sku['title'], + 'price' => substr((string) $sku['price'], 0, -2), + 'available' => $sku['available'], + ]); + }); + } + } + ); + } +} diff --git a/app/Jobs/StarCitizen/Vehicle/ImportShipMatrix.php b/app/Jobs/StarCitizen/Vehicle/ImportShipMatrix.php new file mode 100644 index 000000000..27add7de6 --- /dev/null +++ b/app/Jobs/StarCitizen/Vehicle/ImportShipMatrix.php @@ -0,0 +1,151 @@ +downloadShipMatrix($client); + $this->cleanupDailyFiles($shipMatrixPath); + $vehicles = $this->loadVehicles($shipMatrixPath); + $this->assertStructure($vehicles); + } catch (Throwable $e) { + app('Log')::error( + 'Ship Matrix download or import failed', + [ + 'message' => $e->getMessage(), + ] + ); + $this->fail($e); + + return; + } + + collect($vehicles)->each( + static function (array $vehicle): void { + dispatch(new ImportVehicle(new Collection($vehicle))); + } + ); + } + + /** + * @throws RuntimeException|RequestException|ConnectionException|JsonException + */ + private function downloadShipMatrix(RsiDownloadClient $client): string + { + $path = $this->buildPath(); + + $response = $client->forRsi()->throw()->get(config('services.rsi_url').self::SHIPS_ENDPOINT); + + $parsed = json_decode($response->body(), false, 512, JSON_THROW_ON_ERROR); + + if (($parsed->success ?? 0) !== 1) { + throw new RuntimeException( + sprintf('RSI data is not valid. Expected success = 1, got %d', $parsed->success ?? 0) + ); + } + + $responseJsonData = json_encode($parsed->data, JSON_THROW_ON_ERROR); + + Storage::disk(self::VEHICLES_DISK)->put($path, $responseJsonData); + + return $path; + } + + /** + * @return array> + * + * @throws FileNotFoundException|JsonException + */ + private function loadVehicles(string $path): array + { + $content = Storage::disk(self::VEHICLES_DISK)->get($path); + + if ($content === null) { + throw new FileNotFoundException("Ship Matrix file {$path} could not be read"); + } + + return json_decode($content, true, 512, JSON_THROW_ON_ERROR); + } + + /** + * @param array> $vehicles + * + * @throws JsonException|RuntimeException + */ + private function assertStructure(array $vehicles): void + { + if ($vehicles === []) { + throw new RuntimeException('Ship Matrix payload is empty'); + } + + $groundTruth = File::get(storage_path('framework/testing/shipmatrix/aurora_es.json')); + $groundTruth = collect(json_decode($groundTruth, true, 512, JSON_THROW_ON_ERROR)); + + $diff = $groundTruth->diffKeys($vehicles[0]); + + if ($diff->count() !== 0) { + $keys = $diff->keys(); + + app('Log')::error('Ship Matrix structure changed, aborting job. Missing keys:', $keys->toArray()); + if (class_exists(ShipMatrixStructureChanged::class)) { + ShipMatrixStructureChanged::dispatch(); + } + + throw new RuntimeException('Ship Matrix structure changed. Missing keys: '.$keys->implode(', ')); + } + } + + private function cleanupDailyFiles(string $keepPath): void + { + $directory = Str::before($keepPath, '/'); + + $files = Storage::disk(self::VEHICLES_DISK)->files($directory); + + collect($files) + ->filter(static fn (string $file): bool => $file !== $keepPath && Str::contains($file, 'shipmatrix')) + ->each(static fn (string $file): bool => Storage::disk(self::VEHICLES_DISK)->delete($file)); + } + + private function buildPath(): string + { + $dirName = now()->format('Y-m-d'); + $fileTimeStamp = now()->format('Y-m-d_H-i'); + $filename = "shipmatrix_{$fileTimeStamp}.json"; + + return "{$dirName}/{$filename}"; + } +} diff --git a/app/Jobs/StarCitizen/Vehicle/Import/ImportVehicle.php b/app/Jobs/StarCitizen/Vehicle/ImportVehicle.php similarity index 91% rename from app/Jobs/StarCitizen/Vehicle/Import/ImportVehicle.php rename to app/Jobs/StarCitizen/Vehicle/ImportVehicle.php index ea70fe30c..e7b8ddccd 100644 --- a/app/Jobs/StarCitizen/Vehicle/Import/ImportVehicle.php +++ b/app/Jobs/StarCitizen/Vehicle/ImportVehicle.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\Jobs\StarCitizen\Vehicle\Import; +namespace App\Jobs\StarCitizen\Vehicle; -use App\Models\StarCitizen\Vehicle\Vehicle\Vehicle; +use App\Models\StarCitizen\ShipMatrix\Vehicle\Vehicle; use App\Services\Parser\ShipMatrix\Component; use App\Services\Parser\ShipMatrix\Manufacturer; use App\Services\Parser\ShipMatrix\ProductionNote; @@ -12,7 +12,6 @@ use App\Services\Parser\ShipMatrix\Vehicle\Focus; use App\Services\Parser\ShipMatrix\Vehicle\Size; use App\Services\Parser\ShipMatrix\Vehicle\Type; -use App\Traits\CreateRelationChangelogTrait; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -22,12 +21,8 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; -/** - * Class AbstractParseVehicle - */ class ImportVehicle implements ShouldQueue { - use CreateRelationChangelogTrait; use Dispatchable; use InteractsWithQueue; use Queueable; @@ -105,20 +100,15 @@ public function handle(): void /** @var Vehicle $vehicle */ $vehicle = Vehicle::query()->updateOrCreate($where, $data); - $vehicle->translations()->updateOrCreate( - [ - 'locale_code' => config('language.english'), - ], - [ - 'translation' => strip_tags($this->rawData->get(self::VEHICLE_DESCRIPTION, '') ?? ''), - ] - ); + $translation = strip_tags($this->rawData->get(self::VEHICLE_DESCRIPTION, '') ?? ''); - $changes = []; - $changes['foci'] = $this->syncFociIds($vehicle); - $changes['components'] = $this->syncComponents($vehicle); + if ($translation !== '') { + $vehicle->setTranslation('translation', config('language.english'), $translation); + $vehicle->save(); + } - $this->createRelationChangelog($changes, $vehicle); + $this->syncFociIds($vehicle); + $this->syncComponents($vehicle); } /** diff --git a/app/Jobs/TrackApiRouteCall.php b/app/Jobs/TrackApiRouteCall.php deleted file mode 100644 index 663435ab6..000000000 --- a/app/Jobs/TrackApiRouteCall.php +++ /dev/null @@ -1,51 +0,0 @@ -request = $request; - } - - /** - * Execute the job. - */ - public function handle(): void - { - Http::withHeaders([ - 'User-Agent' => $this->request['user-agent'], - 'X-Forwarded-For' => $this->request['forwarded-for'], - ]) - ->timeout(10) - ->retry(5) - ->post(sprintf('%s/api/event', config('services.plausible.domain')), [ - 'name' => 'pageview', - 'url' => $this->request['url'], - 'domain' => parse_url(config('app.url'))['host'], - ]) - ->onError(fn () => $this->delete()); - } -} diff --git a/app/Jobs/Transcript/ImportMetadata.php b/app/Jobs/Transcript/ImportMetadata.php deleted file mode 100644 index 456d9edad..000000000 --- a/app/Jobs/Transcript/ImportMetadata.php +++ /dev/null @@ -1,103 +0,0 @@ -chunkAll = $chunkAll; - } - - /** - * Execute the job. - */ - public function handle(): void - { - collect(Storage::disk('transcripts')->allFiles()) - ->filter(function (string $path) { - return substr($path, -5) === '.json'; - }) - ->chunk($this->chunkAll ? 2000 : 10) - ->each(function (Collection $chunk) { - $chunk - ->map(function (string $path) { - try { - $content = json_decode( - File::get(Storage::disk('transcripts')->path($path)), - true, - 512, - JSON_THROW_ON_ERROR - ); - } catch (FileNotFoundException|JsonException $e) { - app('Log')::warning($e->getMessage()); - - return null; - } - - $thumbnail = null; - if (count($content['thumbnails']) > 1) { - $thumbnail = array_pop($content['thumbnails']); - if ($thumbnail !== null) { - $thumbnail = explode('?', $thumbnail['url']); - $thumbnail = $thumbnail[0]; - } - } - - $filename = $content['_filename'] ?? null; - if ($filename !== null) { - $filename = explode('/', $filename); - $filename = array_pop($filename); - } - - return [ - 'youtube_id' => $content['id'], - 'title' => $content['title'], - 'playlist_name' => $content['playlist_title'] ?? null, - 'upload_date' => Carbon::parse(str_replace('.', '', $content['upload_date']))->toDateString(), - 'runtime' => $content['duration'], - 'thumbnail' => $thumbnail, - 'youtube_description' => $content['description'], - 'filename' => $filename, - ]; - }) - ->filter(function ($in) { - return ! empty($in); - }) - ->sortBy('upload_date') - ->each(function (array $data) { - $id = $data['youtube_id']; - unset($data['youtube_id']); - - Transcript::query()->firstOrCreate([ - 'youtube_id' => $id, - ], $data); - }); - }); - } -} diff --git a/app/Jobs/Transcript/Translate/TranslateTranscript.php b/app/Jobs/Transcript/Translate/TranslateTranscript.php deleted file mode 100644 index 89508e0e9..000000000 --- a/app/Jobs/Transcript/Translate/TranslateTranscript.php +++ /dev/null @@ -1,99 +0,0 @@ -transcript = $transcript; - } - - /** - * Execute the job. - */ - public function handle(): void - { - if ($this->transcript->english() === null) { - return; - } - - app('Log')::info("Translating Transcript {$this->transcript->url}"); - - $english = $this->transcript->english()->translation; - $translation = ''; - - try { - if (mb_strlen($english) > DeepLy::MAX_TRANSLATION_TEXT_LEN) { - foreach (str_split_unicode($english, DeepLy::MAX_TRANSLATION_TEXT_LEN) as $chunk) { - $chunkTranslation = DeepLyFacade::translate($chunk, config('services.deepl.target_locale'), 'EN'); - $translation .= " {$chunkTranslation}"; - } - } else { - $translation = DeepLyFacade::translate($english, config('services.deepl.target_locale'), 'EN'); - } - } catch (QuotaException $e) { - app('Log')::warning('Deepl Quote exceeded!'); - - $this->fail($e); - - return; - } catch (RateLimitedException $e) { - app('Log')::info('Got rate limit exception. Trying job again in 60 seconds.'); - - $this->release(60); - - return; - } catch (TextLengthException $e) { - app('Log')::warning($e->getMessage()); - - return; - } catch (CallException|AuthenticationException|InvalidArgumentException $e) { - app('Log')::warning(sprintf('%s: %s', 'Translation failed with Message', $e->getMessage())); - - $this->fail($e); - - return; - } - - $this->transcript->translations()->updateOrCreate( - [ - 'locale_code' => 'de_DE', - ], - [ - 'translation' => trim($translation), - 'proofread' => false, - ] - ); - } -} diff --git a/app/Jobs/Transcript/Translate/TranslateTranscripts.php b/app/Jobs/Transcript/Translate/TranslateTranscripts.php deleted file mode 100644 index 4a96d7287..000000000 --- a/app/Jobs/Transcript/Translate/TranslateTranscripts.php +++ /dev/null @@ -1,61 +0,0 @@ -limit = $limit; - } - - /** - * Execute the job. - */ - public function handle(): void - { - app('Log')::info('Starting Transcript Translations'); - - $jobLimit = $this->limit === 0 ? PHP_INT_MAX : $this->limit; - $count = 0; - - Transcript::query()->chunk( - 100, - static function (Collection $transcripts) use ($jobLimit, &$count) { - $transcripts->each( - static function (Transcript $transcript) use ($jobLimit, &$count) { - if ($count < $jobLimit && optional($transcript->german())->translation === null) { - $count++; - dispatch(new TranslateTranscript($transcript)); - } - } - ); - } - ); - } -} diff --git a/app/Jobs/Wiki/ApproveRevisions.php b/app/Jobs/Wiki/ApproveRevisions.php deleted file mode 100644 index 825ee79ac..000000000 --- a/app/Jobs/Wiki/ApproveRevisions.php +++ /dev/null @@ -1,202 +0,0 @@ -pageTitles = $pageTitles; - $this->onlyApproveNew = $onlyApproveNew; - $this->resolveRedirects = $resolveRedirects; - } - - /** - * Execute the job. - */ - public function handle(): void - { - $this->requestCsrfToken(); - $ids = $this->getRevisionIDs(); - $ids = collect($ids['pages'] ?? []) - ->filter( - function ($page) { - if ($this->onlyApproveNew === true) { - // Only approve new pages - return isset($page['new']); - } - - return true; - } - ) - ->map( - function ($page) { - return $page['revisions'] ?? []; - } - ) - ->map( - function ($revisions) { - return Arr::first($revisions, null, []); - } - ) - ->map( - function ($revision) { - return $revision['revid'] ?? 0; - } - ) - ->filter( - function ($id) { - return $id > 0; - } - ); - - $this->approveRevisions($ids); - } - - /** - * Requests an CSRF Token from the Wiki - */ - private function requestCsrfToken(): void - { - try { - $token = $this->getCsrfToken('services.wiki_approve_revs'); - } catch (ErrorException $e) { - app('Log')::info( - sprintf( - '%s: %s', - 'Token retrieval failed', - $e->getMessage() - ) - ); - - $this->release(300); - - return; - } - - if ($token === null) { - $this->release(300); - - return; - } - - $this->token = $token; - } - - /** - * Revision ids from page titles - * - * @return array Page revision ids - */ - private function getRevisionIDs(): array - { - $titles = $this->pageTitles; - - try { - if ($this->resolveRedirects === true) { - $titles = collect($this->pageTitles)->map(function ($title) { - return WrappedWiki::getRedirectTitle($title); - })->toArray(); - } - - $revisions = MediaWikiApi::query() - ->formatVersion(2) - ->json() - ->prop('revisions') - ->prop('info') - ->titles(implode('|', $titles)) - ->addParam('rvprop', 'ids') - ->request(); - } catch (GuzzleException $e) { - $this->release(300); - - return []; - } - - if ($revisions->hasErrors()) { - app('Log')::info( - sprintf( - '%s: %s', - 'Revision retrieval failed', - $revisions->getErrors()['code'] ?? '' - ) - ); - - $this->release(300); - - return []; - } - - return $revisions->getQuery(); - } - - /** - * Approve revisions - */ - private function approveRevisions(Collection $ids): void - { - $ids->each( - function ($id) { - try { - $response = MediaWikiApi::action('approve', 'POST') - ->withAuthentication() - ->csrfToken($this->token) - ->addParam('revid', $id) - ->request(); - if ($response->hasErrors()) { - app('Log')::error( - sprintf( - 'Could not approve revision %s. Message: %s', - $id, - json_encode($response->getErrors()) - ) - ); - } - } catch (GuzzleException $e) { - app('Log')::error( - sprintf( - 'Could not approve revision %s. Message: %s', - $id, - json_encode($e->getMessage()) - ) - ); - } - } - ); - } -} diff --git a/app/Jobs/Wiki/CommLink/CreateCommLinkWikiPage.php b/app/Jobs/Wiki/CommLink/CreateCommLinkWikiPage.php deleted file mode 100644 index 2adec9a62..000000000 --- a/app/Jobs/Wiki/CommLink/CreateCommLinkWikiPage.php +++ /dev/null @@ -1,120 +0,0 @@ -commLink = $commLink; - $this->token = $token; - $this->template = $template; - } - - /** - * Execute the job. - */ - public function handle(): void - { - $this->createCommLinkPage(config('services.wiki_translations.locale'), "Comm-Link:{$this->commLink->cig_id}"); - - if (config('services.wiki_translations.create_english_subpage') === true) { - $this->createCommLinkPage(Language::ENGLISH, "Comm-Link:{$this->commLink->cig_id}/en"); - } - } - - /** - * Handle the actual creation - * - * @param string $language Text language - * @param string $title MediaWiki Page Title - */ - private function createCommLinkPage(string $language, string $title): void - { - app('Log')::info("Creating Wiki Page '{$title}'"); - - try { - if ($language === 'de_DE') { - $text = optional($this->commLink->german())->translation; - } else { - $text = optional($this->commLink->english())->translation; - } - - if ($text !== null && ! Normalizer::isNormalized($text)) { - $text = Normalizer::normalize($text); - } - - if ($text !== null && config('language.translate_wrap_commlinks')) { - $text = sprintf("\n%s\n", $text); - $this->template = str_replace('{{Comm-Link}}', '{{Comm-Link}}', $this->template); - } - - $response = MediaWikiApi::edit($title)->text( - sprintf( - "%s\n%s", - $this->template, - $text ?? '' - ) - ) - ->summary("Importing Comm-Link Translation {$this->commLink->cig_id}") - ->csrfToken($this->token) - ->markBotEdit() - ->createOnly() - ->request(); - } catch (ConnectException $e) { - $this->release(60); - - return; - } catch (GuzzleException|RuntimeException $e) { - app('Log')::error('Could not get an CSRF Token', $e->getResponse()->getErrors()); - - $this->fail($e); - - return; - } - - if (config('services.wiki_approve_revs.access_secret', null) !== null) { - dispatch(new ApproveRevisions(["Comm-Link:{$this->commLink->cig_id}"])); - } - - app('Log')::debug('Wiki Page Response:', $response->getBody()); - } -} diff --git a/app/Jobs/Wiki/CommLink/CreateCommLinkWikiPages.php b/app/Jobs/Wiki/CommLink/CreateCommLinkWikiPages.php deleted file mode 100644 index 121c184e4..000000000 --- a/app/Jobs/Wiki/CommLink/CreateCommLinkWikiPages.php +++ /dev/null @@ -1,141 +0,0 @@ -getCsrfToken('services.wiki_translations'); - } catch (ErrorException $e) { - app('Log')::info( - sprintf( - '%s: %s', - 'Token retrieval failed', - $e->getMessage() - ) - ); - - $this->release(300); - - return; - } - - $commLinkConfig = $this->getCommLinkConfig(); - $commLinkConfig['token'] = $token; - $this->config = $commLinkConfig; - - app('Log')::debug('Current config:', $commLinkConfig); - - $dispatchFunction = function (Collection $commLinks) { - try { - $pageInfoCollection = $this->getPageInfoForCommLinks($commLinks, true); - } catch (RuntimeException $e) { - app('Log')::error($e->getMessage()); - - if (strpos($e->getMessage(), 'Guru Meditation') !== false) { - $this->release(60); - } else { - $this->fail($e); - } - - return; - } - - $localConfig = $this->config; - - $commLinks->each( - static function (CommLink $commLink) use ($pageInfoCollection, $localConfig) { - $wikiPage = $pageInfoCollection->get($commLink->cig_id, []); - - if (isset($wikiPage['missing'])) { - dispatch( - new CreateCommLinkWikiPage( - $commLink, - $localConfig['token'], - $localConfig['template'] - ) - ); - } - } - ); - }; - - CommLink::query()->whereHas( - 'translations', - static function (Builder $query) { - $query->where('locale_code', config('services.wiki_translations.locale')) - ->whereRaw( - "translation <> ''" - ); - } - )->chunk( - 100, - $dispatchFunction - ); - - $commLinkConfig = $this->getCommLinkConfig('Comm-Link:Subscriber-Header'); - $commLinkConfig['token'] = $token; - $this->config = $commLinkConfig; - - CommLink::query()->whereHas( - 'channel', - static function (Builder $query) { - $query->where('name', 'Subscriber'); - } - )->chunk( - 100, - $dispatchFunction - ); - - $commLinkConfig = $this->getCommLinkConfig('Comm-Link:Press-Header'); - $commLinkConfig['token'] = $token; - $this->config = $commLinkConfig; - - CommLink::query()->whereHas( - 'channel', - static function (Builder $query) { - $query->where('name', 'Press'); - } - )->chunk( - 100, - $dispatchFunction - ); - } -} diff --git a/app/Jobs/Wiki/CommLink/CreateCommLinkWikiTranslationPages.php b/app/Jobs/Wiki/CommLink/CreateCommLinkWikiTranslationPages.php deleted file mode 100644 index 66b047538..000000000 --- a/app/Jobs/Wiki/CommLink/CreateCommLinkWikiTranslationPages.php +++ /dev/null @@ -1,88 +0,0 @@ -getCsrfToken('services.wiki_translations'); - } catch (ErrorException $e) { - app('Log')::info( - sprintf( - '%s: %s', - 'Token retrieval failed', - $e->getMessage() - ) - ); - - $this->release(300); - - return; - } - - CommLink::query() - ->whereHas( - 'translations', - static function (Builder $query) { - $query->where('locale_code', config('services.wiki_translations.locale')) - ->whereRaw( - "translation <> ''" - ); - } - )->chunk( - 10, - function (Collection $commlinks) use ($token) { - $commlinks->each(function (CommLink $commLink) use ($token) { - MediaWikiApi::edit("Comm-Link:{$commLink->cig_id}/en") - ->text(sprintf( - "{{Comm-Link}}\n%s", - optional($commLink->english())->translation - )) - ->summary('Importing Comm-Link Translation') - ->csrfToken($token) - ->markBotEdit() - ->request(); - }); - - if (config('services.wiki_approve_revs.access_secret', null) !== null) { - dispatch(new ApproveRevisions($commlinks->map(function (CommLink $commLink) { - return "Comm-Link:{$commLink->cig_id}/en"; - })->toArray())); - } - } - ); - } -} diff --git a/app/Jobs/Wiki/CommLink/UpdateCommLinkProofReadStatus.php b/app/Jobs/Wiki/CommLink/UpdateCommLinkProofReadStatus.php deleted file mode 100644 index dfeca31d0..000000000 --- a/app/Jobs/Wiki/CommLink/UpdateCommLinkProofReadStatus.php +++ /dev/null @@ -1,99 +0,0 @@ -loginWikiBotAccount('services.wiki_translations'); - - $config = $this->getCommLinkConfig(); - - CommLink::query()->whereHas( - 'translations', - static function (Builder $query) { - $query->where(self::LOCALE_CODE, 'de_DE')->whereRaw("translation <> ''"); - } - )->chunk( - 100, - function (Collection $commLinks) use ($config) { - try { - $pageInfoCollection = $this->getPageInfoForCommLinks($commLinks, true); - } catch (RuntimeException $e) { - app('Log')::error($e->getMessage()); - - $this->fail($e); - - return; - } - - $commLinks->each( - static function (CommLink $commLink) use ($pageInfoCollection, $config) { - $wikiPage = $pageInfoCollection->get($commLink->cig_id, []); - - app('Log')::info("Updating Proofread Status for Comm-Link: {$commLink->cig_id}"); - - app('Log')::debug('Wiki Page Data', $wikiPage); - - if (isset($wikiPage[self::CATEGORIES])) { - $proofread = true; - collect($wikiPage[self::CATEGORIES])->each( - static function (array $category) use (&$proofread, $config) { - if (str_contains($category['title'], $config['category'])) { - $proofread = false; - } - } - ); - - $commLink->translations()->where( - [ - self::LOCALE_CODE => 'de_DE', - ] - )->update( - [ - 'proofread' => $proofread, - ] - ); - } - } - ); - } - ); - } -} diff --git a/app/Jobs/Wiki/Galactapedia/CreateGalactapediaWikiPage.php b/app/Jobs/Wiki/Galactapedia/CreateGalactapediaWikiPage.php deleted file mode 100644 index d664a3154..000000000 --- a/app/Jobs/Wiki/Galactapedia/CreateGalactapediaWikiPage.php +++ /dev/null @@ -1,508 +0,0 @@ - 'Menschen', - 'Food and Beverages' => 'Essen und Trinken', - 'Entertainment' => 'Unterhaltung', - 'Law' => 'Recht', - 'Planetary Systems' => 'Planetares System', - 'Education' => 'Bildung', - 'Art' => 'Kunst', - 'Animals' => 'Tier', - 'Space' => 'Weltraum', - 'Ground Transportation' => 'Bodentransport', - 'Culture' => 'Kultur', - 'Music' => 'Musik', - 'Military' => 'Militär', - 'Exploration' => 'Erforschung', - 'Archaeology' => 'Archäologie', - 'Weapons' => 'Waffe', - 'Commerce' => 'Unternehmen', - 'People' => 'Persönlichkeit', - 'Civilizations' => 'Zivilisation', - 'History' => 'Geschichte', - 'Government' => 'Regierung', - 'Fiction' => 'Belletristik', - 'Illegal Activity' => 'Illegale Aktivität', - 'Locations' => 'Standort', - 'Factions' => 'Fraktion', - 'Plants' => 'Pflanze', - 'Politics' => 'Politik', - 'Science and Technology' => 'Wissenschaft und Technik', - 'Settlements' => 'Siedlung', - 'Spacecraft' => 'Raumschiff', - 'Sports' => 'Sport', - 'Holidays' => 'Feiertag', - 'Geography' => 'Geographie', - 'Publications' => 'Publikation', - 'Moons' => 'Mond', - 'Planets' => 'Planet', - ]; - /* jscpd:ignore-end */ - - private Article $article; - - /** - * @var string CSRF Token - */ - private string $token; - - /** - * Response of the thumbnail head request - */ - private ?Response $response = null; - - /** - * The article wiki page title - */ - private string $title = ''; - - /** - * Create a new job instance. - */ - public function __construct(Article $article, string $token) - { - $this->article = $article; - $this->token = $token; - } - - /** - * Execute the job. - */ - public function handle(): void - { - app('Log')::info("Creating Wiki Page '{$this->article->cleanTitle}'"); - - $this->title = WrappedWiki::getRedirectTitle($this->article->cleanTitle); - $wikiText = WrappedWiki::getWikiPageText(Article::normalizeContent($this->title)); - - if (preg_match('/(REDIRECT|WEITERLEITUNG)/', $wikiText ?? '') === 1) { - app('Log')::warning(sprintf( - 'Could not determine redirect title for "%s" (from %s)', - $this->title, - $this->article->cleanTitle - )); - $this->release(7200); - - return; - } - - if ($wikiText === null && WrappedWiki::pageExists($this->title)) { - app('Log')::warning(sprintf('Could not load content for "%s"', $this->title)); - $this->release(7200); - - return; - } - - try { - $text = $this->getFormattedText($this->getArticleText(), $wikiText); - - // Skip if texts are equal or translation markers are present - if (strcmp($text, $wikiText ?? '') === 0 || strpos($wikiText ?? '', '%s[[Category:Galactapedia]]%s -FORMAT; - - $content = $this->createContent($markdown, str_contains($pageContent ?? '', 'galactapedia-box')); - $categories = $this->createCategories(); - $ref = $this->createRef(); - - if ($pageContent !== null) { - if (str_contains($pageContent, 'DISABLE-CATS-->')) { - $categories = ''; - } - - $content = $this->runTextReplacements($content, $pageContent); - - if (strpos($pageContent, '(Getrennter erster Satz)') !== false) { - if (isset($content['repl'])) { - $content['repl'] = sprintf("%s\n(Getrennter erster Satz)", $content['repl']); - } else { - $content['repl'] = "\n(Getrennter erster Satz)"; - } - - $text = explode('. ', $content['content'], 2); - $content['content'] = implode(".\n\n", array_map('trim', $text)); - } - - $contentRef = $content['content'].$ref; - - if ( - config('language.translate_wrap_galactapedia') === true && - strpos('', $contentRef) === false - ) { - $contentRef = sprintf('%s', $contentRef); - } - - $formatted = sprintf( - $format, - '', // Don't replace template - $contentRef, - $categories, - $content['repl'] ?? '' - ); - - return preg_replace( - '/(?:
)?(?:<\/div>)?/s', - $formatted, - $pageContent, - 1 - ); - } - - $contentRef = $content.$ref; - - if ( - config('language.translate_wrap_galactapedia') === true && - strpos('', $contentRef) === false - ) { - $contentRef = sprintf('%s', $contentRef); - } - - return sprintf( - $format, - $this->createTemplate(), - $contentRef, - $categories, - '' - ); - } - - /** - * Creates the galactapedia template with content - */ - private function createTemplate(): string - { - $fileEnding = 'jpg'; - if ($this->response !== null) { - $fileEnding = (str_contains($this->response->header('Content-Type'), 'jpeg') ? 'jpg' : 'png'); - } - - $properties = collect(); - $this->article->properties - ->sortBy('name') - ->each(function (ArticleProperty $property) use ($properties) { - $counter = 0; - - if ($properties->has($property->name)) { - do { - $counter++; - $key = sprintf('%s%d', $property->name, $counter); - } while ($properties->has($key)); - $properties[$key] = $property->content; - } else { - $properties[$property->name] = $property->content; - } - }); - - $properties = $properties->map(function ($item, $key) { - try { - $value = DeepLyFacade::translate($item, config('services.deepl.target_locale'), 'EN'); - } catch (Exception $e) { - app('Log')::warning($e->getMessage()); - $value = $item; - } - - return sprintf( - '|%s=%s', - $key, - $value - ); - }) - ->implode("\n"); - - $relatedArticles = $this->article->related - ->map(function (Article $article) { - return sprintf('[[%s]]', $article->cleanTitle); - }) - ->implode("
\n"); - - $normalizedFileName = str_replace('/', '_', $this->article->cleanTitle); - - // The actual template content - return <<