├── backend ├── rocket │ ├── __init__.py │ ├── team.py │ ├── hitbox.py │ ├── quality.py │ ├── paint.py │ └── ids.py ├── utils │ ├── __init__.py │ └── network │ │ ├── __init__.py │ │ ├── exc.py │ │ └── decorators.py ├── controllers │ ├── __init__.py │ ├── bodies │ │ └── __init__.py │ ├── decals │ │ └── __init__.py │ ├── wheels │ │ └── __init__.py │ ├── toppers │ │ └── __init__.py │ └── antennas │ │ └── __init__.py ├── _version.py ├── api │ ├── __init__.py │ └── api.py ├── sql │ ├── 0.6.0 │ │ ├── 20190921_0_ddl_drop_table_decals.sql │ │ ├── 20190921_1_ddl_drop_table_decal_details.sql │ │ ├── 20190831_0_ddl_drop_column_hitbox.sql │ │ └── 20190921_2_create_table_decals.sql │ ├── 0.4.0 │ │ └── 20190817_0_ddl_add_column_hitbox.sql │ ├── 0.8.0 │ │ ├── 20191119_0_ddl_add_columns_antenna.sql │ │ ├── 20191119_0_ddl_add_columns_topper.sql │ │ ├── 20190927_0_ddl_add_column_chassis_paintable.sql │ │ └── 20191117_0_ddl_add_columns_wheel.sql │ ├── 0.5.0 │ │ ├── 20190824_0_ddl_alter_table_nullable_blank_skin.sql │ │ ├── 20190824_0_ddl_drop_replay_id.sql │ │ └── 20190824_0_ddl_remove_autoincrement.sql │ ├── 0.0.2 │ │ ├── 20190801_0_ddl_create_antenna_stick.sql │ │ ├── 20190801_0_ddl_create_decal_detail.sql │ │ ├── 20190801_0_ddl_create_wheel.sql │ │ ├── 20190801_1_ddl_create_decal.sql │ │ ├── 20190801_0_ddl_create_topper.sql │ │ ├── 20190801_0_ddl_create_body.sql │ │ └── 20190801_1_ddl_create_antenna.sql │ ├── 0.4.2 │ │ └── 20190817_0_ddl_add_column_hitbox_translate.sql │ ├── 0.2.0 │ │ └── 20190105_0_ddl_create_user.sql │ ├── 0.9.0 │ │ ├── 20191129_0_ddl_create_product.sql │ │ ├── 20191130_2_ddl_drop_name_columns.sql │ │ └── 20191129_1_ddl_add_foreign_keys.sql │ ├── 0.3.0 │ │ ├── 20190817_0_ddl_alter_table_unique_replay_id.sql │ │ └── 20190813_0_ddl_create_api_key.sql │ └── 0.2.2 │ │ └── 20190807_0_dml_increment_qualities.sql ├── entity │ ├── base.py │ ├── __init__.py │ ├── user.py │ ├── product.py │ ├── api_key.py │ ├── topper.py │ ├── decal.py │ ├── item.py │ ├── wheel.py │ └── body.py ├── dao │ ├── body.py │ ├── product.py │ ├── topper.py │ ├── item.py │ ├── __init__.py │ ├── wheel.py │ ├── api_key.py │ ├── dao.py │ ├── antenna.py │ ├── user.py │ └── decal.py ├── requirements.txt ├── rl_loadout.ini ├── wsgi.py ├── config.py ├── config.ini ├── print_ddl.py ├── auth.py ├── blueprints │ ├── __init__.py │ ├── auth.py │ ├── api.py │ ├── bodies.py │ ├── wheels.py │ ├── toppers.py │ ├── antenna_sticks.py │ ├── api_keys.py │ ├── decals.py │ ├── antennas.py │ └── product.py ├── logging_config.py ├── database.py ├── server.py └── .gitignore ├── frontend ├── src │ ├── assets │ │ ├── .gitkeep │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── rl-icon-about.png │ │ ├── mstile-150x150.png │ │ ├── rl-icon-500x500.png │ │ ├── apple-touch-icon.png │ │ ├── icons │ │ │ ├── Decals-icon.png │ │ │ ├── Paint-icon.png │ │ │ ├── Toppers-icon.png │ │ │ ├── Trails-icon.png │ │ │ ├── Wheels-icon.png │ │ │ ├── Antennas-icon.png │ │ │ ├── Vehicles-icon.png │ │ │ └── Trail_garage_icon.png │ │ ├── rl-icon-2000x2000.png │ │ ├── draco │ │ │ └── draco_decoder.wasm │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── mannfield_equirectangular.jpg │ │ ├── browserconfig.xml │ │ └── site.webmanifest │ ├── app │ │ ├── app.component.scss │ │ ├── admin │ │ │ └── components │ │ │ │ ├── items │ │ │ │ ├── items.component.scss │ │ │ │ ├── decals │ │ │ │ │ ├── decals.component.scss │ │ │ │ │ ├── decals.component.html │ │ │ │ │ ├── decals.component.spec.ts │ │ │ │ │ └── decals.component.ts │ │ │ │ ├── antenna-sticks │ │ │ │ │ ├── antenna-sticks.component.scss │ │ │ │ │ ├── antenna-sticks.component.html │ │ │ │ │ ├── antenna-sticks.component.spec.ts │ │ │ │ │ └── antenna-sticks.component.ts │ │ │ │ ├── items.component.html │ │ │ │ ├── bodies │ │ │ │ │ ├── bodies.component.spec.ts │ │ │ │ │ └── bodies.component.ts │ │ │ │ ├── wheels │ │ │ │ │ ├── wheels.component.spec.ts │ │ │ │ │ └── wheels.component.ts │ │ │ │ ├── toppers │ │ │ │ │ ├── toppers.component.spec.ts │ │ │ │ │ └── toppers.component.ts │ │ │ │ └── antennas │ │ │ │ │ ├── antennas.component.spec.ts │ │ │ │ │ └── antennas.component.ts │ │ │ │ ├── dialog │ │ │ │ ├── create-body │ │ │ │ │ ├── create-body.component.scss │ │ │ │ │ ├── create-body.component.spec.ts │ │ │ │ │ └── create-body.component.ts │ │ │ │ ├── create-decal │ │ │ │ │ ├── create-decal.component.scss │ │ │ │ │ └── create-decal.component.spec.ts │ │ │ │ ├── create-topper │ │ │ │ │ ├── create-topper.component.scss │ │ │ │ │ ├── create-topper.component.spec.ts │ │ │ │ │ ├── create-topper.component.ts │ │ │ │ │ └── create-topper.component.html │ │ │ │ ├── create-wheel │ │ │ │ │ ├── create-wheel.component.scss │ │ │ │ │ ├── create-wheel.component.spec.ts │ │ │ │ │ └── create-wheel.component.ts │ │ │ │ ├── create-antenna │ │ │ │ │ ├── create-antenna.component.scss │ │ │ │ │ ├── create-antenna.component.spec.ts │ │ │ │ │ └── create-antenna.component.ts │ │ │ │ ├── create-antenna-stick │ │ │ │ │ ├── create-antenna-stick.component.scss │ │ │ │ │ ├── create-antenna-stick.component.html │ │ │ │ │ ├── create-antenna-stick.component.spec.ts │ │ │ │ │ └── create-antenna-stick.component.ts │ │ │ │ └── dialog.scss │ │ │ │ ├── api-keys │ │ │ │ ├── create-api-key │ │ │ │ │ ├── create-api-key.component.scss │ │ │ │ │ ├── create-api-key.component.html │ │ │ │ │ ├── create-api-key.component.spec.ts │ │ │ │ │ └── create-api-key.component.ts │ │ │ │ ├── api-keys.component.scss │ │ │ │ ├── api-keys.component.spec.ts │ │ │ │ ├── api-keys.component.html │ │ │ │ └── api-keys.component.ts │ │ │ │ ├── product-upload │ │ │ │ ├── product-upload.component.scss │ │ │ │ ├── product-upload.component.html │ │ │ │ ├── product-upload.component.spec.ts │ │ │ │ └── product-upload.component.ts │ │ │ │ ├── login │ │ │ │ ├── login.component.scss │ │ │ │ ├── login.component.html │ │ │ │ ├── login.component.spec.ts │ │ │ │ └── login.component.ts │ │ │ │ ├── item-list │ │ │ │ ├── item-list.component.scss │ │ │ │ ├── item-list.component.spec.ts │ │ │ │ └── item-list.component.html │ │ │ │ └── main │ │ │ │ ├── main.component.scss │ │ │ │ ├── main.component.ts │ │ │ │ ├── main.component.spec.ts │ │ │ │ └── main.component.html │ │ ├── shared │ │ │ ├── confirm-dialog │ │ │ │ ├── confirm-dialog.component.scss │ │ │ │ ├── confirm-dialog.component.html │ │ │ │ ├── confirm-dialog.component.spec.ts │ │ │ │ └── confirm-dialog.component.ts │ │ │ └── shared.module.ts │ │ ├── app.component.html │ │ ├── home │ │ │ ├── components │ │ │ │ ├── debug │ │ │ │ │ └── texture-viewer │ │ │ │ │ │ ├── texture-viewer.component.scss │ │ │ │ │ │ ├── texture-viewer.component.html │ │ │ │ │ │ ├── texture-viewer.component.spec.ts │ │ │ │ │ │ └── texture-viewer.component.ts │ │ │ │ ├── about-dialog │ │ │ │ │ ├── about-dialog.component.scss │ │ │ │ │ ├── about-dialog.component.ts │ │ │ │ │ ├── about-dialog.component.html │ │ │ │ │ └── about-dialog.component.spec.ts │ │ │ │ ├── loadout-toolbar │ │ │ │ │ ├── loadout-toolbar.component.html │ │ │ │ │ ├── loadout-toolbar.component.scss │ │ │ │ │ └── loadout-toolbar.component.spec.ts │ │ │ │ ├── canvas │ │ │ │ │ ├── canvas.component.html │ │ │ │ │ ├── canvas.component.spec.ts │ │ │ │ │ └── canvas.component.scss │ │ │ │ ├── home │ │ │ │ │ ├── home.component.spec.ts │ │ │ │ │ ├── home.component.scss │ │ │ │ │ ├── home.component.ts │ │ │ │ │ └── home.component.html │ │ │ │ ├── color-selector │ │ │ │ │ ├── color-selector.component.spec.ts │ │ │ │ │ └── color-selector.component.scss │ │ │ │ └── loadout-grid-selector │ │ │ │ │ ├── loadout-grid-selector.component.spec.ts │ │ │ │ │ ├── loadout-grid-selector.component.html │ │ │ │ │ ├── loadout-grid-selector.component.ts │ │ │ │ │ └── loadout-grid-selector.component.scss │ │ │ ├── pipes │ │ │ │ ├── item-filter.pipe.spec.ts │ │ │ │ └── item-filter.pipe.ts │ │ │ └── home.module.ts │ │ ├── model │ │ │ ├── api-key.ts │ │ │ └── product.ts │ │ ├── app.component.ts │ │ ├── auth │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth-guard.service.ts │ │ │ ├── auth.interceptor.ts │ │ │ └── auth.service.ts │ │ ├── service │ │ │ ├── api-keys.service.spec.ts │ │ │ ├── items │ │ │ │ ├── bodies.service.spec.ts │ │ │ │ ├── decals.service.spec.ts │ │ │ │ ├── wheels.service.spec.ts │ │ │ │ ├── toppers.service.spec.ts │ │ │ │ ├── antennas.service.spec.ts │ │ │ │ ├── bodies.service.ts │ │ │ │ ├── antenna-sticks.service.spec.ts │ │ │ │ ├── decals.service.ts │ │ │ │ ├── wheels.service.ts │ │ │ │ ├── toppers.service.ts │ │ │ │ ├── antennas.service.ts │ │ │ │ └── antenna-sticks.service.ts │ │ │ ├── loadout.service.spec.ts │ │ │ ├── texture.service.spec.ts │ │ │ ├── cloud-storage.service.spec.ts │ │ │ ├── loadout-store.service.spec.ts │ │ │ ├── product.service.spec.ts │ │ │ ├── texture.service.ts │ │ │ ├── product.service.ts │ │ │ ├── abstract-item-service.ts │ │ │ └── api-keys.service.ts │ │ ├── utils │ │ │ ├── util.ts │ │ │ ├── color.ts │ │ │ └── network.ts │ │ ├── app-routing.module.ts │ │ ├── app.module.ts │ │ └── app.component.spec.ts │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── main.ts │ ├── styles.scss │ ├── theme.scss │ ├── test.ts │ └── index.html ├── e2e │ ├── tsconfig.json │ ├── src │ │ ├── app.po.ts │ │ └── app.e2e-spec.ts │ └── protractor.conf.js ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── browserslist ├── tsconfig.json ├── .gitignore ├── karma.conf.js └── package.json ├── cloud_functions ├── convert_texture │ ├── requirements.txt │ └── main.py └── compress_model │ ├── package.json │ ├── index.js │ └── .gitignore ├── tools ├── .gitignore ├── batch_download.py ├── compress_models.py ├── batch_download_textures.py ├── convert_textures.py ├── convert_thumbnails.py ├── extract_static_skins.py ├── update_checklist.py └── extract_thumbnails.py ├── .gitmodules ├── .gitignore ├── vm_setup.sh └── version_check.py /backend/rocket/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.9.6' 2 | -------------------------------------------------------------------------------- /backend/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import verify_api_key 2 | -------------------------------------------------------------------------------- /backend/rocket/team.py: -------------------------------------------------------------------------------- 1 | TEAM_BLUE = 0 2 | TEAM_ORANGE = 1 3 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/items.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/decals/decals.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/shared/confirm-dialog/confirm-dialog.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/sql/0.6.0/20190921_0_ddl_drop_table_decals.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE decal; 2 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/antenna-sticks/antenna-sticks.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/sql/0.6.0/20190921_1_ddl_drop_table_decal_details.sql: -------------------------------------------------------------------------------- 1 | DROP Table decal_detail; 2 | -------------------------------------------------------------------------------- /cloud_functions/convert_texture/requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow==6.2.0 2 | google-cloud-storage==1.20.0 3 | -------------------------------------------------------------------------------- /tools/.gitignore: -------------------------------------------------------------------------------- 1 | thumbnails/ 2 | UmodelExport/ 3 | draco/ 4 | models/ 5 | textures/ 6 | converted/ 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib"] 2 | path = lib 3 | url = https://github.com/Longi94/rl-loadout-lib.git 4 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-body/create-body.component.scss: -------------------------------------------------------------------------------- 1 | @import "../dialog"; 2 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-decal/create-decal.component.scss: -------------------------------------------------------------------------------- 1 | @import "../dialog"; 2 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-topper/create-topper.component.scss: -------------------------------------------------------------------------------- 1 | @import "../dialog"; 2 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-wheel/create-wheel.component.scss: -------------------------------------------------------------------------------- 1 | @import "../dialog"; 2 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-antenna/create-antenna.component.scss: -------------------------------------------------------------------------------- 1 | @import "../dialog"; 2 | -------------------------------------------------------------------------------- /backend/entity/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /backend/sql/0.4.0/20190817_0_ddl_add_column_hitbox.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.body 2 | ADD COLUMN hitbox integer; 3 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/favicon.ico -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-antenna-stick/create-antenna-stick.component.scss: -------------------------------------------------------------------------------- 1 | @import "../dialog"; 2 | -------------------------------------------------------------------------------- /frontend/src/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/src/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/src/assets/rl-icon-about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/rl-icon-about.png -------------------------------------------------------------------------------- /backend/sql/0.8.0/20191119_0_ddl_add_columns_antenna.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.antenna 2 | ADD COLUMN normal_map VARCHAR(255); 3 | -------------------------------------------------------------------------------- /backend/sql/0.8.0/20191119_0_ddl_add_columns_topper.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.topper 2 | ADD COLUMN normal_map VARCHAR(255); 3 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/api-keys/create-api-key/create-api-key.component.scss: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/assets/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/mstile-150x150.png -------------------------------------------------------------------------------- /frontend/src/assets/rl-icon-500x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/rl-icon-500x500.png -------------------------------------------------------------------------------- /backend/sql/0.5.0/20190824_0_ddl_alter_table_nullable_blank_skin.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.body ALTER COLUMN blank_skin DROP NOT NULL; 2 | -------------------------------------------------------------------------------- /frontend/src/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/Decals-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/icons/Decals-icon.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/Paint-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/icons/Paint-icon.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/Toppers-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/icons/Toppers-icon.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/Trails-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/icons/Trails-icon.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/Wheels-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/icons/Wheels-icon.png -------------------------------------------------------------------------------- /frontend/src/assets/rl-icon-2000x2000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/rl-icon-2000x2000.png -------------------------------------------------------------------------------- /backend/dao/body.py: -------------------------------------------------------------------------------- 1 | from .item import BaseItemDao 2 | from entity import Body 3 | 4 | 5 | class BodyDao(BaseItemDao): 6 | T = Body 7 | -------------------------------------------------------------------------------- /backend/dao/product.py: -------------------------------------------------------------------------------- 1 | from entity import Product 2 | from .dao import BaseDao 3 | 4 | 5 | class ProductDao(BaseDao): 6 | T = Product 7 | -------------------------------------------------------------------------------- /frontend/src/assets/draco/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/draco/draco_decoder.wasm -------------------------------------------------------------------------------- /frontend/src/assets/icons/Antennas-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/icons/Antennas-icon.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/Vehicles-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/icons/Vehicles-icon.png -------------------------------------------------------------------------------- /backend/dao/topper.py: -------------------------------------------------------------------------------- 1 | from .item import BaseItemDao 2 | from entity import Topper 3 | 4 | 5 | class TopperDao(BaseItemDao): 6 | T = Topper 7 | -------------------------------------------------------------------------------- /frontend/src/assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/src/assets/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/Trail_garage_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/icons/Trail_garage_icon.png -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/dialog.scss: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | 5 | mat-checkbox { 6 | display: block; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/assets/mannfield_equirectangular.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Longi94/rl-loadout/HEAD/frontend/src/assets/mannfield_equirectangular.jpg -------------------------------------------------------------------------------- /backend/rocket/hitbox.py: -------------------------------------------------------------------------------- 1 | HITBOX_OCTANE = 0 2 | HITBOX_DOMINUS = 1 3 | HITBOX_PLANK = 2 4 | HITBOX_BREAKOUT = 3 5 | HITBOX_HYBRID = 4 6 | HITBOX_BATMOBILE = 5 7 | -------------------------------------------------------------------------------- /backend/sql/0.8.0/20190927_0_ddl_add_column_chassis_paintable.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.body 2 | ADD COLUMN chassis_paintable BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/debug/texture-viewer/texture-viewer.component.scss: -------------------------------------------------------------------------------- 1 | mat-dialog-content { 2 | display: flex; 3 | flex-flow: column; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/app/model/api-key.ts: -------------------------------------------------------------------------------- 1 | export class ApiKey { 2 | id: number; 3 | key: string; 4 | name: string; 5 | description: string; 6 | active = false; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/items.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /backend/sql/0.6.0/20190831_0_ddl_drop_column_hitbox.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.body 2 | DROP COLUMN hitbox, 3 | DROP COLUMN hitbox_translate_x, 4 | DROP COLUMN hitbox_translate_z; 5 | -------------------------------------------------------------------------------- /frontend/src/app/model/product.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:variable-name */ 2 | 3 | export class Product { 4 | id: number; 5 | type: string; 6 | product_name: string; 7 | name: string; 8 | } 9 | -------------------------------------------------------------------------------- /backend/sql/0.0.2/20190801_0_ddl_create_antenna_stick.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE antenna_stick 2 | ( 3 | id SERIAL NOT NULL, 4 | model VARCHAR(255) NOT NULL, 5 | PRIMARY KEY (id) 6 | ); 7 | -------------------------------------------------------------------------------- /backend/sql/0.4.2/20190817_0_ddl_add_column_hitbox_translate.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.body 2 | ADD COLUMN hitbox_translate_x real; 3 | ALTER TABLE public.body 4 | ADD COLUMN hitbox_translate_z real; 5 | -------------------------------------------------------------------------------- /cloud_functions/compress_model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compress-model", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "@google-cloud/storage": "^4.0.1", 6 | "gltf-pipeline": "^2.1.4" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/decals/decals.component.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /backend/rocket/quality.py: -------------------------------------------------------------------------------- 1 | QUALITY_COMMON = 0 2 | QUALITY_UNCOMMON = 1 3 | QUALITY_RARE = 2 4 | QUALITY_VERY_RARE = 3 5 | QUALITY_IMPORT = 4 6 | QUALITY_EXOTIC = 5 7 | QUALITY_BLACK_MARKET = 6 8 | QUALITY_LIMITED = 7 9 | QUALITY_PREMIUM = 8 10 | -------------------------------------------------------------------------------- /backend/sql/0.2.0/20190105_0_ddl_create_user.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "user" 2 | ( 3 | id SERIAL NOT NULL, 4 | name VARCHAR(255) NOT NULL, 5 | password VARCHAR(255) NOT NULL, 6 | PRIMARY KEY (id), 7 | UNIQUE (name) 8 | ); 9 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/product-upload/product-upload.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 20px; 3 | display: flex; 4 | flex-flow: row; 5 | align-items: center; 6 | } 7 | 8 | button { 9 | margin-right: 10px; 10 | } 11 | -------------------------------------------------------------------------------- /backend/sql/0.8.0/20191117_0_ddl_add_columns_wheel.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.wheel 2 | ADD COLUMN rim_n VARCHAR(255); 3 | ALTER TABLE public.wheel 4 | ADD COLUMN tire_base VARCHAR(255); 5 | ALTER TABLE public.wheel 6 | ADD COLUMN tire_n VARCHAR(255); 7 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/antenna-sticks/antenna-sticks.component.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.23.0 2 | flask==1.1.1 3 | sqlalchemy==1.3.15 4 | psycopg2==2.8.4 5 | flask_cors==3.0.8 6 | flask-jwt-extended==3.24.1 7 | bcrypt==3.1.7 8 | connexion==2.6.0 9 | connexion[swagger-ui] 10 | numpy==1.18.1 11 | pandas==1.0.1 12 | -------------------------------------------------------------------------------- /backend/dao/item.py: -------------------------------------------------------------------------------- 1 | from entity import Product 2 | from .dao import BaseDao 3 | 4 | 5 | class BaseItemDao(BaseDao): 6 | 7 | def get_all_join_product(self): 8 | session = self.Session() 9 | return session.query(self.T, Product).join(Product) 10 | -------------------------------------------------------------------------------- /backend/rl_loadout.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = wsgi:app 3 | 4 | master = true 5 | processes = 5 6 | 7 | socket = rl-loadout.sock 8 | chmod-socket = 660 9 | vacuum = true 10 | 11 | die-on-term = true 12 | 13 | # needed for postgres/sqlalchemy 14 | lazy-apps = true 15 | -------------------------------------------------------------------------------- /backend/dao/__init__.py: -------------------------------------------------------------------------------- 1 | from .api_key import ApiKeyDao 2 | from .body import BodyDao 3 | from .wheel import WheelDao 4 | from .topper import TopperDao 5 | from .decal import DecalDao 6 | from .antenna import AntennaDao 7 | from .user import UserDao 8 | from .product import ProductDao 9 | -------------------------------------------------------------------------------- /frontend/src/app/home/pipes/item-filter.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { ItemFilterPipe } from './item-filter.pipe'; 2 | 3 | describe('ItemFilterPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new ItemFilterPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | // @ts-ignore 3 | version: require('../../package.json').version, 4 | production: true, 5 | assetHost: 'https://storage.googleapis.com/rl-loadout', 6 | backend: 'https://rocket-loadout.com' 7 | }; 8 | -------------------------------------------------------------------------------- /backend/sql/0.9.0/20191129_0_ddl_create_product.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public.product 2 | ( 3 | id INTEGER NOT NULL, 4 | type VARCHAR(255) NOT NULL, 5 | product_name VARCHAR(255) NOT NULL, 6 | name VARCHAR(255) NOT NULL, 7 | PRIMARY KEY (id) 8 | ); 9 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'rl-loadout'; 10 | } 11 | -------------------------------------------------------------------------------- /backend/sql/0.9.0/20191130_2_ddl_drop_name_columns.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE antenna 2 | DROP COLUMN name; 3 | ALTER TABLE body 4 | DROP COLUMN name; 5 | ALTER TABLE decal 6 | DROP COLUMN name; 7 | ALTER TABLE topper 8 | DROP COLUMN name; 9 | ALTER TABLE wheel 10 | DROP COLUMN name; 11 | -------------------------------------------------------------------------------- /frontend/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/entity/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | from .antenna import Antenna, AntennaStick 3 | from .body import Body 4 | from .decal import Decal 5 | from .topper import Topper 6 | from .user import User 7 | from .wheel import Wheel 8 | from .api_key import ApiKey 9 | from .product import Product 10 | -------------------------------------------------------------------------------- /backend/sql/0.5.0/20190824_0_ddl_drop_replay_id.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.antenna DROP COLUMN replay_id; 2 | ALTER TABLE public.body DROP COLUMN replay_id; 3 | ALTER TABLE public.decal_detail DROP COLUMN replay_id; 4 | ALTER TABLE public.topper DROP COLUMN replay_id; 5 | ALTER TABLE public.wheel DROP COLUMN replay_id; 6 | -------------------------------------------------------------------------------- /frontend/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /frontend/src/assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #00aba9 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /backend/entity/user.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | from sqlalchemy import Column, Integer, String 3 | 4 | 5 | class User(Base): 6 | __tablename__ = 'user' 7 | id = Column(Integer, primary_key=True) 8 | name = Column(String(255), nullable=False, unique=True) 9 | password = Column(String(255), nullable=False) 10 | -------------------------------------------------------------------------------- /backend/sql/0.3.0/20190817_0_ddl_alter_table_unique_replay_id.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.antenna ADD UNIQUE (replay_id); 2 | ALTER TABLE public.body ADD UNIQUE (replay_id); 3 | ALTER TABLE public.decal_detail ADD UNIQUE (replay_id); 4 | ALTER TABLE public.topper ADD UNIQUE (replay_id); 5 | ALTER TABLE public.wheel ADD UNIQUE (replay_id); 6 | -------------------------------------------------------------------------------- /backend/sql/0.3.0/20190813_0_ddl_create_api_key.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE api_key 2 | ( 3 | id SERIAL NOT NULL, 4 | key VARCHAR(32) NOT NULL, 5 | name VARCHAR(255) NOT NULL, 6 | description VARCHAR(255) NOT NULL, 7 | active BOOLEAN NOT NULL, 8 | PRIMARY KEY (id), 9 | UNIQUE (key) 10 | ); 11 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .container-div { 2 | display: flex; 3 | width: 100%; 4 | height: 100%; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | 9 | .container-div > div { 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | 14 | span { 15 | text-align: center; 16 | } 17 | -------------------------------------------------------------------------------- /backend/dao/wheel.py: -------------------------------------------------------------------------------- 1 | from .item import BaseItemDao 2 | from entity import Wheel 3 | 4 | 5 | class WheelDao(BaseItemDao): 6 | T = Wheel 7 | 8 | def get_default(self) -> Wheel: 9 | """ 10 | :return: the default OEM wheel 11 | """ 12 | session = self.Session() 13 | return session.query(Wheel).get(376) 14 | -------------------------------------------------------------------------------- /backend/sql/0.0.2/20190801_0_ddl_create_decal_detail.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE decal_detail 2 | ( 3 | id SERIAL NOT NULL, 4 | replay_id INTEGER, 5 | name VARCHAR(255) NOT NULL, 6 | quality INTEGER NOT NULL, 7 | icon VARCHAR(255) NOT NULL, 8 | paintable BOOLEAN NOT NULL, 9 | PRIMARY KEY (id) 10 | ); 11 | -------------------------------------------------------------------------------- /frontend/src/app/shared/confirm-dialog/confirm-dialog.component.html: -------------------------------------------------------------------------------- 1 |

{{prompt}}

2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /backend/rocket/paint.py: -------------------------------------------------------------------------------- 1 | PAINT_NONE = 0 # is this actually used? 2 | PAINT_CRIMSON = 1 3 | PAINT_LIME = 2 4 | PAINT_BLACK = 3 5 | PAINT_SKY_BLUE = 4 6 | PAINT_COBALT = 5 7 | PAINT_BURNT_SIENNA = 6 8 | PAINT_FOREST_GREEN = 7 9 | PAINT_PURPLE = 8 10 | PAINT_PINK = 9 11 | PAINT_ORANGE = 10 12 | PAINT_GREY = 11 13 | PAINT_TITANIUM_WHITE = 12 14 | PAINT_SAFFRON = 13 15 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/item-list/item-list.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | display: flex; 4 | justify-content: center; 5 | } 6 | 7 | mat-card { 8 | max-width: 100%; 9 | width: 1140px; 10 | margin: 16px 0; 11 | } 12 | 13 | .floating-action-button { 14 | position: fixed; 15 | right: 64px; 16 | bottom: 64px; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "angularCompilerOptions": { 3 | "enableIvy": false 4 | }, 5 | "extends": "./tsconfig.json", 6 | "compilerOptions": { 7 | "outDir": "./out-tsc/app", 8 | "types": [] 9 | }, 10 | "files": [ 11 | "src/main.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /backend/wsgi.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from server import app, logging_config 3 | from utils.network import log_endpoints 4 | from _version import __version__ 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | logging_config() 9 | log.info(f'Running rl-loadout {__version__} with wsgi using socket file') 10 | log_endpoints(log, app) 11 | 12 | if __name__ == '__main__': 13 | app.run() 14 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/about-dialog/about-dialog.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../theme"; 2 | 3 | a, a:visited, a:hover, a:active { 4 | color: map-get($app-primary, 300); 5 | } 6 | 7 | .about-logo { 8 | width: 200px; 9 | } 10 | 11 | .title-container { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | flex-flow: column; 16 | } 17 | -------------------------------------------------------------------------------- /backend/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import configparser 4 | 5 | log = logging.getLogger(__name__) 6 | 7 | ETC_CONFIG = '/etc/rl-loadout/config.ini' 8 | CONFIG_FILE = 'config.ini' 9 | 10 | config = configparser.ConfigParser() 11 | 12 | if os.path.exists(ETC_CONFIG): 13 | config.read(ETC_CONFIG) 14 | 15 | if os.path.exists(CONFIG_FILE): 16 | config.read(CONFIG_FILE) 17 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/api-keys/api-keys.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | display: flex; 4 | justify-content: center; 5 | } 6 | 7 | mat-card { 8 | max-width: 100%; 9 | width: 1140px; 10 | margin: 16px 0; 11 | } 12 | 13 | .floating-action-button { 14 | position: fixed; 15 | right: 64px; 16 | bottom: 64px; 17 | } 18 | 19 | .inactive { 20 | color: red; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/main/main.component.scss: -------------------------------------------------------------------------------- 1 | .container-div { 2 | height: 100%; 3 | min-height: 100%; 4 | } 5 | 6 | .content { 7 | padding-top: 56px; 8 | height: 100%; 9 | min-height: 100%; 10 | } 11 | 12 | mat-toolbar { 13 | top: 0; 14 | left: 0; 15 | right: 0; 16 | position: fixed; 17 | height: 56px; 18 | z-index: 100; 19 | } 20 | 21 | .title { 22 | padding: 0 16px; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/app/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AuthService = TestBed.inject(AuthService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /backend/config.ini: -------------------------------------------------------------------------------- 1 | [server] 2 | port = 10000 3 | jwt_secret = dummy_secret 4 | host = http://localhost:10000 5 | 6 | [database] 7 | driver = postgresql+psycopg2 8 | host = localhost 9 | port = 5432 10 | database = rl_loadout 11 | username = rl_loadout 12 | password = dev 13 | create_all = true 14 | 15 | [log] 16 | level = DEBUG 17 | file = 18 | 19 | [assets] 20 | host = https://storage.googleapis.com/rl-loadout-dev 21 | -------------------------------------------------------------------------------- /frontend/src/app/service/api-keys.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ApiKeysService } from './api-keys.service'; 4 | 5 | describe('ApiKeysService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ApiKeysService = TestBed.inject(ApiKeysService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/service/items/bodies.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { BodiesService } from './bodies.service'; 4 | 5 | describe('BodiesService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: BodiesService = TestBed.inject(BodiesService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/service/items/decals.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { DecalsService } from './decals.service'; 4 | 5 | describe('DecalsService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: DecalsService = TestBed.inject(DecalsService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/service/items/wheels.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { WheelsService } from './wheels.service'; 4 | 5 | describe('WheelsService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: WheelsService = TestBed.inject(WheelsService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/service/loadout.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { LoadoutService } from './loadout.service'; 4 | 5 | describe('LoadoutService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: LoadoutService = TestBed.inject(LoadoutService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/service/texture.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TextureService } from './texture.service'; 4 | 5 | describe('TextureService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: TextureService = TestBed.inject(TextureService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/product-upload/product-upload.component.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 | 6 | {{message}} 7 |
8 | -------------------------------------------------------------------------------- /frontend/src/app/service/items/toppers.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ToppersService } from './toppers.service'; 4 | 5 | describe('ToppersService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ToppersService = TestBed.inject(ToppersService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/service/items/antennas.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AntennasService } from './antennas.service'; 4 | 5 | describe('AntennasService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AntennasService = TestBed.inject(AntennasService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /backend/sql/0.0.2/20190801_0_ddl_create_wheel.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE wheel 2 | ( 3 | id SERIAL NOT NULL, 4 | replay_id INTEGER, 5 | name VARCHAR(255) NOT NULL, 6 | quality INTEGER NOT NULL, 7 | icon VARCHAR(255) NOT NULL, 8 | paintable BOOLEAN NOT NULL, 9 | model VARCHAR(255) NOT NULL, 10 | rim_base VARCHAR(255), 11 | rim_rgb_map VARCHAR(255), 12 | PRIMARY KEY (id) 13 | ); 14 | -------------------------------------------------------------------------------- /frontend/src/app/service/cloud-storage.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CloudStorageService } from './cloud-storage.service'; 4 | 5 | describe('CloudStorageService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: CloudStorageService = TestBed.inject(CloudStorageService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/service/loadout-store.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { LoadoutStoreService } from './loadout-store.service'; 4 | 5 | describe('LoadoutStoreService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: LoadoutStoreService = TestBed.inject(LoadoutStoreService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /backend/sql/0.0.2/20190801_1_ddl_create_decal.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE decal 2 | ( 3 | id SERIAL NOT NULL, 4 | base_texture VARCHAR(255), 5 | rgba_map VARCHAR(255) NOT NULL, 6 | body_id INTEGER, 7 | decal_detail_id INTEGER NOT NULL, 8 | quality INTEGER, 9 | PRIMARY KEY (id), 10 | FOREIGN KEY (body_id) REFERENCES body (id), 11 | FOREIGN KEY (decal_detail_id) REFERENCES decal_detail (id) 12 | ); 13 | -------------------------------------------------------------------------------- /backend/print_ddl.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import entity 3 | from sqlalchemy.schema import CreateTable 4 | from sqlalchemy.dialects import postgresql 5 | 6 | 7 | table_classes = list(map(lambda x: x[1], inspect.getmembers(entity, inspect.isclass))) 8 | table_classes = list(filter(lambda cls: issubclass(cls, entity.Base) and cls != entity.Base, table_classes)) 9 | 10 | for cls in table_classes: 11 | print(CreateTable(cls.__table__).compile(dialect=postgresql.dialect())) 12 | -------------------------------------------------------------------------------- /backend/sql/0.0.2/20190801_0_ddl_create_topper.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE topper 2 | ( 3 | id SERIAL NOT NULL, 4 | replay_id INTEGER, 5 | name VARCHAR(255) NOT NULL, 6 | quality INTEGER NOT NULL, 7 | icon VARCHAR(255) NOT NULL, 8 | paintable BOOLEAN NOT NULL, 9 | model VARCHAR(255) NOT NULL, 10 | base_texture VARCHAR(255), 11 | rgba_map VARCHAR(255), 12 | PRIMARY KEY (id) 13 | ); 14 | -------------------------------------------------------------------------------- /backend/utils/network/__init__.py: -------------------------------------------------------------------------------- 1 | from config import config 2 | 3 | 4 | def log_endpoints(log, app): 5 | log.info(f'Registered {len(list(app.url_map.iter_rules()))} endpoints:') 6 | for rule in app.url_map.iter_rules(): 7 | log.info(f'[{rule.methods}] {rule.rule} -> {rule.endpoint}') 8 | 9 | 10 | def get_asset_url(path: str) -> str or None: 11 | if path is None: 12 | return None 13 | 14 | return f'{config.get("assets", "host")}/{path}' 15 | -------------------------------------------------------------------------------- /frontend/src/app/service/items/bodies.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AbstractItemService } from '../abstract-item-service'; 3 | import { Body } from 'rl-loadout-lib'; 4 | import { HttpClient } from '@angular/common/http'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class BodiesService extends AbstractItemService { 10 | constructor(httpClient: HttpClient) { 11 | super('bodies', httpClient); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/service/product.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductService } from './product.service'; 4 | 5 | describe('ProductService', () => { 6 | let service: ProductService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ProductService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /frontend/src/app/service/items/antenna-sticks.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AntennaSticksService } from './antenna-sticks.service'; 4 | 5 | describe('AntennaSticksService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AntennaSticksService = TestBed.inject(AntennaSticksService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/service/items/decals.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { AbstractItemService } from '../abstract-item-service'; 4 | import { Decal } from 'rl-loadout-lib'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class DecalsService extends AbstractItemService { 10 | constructor(httpClient: HttpClient) { 11 | super('decals', httpClient); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/service/items/wheels.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { AbstractItemService } from '../abstract-item-service'; 4 | import { Wheel } from 'rl-loadout-lib'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class WheelsService extends AbstractItemService { 10 | constructor(httpClient: HttpClient) { 11 | super('wheels', httpClient); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/service/items/toppers.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { AbstractItemService } from '../abstract-item-service'; 4 | import { Topper } from 'rl-loadout-lib'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class ToppersService extends AbstractItemService { 10 | constructor(httpClient: HttpClient) { 11 | super('toppers', httpClient); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/dao/api_key.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound 2 | from entity import ApiKey 3 | from .dao import BaseDao 4 | 5 | 6 | class ApiKeyDao(BaseDao): 7 | T = ApiKey 8 | 9 | def get_by_value(self, key: str) -> ApiKey or None: 10 | session = self.Session() 11 | try: 12 | return session.query(ApiKey).filter(ApiKey.key == key).one() 13 | except (MultipleResultsFound, NoResultFound): 14 | return None 15 | -------------------------------------------------------------------------------- /backend/sql/0.2.2/20190807_0_dml_increment_qualities.sql: -------------------------------------------------------------------------------- 1 | UPDATE antenna 2 | SET quality = quality + 1 3 | WHERE quality > 0; 4 | UPDATE body 5 | SET quality = quality + 1 6 | WHERE quality > 0; 7 | UPDATE decal 8 | SET quality = quality + 1 9 | WHERE quality > 0; 10 | UPDATE decal_detail 11 | SET quality = quality + 1 12 | WHERE quality > 0; 13 | UPDATE topper 14 | SET quality = quality + 1 15 | WHERE quality > 0; 16 | UPDATE wheel 17 | SET quality = quality + 1 18 | WHERE quality > 0; 19 | -------------------------------------------------------------------------------- /backend/sql/0.6.0/20190921_2_create_table_decals.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE decal 2 | ( 3 | name VARCHAR(255) NOT NULL, 4 | quality INTEGER NOT NULL, 5 | icon VARCHAR(255) NOT NULL, 6 | paintable BOOLEAN NOT NULL, 7 | id SERIAL NOT NULL, 8 | base_texture VARCHAR(255), 9 | rgba_map VARCHAR(255) NOT NULL, 10 | body_id INTEGER, 11 | PRIMARY KEY (id), 12 | FOREIGN KEY (body_id) REFERENCES body (id) 13 | ); 14 | -------------------------------------------------------------------------------- /frontend/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /frontend/src/app/service/items/antennas.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { AbstractItemService } from '../abstract-item-service'; 4 | import { Antenna } from 'rl-loadout-lib'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AntennasService extends AbstractItemService { 10 | constructor(httpClient: HttpClient) { 11 | super('antennas', httpClient); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "theme.scss"; 2 | @import "~angular-notifier/styles.scss"; 3 | 4 | html, body { 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | body { 10 | color: white; 11 | background-color: $background; 12 | margin: 0; 13 | font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 14 | } 15 | 16 | .flex-spacer { 17 | flex-grow: 1; 18 | } 19 | 20 | .dg li:not(.folder).stats { 21 | height: auto; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/theme.scss: -------------------------------------------------------------------------------- 1 | //@import '~@angular/material/prebuilt-themes/purple-green.css'; 2 | @import '~@angular/material/theming'; 3 | 4 | @include mat-core(); 5 | 6 | $app-primary: mat-palette($mat-blue); 7 | $app-accent: mat-palette($mat-pink, A200, A100, A400); 8 | $app-warn: mat-palette($mat-red); 9 | $app-theme: mat-dark-theme($app-primary, $app-accent, $app-warn); 10 | 11 | @include angular-material-theme($app-theme); 12 | 13 | $background: #303030; 14 | 15 | $toolbar-height: 56px; 16 | -------------------------------------------------------------------------------- /frontend/src/app/home/pipes/item-filter.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { Item } from 'rl-loadout-lib'; 3 | 4 | @Pipe({ 5 | name: 'itemFilter', 6 | pure: false 7 | }) 8 | export class ItemFilterPipe implements PipeTransform { 9 | 10 | transform(items: Item[], filter: string): Item[] { 11 | if (!items || !filter) { 12 | return items; 13 | } 14 | return items.filter(item => item.name.toLowerCase().includes(filter.toLowerCase())); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/app/service/items/antenna-sticks.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { AbstractItemService } from '../abstract-item-service'; 4 | import { AntennaStick } from 'rl-loadout-lib'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AntennaSticksService extends AbstractItemService { 10 | constructor(httpClient: HttpClient) { 11 | super('antenna-sticks', httpClient); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Admin login 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/about-dialog/about-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { environment } from '../../../../environments/environment'; 3 | 4 | @Component({ 5 | selector: 'app-about-dialog', 6 | templateUrl: './about-dialog.component.html', 7 | styleUrls: ['./about-dialog.component.scss'] 8 | }) 9 | export class AboutDialogComponent implements OnInit { 10 | 11 | version = environment.version; 12 | 13 | constructor() { } 14 | 15 | ngOnInit() { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /backend/sql/0.0.2/20190801_0_ddl_create_body.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE body 2 | ( 3 | id SERIAL NOT NULL, 4 | replay_id INTEGER, 5 | name VARCHAR(255) NOT NULL, 6 | quality INTEGER NOT NULL, 7 | icon VARCHAR(255) NOT NULL, 8 | paintable BOOLEAN NOT NULL, 9 | model VARCHAR(255) NOT NULL, 10 | blank_skin VARCHAR(255) NOT NULL, 11 | base_skin VARCHAR(255), 12 | chassis_base VARCHAR(255), 13 | chassis_n VARCHAR(255), 14 | PRIMARY KEY (id) 15 | ); 16 | -------------------------------------------------------------------------------- /backend/sql/0.5.0/20190824_0_ddl_remove_autoincrement.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.antenna ALTER COLUMN id DROP DEFAULT; 2 | ALTER TABLE public.body ALTER COLUMN id DROP DEFAULT; 3 | ALTER TABLE public.decal ALTER COLUMN id DROP DEFAULT; 4 | ALTER TABLE public.topper ALTER COLUMN id DROP DEFAULT; 5 | ALTER TABLE public.wheel ALTER COLUMN id DROP DEFAULT; 6 | 7 | DROP SEQUENCE public.antenna_id_seq; 8 | DROP SEQUENCE public.body_id_seq; 9 | DROP SEQUENCE public.decal_id_seq; 10 | DROP SEQUENCE public.topper_id_seq; 11 | DROP SEQUENCE public.wheel_id_seq; 12 | -------------------------------------------------------------------------------- /frontend/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component'; 4 | import { SharedMaterialModule } from '../shared-material/shared-material.module'; 5 | 6 | 7 | 8 | @NgModule({ 9 | declarations: [ConfirmDialogComponent], 10 | imports: [ 11 | CommonModule, 12 | SharedMaterialModule 13 | ], 14 | entryComponents: [ConfirmDialogComponent] 15 | }) 16 | export class SharedModule { } 17 | -------------------------------------------------------------------------------- /frontend/src/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/assets/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#e2e2e2", 17 | "background_color": "#e2e2e2", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/service/texture.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Texture } from 'three'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class TextureService { 8 | 9 | private textures: { [key: string]: Texture } = {}; 10 | 11 | constructor() { 12 | } 13 | 14 | set(key: string, t: Texture) { 15 | this.textures[key] = t; 16 | } 17 | 18 | get(key: string) { 19 | return this.textures[key]; 20 | } 21 | 22 | getKeys() { 23 | return Object.keys(this.textures); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/sql/0.9.0/20191129_1_ddl_add_foreign_keys.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE antenna 2 | ADD CONSTRAINT constraint_name FOREIGN KEY (id) REFERENCES product (id); 3 | ALTER TABLE body 4 | ADD CONSTRAINT constraint_name FOREIGN KEY (id) REFERENCES product (id); 5 | ALTER TABLE decal 6 | ADD CONSTRAINT constraint_name FOREIGN KEY (id) REFERENCES product (id); 7 | ALTER TABLE topper 8 | ADD CONSTRAINT constraint_name FOREIGN KEY (id) REFERENCES product (id); 9 | ALTER TABLE wheel 10 | ADD CONSTRAINT constraint_name FOREIGN KEY (id) REFERENCES product (id); 11 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/debug/texture-viewer/texture-viewer.component.html: -------------------------------------------------------------------------------- 1 |

Texture viewer

2 | 3 | 4 | Part 5 | 6 | 7 | {{texture}} 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /backend/sql/0.0.2/20190801_1_ddl_create_antenna.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE antenna 2 | ( 3 | id SERIAL NOT NULL, 4 | replay_id INTEGER, 5 | name VARCHAR(255) NOT NULL, 6 | quality INTEGER NOT NULL, 7 | icon VARCHAR(255) NOT NULL, 8 | paintable BOOLEAN NOT NULL, 9 | model VARCHAR(255) NOT NULL, 10 | base_texture VARCHAR(255), 11 | rgba_map VARCHAR(255), 12 | stick_id INTEGER NOT NULL, 13 | PRIMARY KEY (id), 14 | FOREIGN KEY (stick_id) REFERENCES antenna_stick (id) 15 | ); 16 | -------------------------------------------------------------------------------- /frontend/src/app/auth/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate } from '@angular/router'; 3 | import { AuthService } from './auth.service'; 4 | 5 | @Injectable({providedIn: 'root'}) 6 | export class AuthGuardService implements CanActivate { 7 | constructor(public auth: AuthService, public router: Router) { 8 | } 9 | 10 | canActivate(): boolean { 11 | if (!this.auth.isAuthenticated()) { 12 | this.router.navigate(['/admin/login']).then(); 13 | return false; 14 | } 15 | return true; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/app/utils/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copies a text to the clip board 3 | * @param val rhe text that will be copied to the clipboard 4 | */ 5 | export function copyMessage(val: string) { 6 | const selBox = document.createElement('textarea'); 7 | selBox.style.position = 'fixed'; 8 | selBox.style.left = '0'; 9 | selBox.style.top = '0'; 10 | selBox.style.opacity = '0'; 11 | selBox.value = val; 12 | document.body.appendChild(selBox); 13 | selBox.focus(); 14 | selBox.select(); 15 | document.execCommand('copy'); 16 | document.body.removeChild(selBox); 17 | } 18 | -------------------------------------------------------------------------------- /backend/auth.py: -------------------------------------------------------------------------------- 1 | import bcrypt 2 | 3 | 4 | def hash_password(password: str) -> str: 5 | """Hash a password for storing.""" 6 | password = password.encode('utf-8') 7 | return bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8') 8 | 9 | 10 | def verify_password(stored_password: str, provided_password: str) -> bool: 11 | """Verify a stored password against one provided by user""" 12 | provided_password = provided_password.encode('utf-8') 13 | stored_password = stored_password.encode('utf-8') 14 | return bcrypt.checkpw(provided_password, stored_password) 15 | -------------------------------------------------------------------------------- /backend/controllers/bodies/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from dao import BodyDao 3 | from utils.network.exc import NotFoundException 4 | 5 | body_dao = BodyDao() 6 | 7 | 8 | def get(): 9 | bodies = body_dao.get_all() 10 | return jsonify([body.to_dict() for body in bodies]), 200 11 | 12 | 13 | def get_by_id(id: int): 14 | """ 15 | :param id: ID of the item (in-game item ID). 16 | """ 17 | body = body_dao.get(id) 18 | 19 | if body is None: 20 | raise NotFoundException('Body not found') 21 | 22 | return jsonify(body.to_dict()), 200 23 | -------------------------------------------------------------------------------- /frontend/src/app/utils/color.ts: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | 3 | /** 4 | * Get the text color for the background to make it readable. 5 | * https://www.w3.org/TR/AERT/#color-contrast 6 | * 7 | * @param backgroundColor color of background in #FFFFFF format 8 | */ 9 | export function getTextColor(backgroundColor: Color) { 10 | if (backgroundColor == undefined) { 11 | return 'white'; 12 | } 13 | const o = Math.round(((backgroundColor.r * 76245) + (backgroundColor.g * 149685) + (backgroundColor.b * 29070)) / 1000); 14 | return (o > 125) ? 'black' : 'white'; 15 | } 16 | -------------------------------------------------------------------------------- /backend/controllers/decals/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from dao import DecalDao 3 | from utils.network.exc import NotFoundException 4 | 5 | decal_dao = DecalDao() 6 | 7 | 8 | def get(): 9 | decals = decal_dao.get_all() 10 | return jsonify([decal.to_dict() for decal in decals]), 200 11 | 12 | 13 | def get_by_id(id: int): 14 | """ 15 | :param id: ID of the item (in-game item ID). 16 | """ 17 | decal = decal_dao.get(id) 18 | 19 | if decal is None: 20 | raise NotFoundException('Wheel not found') 21 | 22 | return jsonify(decal.to_dict()), 200 23 | -------------------------------------------------------------------------------- /backend/controllers/wheels/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from dao import WheelDao 3 | from utils.network.exc import NotFoundException 4 | 5 | wheel_dao = WheelDao() 6 | 7 | 8 | def get(): 9 | wheels = wheel_dao.get_all() 10 | return jsonify([wheel.to_dict() for wheel in wheels]), 200 11 | 12 | 13 | def get_by_id(id: int): 14 | """ 15 | :param id: ID of the item (in-game item ID). 16 | """ 17 | wheel = wheel_dao.get(id) 18 | 19 | if wheel is None: 20 | raise NotFoundException('Wheel not found') 21 | 22 | return jsonify(wheel.to_dict()), 200 23 | -------------------------------------------------------------------------------- /backend/entity/product.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | from .base import Base 3 | 4 | 5 | class Product(Base): 6 | __tablename__ = 'product' 7 | 8 | id = Column(Integer, primary_key=True) 9 | type = Column(String(255), nullable=False) 10 | product_name = Column(String(255), nullable=False) 11 | name = Column(String(255), nullable=False) 12 | 13 | def to_dict(self): 14 | return { 15 | 'id': self.id, 16 | 'type': self.type, 17 | 'product_name': self.product_name, 18 | 'name': self.name 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/loadout-toolbar/loadout-toolbar.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /backend/controllers/toppers/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from dao import TopperDao 3 | from utils.network.exc import NotFoundException 4 | 5 | topper_dao = TopperDao() 6 | 7 | 8 | def get(): 9 | toppers = topper_dao.get_all() 10 | return jsonify([topper.to_dict() for topper in toppers]), 200 11 | 12 | 13 | def get_by_id(id: int): 14 | """ 15 | :param id: ID of the item (in-game item ID). 16 | """ 17 | topper = topper_dao.get(id) 18 | 19 | if topper is None: 20 | raise NotFoundException('Wheel not found') 21 | 22 | return jsonify(topper.to_dict()), 200 23 | -------------------------------------------------------------------------------- /frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { HomeComponent } from './home/components/home/home.component'; 4 | 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: HomeComponent 10 | }, 11 | { 12 | path: 'admin', 13 | loadChildren: () => import('./admin/admin.module').then(mod => mod.AdminModule) 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forRoot(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class AppRoutingModule { 22 | } 23 | -------------------------------------------------------------------------------- /backend/controllers/antennas/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from dao import AntennaDao 3 | from utils.network.exc import NotFoundException 4 | 5 | antenna_dao = AntennaDao() 6 | 7 | 8 | def get(): 9 | antennas = antenna_dao.get_all() 10 | return jsonify([antenna.to_dict() for antenna in antennas]), 200 11 | 12 | 13 | def get_by_id(id: int): 14 | """ 15 | :param id: ID of the item (in-game item ID). 16 | """ 17 | antenna = antenna_dao.get(id) 18 | 19 | if antenna is None: 20 | raise NotFoundException('Wheel not found') 21 | 22 | return jsonify(antenna.to_dict()), 200 23 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/api-keys/create-api-key/create-api-key.component.html: -------------------------------------------------------------------------------- 1 |

Edit Antenna

2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | Active 10 |
11 |
12 | 13 | 14 |
15 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-antenna-stick/create-antenna-stick.component.html: -------------------------------------------------------------------------------- 1 |

Edit Antenna Stick

2 |
3 | 4 | 5 | {{obj}} 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/main/main.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AuthService } from '../../../auth/auth.service'; 4 | 5 | @Component({ 6 | selector: 'app-main', 7 | templateUrl: './main.component.html', 8 | styleUrls: ['./main.component.scss'] 9 | }) 10 | export class MainComponent implements OnInit { 11 | 12 | username: string; 13 | 14 | constructor(public router: Router, 15 | private authService: AuthService) { } 16 | 17 | ngOnInit() { 18 | this.username = this.authService.getUsername(); 19 | } 20 | 21 | logout() { 22 | this.authService.logout(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tools/batch_download.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import argparse 3 | import os 4 | import urllib.request 5 | 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument('--bucket', '-b', type=str, required=True) 8 | args = parser.parse_args() 9 | 10 | url = f'https://www.googleapis.com/storage/v1/b/{args.bucket}/o' 11 | 12 | objects = requests.get(url).json()['items'] 13 | objects = list( 14 | filter(lambda x: x['name'].startswith('models/') and x['name'].endswith('.glb') and len(x['name']) > 7, objects)) 15 | 16 | if not os.path.exists('models'): 17 | os.makedirs('models') 18 | 19 | for obj in objects: 20 | print(f'Downloading {obj["name"]}...') 21 | urllib.request.urlretrieve(obj['mediaLink'], obj['name']) 22 | -------------------------------------------------------------------------------- /frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/about-dialog/about-dialog.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Rocket Loadout — {{version}}

4 |
5 |
6 |

Create and share car designs for Rocket League. Built by klay.

7 |

Rocket Loadout is not affiliated with Psyonix, Inc. All models and textures belong to Psyonix, Inc.

8 |

View source on GitHub.

9 |

Hitbox data provided by Trelgne.

10 |
11 | -------------------------------------------------------------------------------- /backend/api/api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import secrets 3 | from dao import ApiKeyDao 4 | from utils.network.exc import UnauthorizedException 5 | 6 | log = logging.getLogger(__name__) 7 | api_key_dao = ApiKeyDao() 8 | 9 | 10 | def verify_api_key(apikey: str, required_scopes): 11 | api_key_record = api_key_dao.get_by_value(apikey) 12 | 13 | if api_key_record is None: 14 | log.info(f"Unknown API key: {apikey}") 15 | raise UnauthorizedException('Provided API key is invalid') 16 | 17 | if not api_key_record.active: 18 | raise UnauthorizedException('Provided API key is inactive') 19 | 20 | return {'sub': api_key_record.name} 21 | 22 | 23 | def generate_api_key() -> str: 24 | return secrets.token_hex(16) 25 | -------------------------------------------------------------------------------- /backend/blueprints/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import api_blueprint 2 | from .api_keys import api_keys_blueprint 3 | from .auth import auth_blueprint 4 | from .bodies import bodies_blueprint 5 | from .wheels import wheels_blueprint 6 | from .toppers import toppers_blueprint 7 | from .decals import decals_blueprint 8 | from .antenna_sticks import antenna_sticks_blueprint 9 | from .antennas import antennas_blueprint 10 | from .product import products_blueprint 11 | 12 | blueprints = [ 13 | api_blueprint, 14 | api_keys_blueprint, 15 | auth_blueprint, 16 | bodies_blueprint, 17 | wheels_blueprint, 18 | toppers_blueprint, 19 | decals_blueprint, 20 | antennas_blueprint, 21 | antenna_sticks_blueprint, 22 | products_blueprint 23 | ] 24 | -------------------------------------------------------------------------------- /frontend/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to rl-loadout!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/canvas/canvas.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | Loading {{mathRound(progress.percent * 100) / 100}}% 8 | 9 |
10 |
11 | 12 |
13 | Loading {{mathRound(progress.percent * 100) / 100}}% 14 | 15 |
16 | -------------------------------------------------------------------------------- /backend/dao/dao.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from database import database 3 | 4 | 5 | class BaseDao(object): 6 | T = None 7 | 8 | def __init__(self): 9 | self.Session = database.Session 10 | 11 | def get(self, record_id) -> T: 12 | if record_id is None: 13 | return None 14 | session = self.Session() 15 | return session.query(self.T).get(record_id) 16 | 17 | def get_all(self) -> List[T]: 18 | session = self.Session() 19 | return session.query(self.T) 20 | 21 | def add(self, record: T): 22 | self.Session().add(record) 23 | 24 | def delete(self, record_id): 25 | session = self.Session() 26 | session.query(self.T).filter(self.T.id == record_id).delete() 27 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/main/main.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MainComponent } from './main.component'; 4 | 5 | describe('MainComponent', () => { 6 | let component: MainComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ MainComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MainComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /backend/dao/antenna.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from .item import BaseItemDao 3 | from entity import Antenna, AntennaStick 4 | 5 | 6 | class AntennaDao(BaseItemDao): 7 | T = Antenna 8 | 9 | def get_sticks(self) -> List[AntennaStick]: 10 | session = self.Session() 11 | return session.query(AntennaStick) 12 | 13 | def get_stick(self, stick_id) -> AntennaStick: 14 | session = self.Session() 15 | return session.query(AntennaStick).get(stick_id) 16 | 17 | def delete_stick(self, antenna_stick_id: int): 18 | session = self.Session() 19 | session.query(AntennaStick).filter(AntennaStick.id == antenna_stick_id).delete() 20 | 21 | def add_stick(self, antenna_stick: AntennaStick): 22 | self.Session().add(antenna_stick) 23 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/canvas/canvas.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CanvasComponent } from './canvas.component'; 4 | 5 | describe('CanvasComponent', () => { 6 | let component: CanvasComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CanvasComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CanvasComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tools/compress_models.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | from subprocess import Popen, PIPE 4 | 5 | parser = argparse.ArgumentParser() 6 | parser.add_argument('--dir', '-d', type=str, required=True) 7 | args = parser.parse_args() 8 | 9 | if not os.path.exists('draco'): 10 | os.makedirs('draco') 11 | 12 | for file in filter(lambda x: x.endswith('.glb'), os.listdir(args.dir)): 13 | command = f'gltf-pipeline -i {os.path.join(args.dir, file)} -o {os.path.join("draco", file)} -d' 14 | print(f'Running {command}') 15 | 16 | p = Popen(command, stdout=PIPE, shell=True) 17 | for line in p.stdout: 18 | print(line.decode("utf-8"), end='') 19 | p.wait() 20 | 21 | if p.returncode != 0: 22 | print(f'gltf-pipeline returned with non-zero exit code: {p.returncode}') 23 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/bodies/bodies.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BodiesComponent } from './bodies.component'; 4 | 5 | describe('BodiesComponent', () => { 6 | let component: BodiesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ BodiesComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(BodiesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/decals/decals.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DecalsComponent } from './decals.component'; 4 | 5 | describe('DecalsComponent', () => { 6 | let component: DecalsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DecalsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DecalsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/wheels/wheels.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { WheelsComponent } from './wheels.component'; 4 | 5 | describe('WheelsComponent', () => { 6 | let component: WheelsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ WheelsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(WheelsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/api-keys/api-keys.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ApiKeysComponent } from './api-keys.component'; 4 | 5 | describe('ApiKeysComponent', () => { 6 | let component: ApiKeysComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ApiKeysComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ApiKeysComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/loadout-toolbar/loadout-toolbar.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../theme"; 2 | 3 | .toolbar-container { 4 | height: 100%; 5 | width: 100%; 6 | display: flex; 7 | flex-flow: row; 8 | } 9 | 10 | .toolbar { 11 | height: 100%; 12 | display: flex; 13 | flex-flow: column; 14 | background-color: $background; 15 | overflow-y: auto; 16 | z-index: 100; 17 | box-shadow: -12px 0px 35px 15px rgba(0, 0, 0, 0.4); 18 | } 19 | 20 | .toolbar-button { 21 | cursor: pointer; 22 | padding: 0 8px; 23 | } 24 | 25 | .toolbar-button.selected { 26 | background: rgba(255, 255, 255, 0.31); 27 | } 28 | 29 | .toolbar-button img { 30 | width: 48px; 31 | height: 48px; 32 | } 33 | 34 | .loadout-dropdown { 35 | height: 100%; 36 | background-color: $background; 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/app/utils/network.ts: -------------------------------------------------------------------------------- 1 | import { MatSnackBar } from '@angular/material/snack-bar'; 2 | 3 | export function handleErrorSnackbar(error, snackBar: MatSnackBar, msg?: string) { 4 | let message; 5 | if (msg) { 6 | message = msg; 7 | } else if ('detail' in error.error) { 8 | message = error.error.detail; 9 | } else { 10 | message = error.statusText; 11 | } 12 | snackBar.open(message, null, {duration: 2000}); 13 | } 14 | 15 | export function parseJwt(token: string) { 16 | const base64Url = token.split('.')[1]; 17 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); 18 | const jsonPayload = decodeURIComponent(atob(base64).split('') 19 | .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')); 20 | return JSON.parse(jsonPayload); 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/toppers/toppers.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ToppersComponent } from './toppers.component'; 4 | 5 | describe('ToppersComponent', () => { 6 | let component: ToppersComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ToppersComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ToppersComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /backend/dao/user.py: -------------------------------------------------------------------------------- 1 | from entity import User 2 | from .dao import BaseDao 3 | 4 | 5 | class UserDao(BaseDao): 6 | T = User 7 | 8 | def add_user(self, username: str, password: str): 9 | """ 10 | Add a user to the db 11 | :param username: 12 | :param password: 13 | """ 14 | session = self.Session() 15 | user = User(name=username.lower(), password=password) 16 | session.add(user) 17 | session.commit() 18 | 19 | def get_by_username(self, username: str): 20 | """ 21 | Find user by username 22 | :param username: username 23 | :return: 24 | """ 25 | session = self.Session() 26 | username = username.lower() 27 | return session.query(User).filter(User.name == username).first() 28 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/item-list/item-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ItemListComponent } from './item-list.component'; 4 | 5 | describe('ItemListComponent', () => { 6 | let component: ItemListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ItemListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ItemListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/antennas/antennas.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AntennasComponent } from './antennas.component'; 4 | 5 | describe('AntennasComponent', () => { 6 | let component: AntennasComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AntennasComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AntennasComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/bodies/bodies.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { ItemListComponent } from '../../item-list/item-list.component'; 3 | import { CreateBodyComponent } from '../../dialog/create-body/create-body.component'; 4 | import { BodiesService } from '../../../../service/items/bodies.service'; 5 | 6 | @Component({ 7 | selector: 'app-bodies', 8 | templateUrl: '../items.component.html', 9 | styleUrls: ['../items.component.scss'] 10 | }) 11 | export class BodiesComponent implements OnInit { 12 | 13 | @ViewChild('itemListComponent', {static: true}) 14 | itemListComponent: ItemListComponent; 15 | 16 | dialogComponent = CreateBodyComponent; 17 | 18 | constructor(public itemService: BodiesService) { 19 | } 20 | 21 | ngOnInit() { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-body/create-body.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateBodyComponent } from './create-body.component'; 4 | 5 | describe('CreateBodyComponent', () => { 6 | let component: CreateBodyComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CreateBodyComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CreateBodyComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/decals/decals.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { ItemListComponent } from '../../item-list/item-list.component'; 3 | import { CreateDecalComponent } from '../../dialog/create-decal/create-decal.component'; 4 | import { DecalsService } from '../../../../service/items/decals.service'; 5 | 6 | @Component({ 7 | selector: 'app-decals', 8 | templateUrl: './decals.component.html', 9 | styleUrls: ['./decals.component.scss'] 10 | }) 11 | export class DecalsComponent implements OnInit { 12 | 13 | @ViewChild('itemListComponent', {static: true}) 14 | itemListComponent: ItemListComponent; 15 | 16 | dialogComponent = CreateDecalComponent; 17 | 18 | constructor(public itemService: DecalsService) { 19 | } 20 | 21 | ngOnInit() { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/wheels/wheels.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { ItemListComponent } from '../../item-list/item-list.component'; 3 | import { CreateWheelComponent } from '../../dialog/create-wheel/create-wheel.component'; 4 | import { WheelsService } from '../../../../service/items/wheels.service'; 5 | 6 | @Component({ 7 | selector: 'app-wheels', 8 | templateUrl: '../items.component.html', 9 | styleUrls: ['../items.component.scss'] 10 | }) 11 | export class WheelsComponent implements OnInit { 12 | 13 | @ViewChild('itemListComponent', {static: true}) 14 | itemListComponent: ItemListComponent; 15 | 16 | dialogComponent = CreateWheelComponent; 17 | 18 | constructor(public itemService: WheelsService) { 19 | } 20 | 21 | ngOnInit() { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/app/auth/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; 3 | import { AuthService } from './auth.service'; 4 | import { Observable } from 'rxjs'; 5 | import { environment } from '../../environments/environment'; 6 | 7 | @Injectable() 8 | export class AuthInterceptor implements HttpInterceptor { 9 | constructor(public auth: AuthService) {} 10 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 11 | 12 | if (request.url.startsWith(environment.backend)) { 13 | request = request.clone({ 14 | setHeaders: { 15 | Authorization: `Bearer ${this.auth.getToken()}` 16 | } 17 | }); 18 | } 19 | return next.handle(request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/about-dialog/about-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AboutDialogComponent } from './about-dialog.component'; 4 | 5 | describe('AboutDialogComponent', () => { 6 | let component: AboutDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AboutDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AboutDialogComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-decal/create-decal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateDecalComponent } from './create-decal.component'; 4 | 5 | describe('CreateDecalComponent', () => { 6 | let component: CreateDecalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CreateDecalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CreateDecalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-wheel/create-wheel.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateWheelComponent } from './create-wheel.component'; 4 | 5 | describe('CreateWheelComponent', () => { 6 | let component: CreateWheelComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CreateWheelComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CreateWheelComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/toppers/toppers.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { ItemListComponent } from '../../item-list/item-list.component'; 3 | import { CreateTopperComponent } from '../../dialog/create-topper/create-topper.component'; 4 | import { ToppersService } from '../../../../service/items/toppers.service'; 5 | 6 | @Component({ 7 | selector: 'app-toppers', 8 | templateUrl: '../items.component.html', 9 | styleUrls: ['../items.component.scss'] 10 | }) 11 | export class ToppersComponent implements OnInit { 12 | 13 | @ViewChild('itemListComponent', {static: true}) 14 | itemListComponent: ItemListComponent; 15 | 16 | dialogComponent = CreateTopperComponent; 17 | 18 | constructor(public itemService: ToppersService) { 19 | } 20 | 21 | ngOnInit() { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/app/shared/confirm-dialog/confirm-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ConfirmDialogComponent } from './confirm-dialog.component'; 4 | 5 | describe('ConfirmDialogComponent', () => { 6 | let component: ConfirmDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ConfirmDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ConfirmDialogComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /backend/dao/decal.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from .item import BaseItemDao 3 | from entity import Decal, Body, Product 4 | from rocket.ids import tier_floor 5 | 6 | 7 | class DecalDao(BaseItemDao): 8 | T = Decal 9 | 10 | def get_all_for_body(self, body_id: int) -> List: 11 | """ 12 | Find decals that are applicable to a body 13 | 14 | :param body_id: id of the body 15 | :return: decal 16 | """ 17 | session = self.Session() 18 | if body_id is not None: 19 | body_id = int(body_id) 20 | body = session.query(Body).get(body_id) 21 | if body is None: 22 | return [] 23 | return session.query(Decal, Product).join(Product).filter(Decal.body_id == tier_floor(body_id)) 24 | return session.query(Decal, Product).join(Product) 25 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-topper/create-topper.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateTopperComponent } from './create-topper.component'; 4 | 5 | describe('CreateTopperComponent', () => { 6 | let component: CreateTopperComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CreateTopperComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CreateTopperComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/api-keys/create-api-key/create-api-key.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateApiKeyComponent } from './create-api-key.component'; 4 | 5 | describe('CreateApiKeyComponent', () => { 6 | let component: CreateApiKeyComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CreateApiKeyComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CreateApiKeyComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/product-upload/product-upload.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductUploadComponent } from './product-upload.component'; 4 | 5 | describe('ProductUploadComponent', () => { 6 | let component: ProductUploadComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductUploadComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductUploadComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/color-selector/color-selector.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ColorSelectorComponent } from './color-selector.component'; 4 | 5 | describe('ColorSelectorComponent', () => { 6 | let component: ColorSelectorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ColorSelectorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ColorSelectorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/home/home.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../theme"; 2 | 3 | .main-container { 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | app-canvas { 9 | height: calc(100% - #{$toolbar-height}); 10 | } 11 | 12 | app-loadout-toolbar { 13 | height: calc(100% - #{$toolbar-height}); 14 | float: left; 15 | } 16 | 17 | mat-toolbar { 18 | height: 56px; 19 | z-index: 500; 20 | position: relative; 21 | box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); 22 | } 23 | 24 | .alpha-container { 25 | height: 32px; 26 | display: flex; 27 | align-items: flex-start; 28 | } 29 | 30 | .alpha { 31 | margin-left: 4px; 32 | font-size: 10px; 33 | line-height: 10px; 34 | } 35 | 36 | .logo { 37 | height: 50px; 38 | width: 50px; 39 | margin-right: 16px; 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-antenna/create-antenna.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateAntennaComponent } from './create-antenna.component'; 4 | 5 | describe('CreateAntennaComponent', () => { 6 | let component: CreateAntennaComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CreateAntennaComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CreateAntennaComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/antenna-sticks/antenna-sticks.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AntennaSticksComponent } from './antenna-sticks.component'; 4 | 5 | describe('AntennaSticksComponent', () => { 6 | let component: AntennaSticksComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AntennaSticksComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AntennaSticksComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/antennas/antennas.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { ItemListComponent } from '../../item-list/item-list.component'; 3 | import { CreateAntennaComponent } from '../../dialog/create-antenna/create-antenna.component'; 4 | import { AntennasService } from '../../../../service/items/antennas.service'; 5 | 6 | @Component({ 7 | selector: 'app-antennas', 8 | templateUrl: '../items.component.html', 9 | styleUrls: ['../items.component.scss'] 10 | }) 11 | export class AntennasComponent implements OnInit { 12 | 13 | @ViewChild('itemListComponent', {static: true}) 14 | itemListComponent: ItemListComponent; 15 | 16 | dialogComponent = CreateAntennaComponent; 17 | 18 | constructor(public itemService: AntennasService) { 19 | } 20 | 21 | ngOnInit() { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/debug/texture-viewer/texture-viewer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TextureViewerComponent } from './texture-viewer.component'; 4 | 5 | describe('TextureViewerComponent', () => { 6 | let component: TextureViewerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TextureViewerComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TextureViewerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/loadout-toolbar/loadout-toolbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoadoutToolbarComponent } from './loadout-toolbar.component'; 4 | 5 | describe('LoadoutToolbarComponent', () => { 6 | let component: LoadoutToolbarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoadoutToolbarComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoadoutToolbarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | 48 | *.iml 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | 48 | *.iml 49 | *.log 50 | .vagrant/ 51 | -------------------------------------------------------------------------------- /backend/logging_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import WatchedFileHandler 3 | from config import config 4 | 5 | 6 | LOG_FORMAT = '%(asctime)s %(levelname)7s %(name)s [%(threadName)s] : %(message)s' 7 | 8 | 9 | def logging_config(): 10 | level = logging.ERROR 11 | if config.get('log', 'level') == 'DEBUG': 12 | level = logging.DEBUG 13 | elif config.get('log', 'level') == 'INFO': 14 | level = logging.INFO 15 | elif config.get('log', 'level') == 'WARNING': 16 | level = logging.WARNING 17 | 18 | log_file = None 19 | if config.get('log', 'file'): 20 | log_file = config.get('log', 'file') 21 | 22 | if log_file: 23 | handlers = [WatchedFileHandler(log_file)] 24 | else: 25 | handlers = [logging.StreamHandler()] 26 | 27 | logging.basicConfig(level=level, handlers=handlers, format=LOG_FORMAT) 28 | -------------------------------------------------------------------------------- /backend/utils/network/exc.py: -------------------------------------------------------------------------------- 1 | class HttpException(Exception): 2 | 3 | def __init__(self, code: int, message: str): 4 | self.code = code 5 | self.message = message 6 | 7 | 8 | class BadRequestException(HttpException): 9 | 10 | def __init__(self, message: str): 11 | super(BadRequestException, self).__init__(400, message) 12 | 13 | 14 | class UnauthorizedException(HttpException): 15 | 16 | def __init__(self, message: str): 17 | super(UnauthorizedException, self).__init__(401, message) 18 | 19 | 20 | class NotFoundException(HttpException): 21 | 22 | def __init__(self, message: str): 23 | super(NotFoundException, self).__init__(404, message) 24 | 25 | 26 | class InternalServerErrorException(HttpException): 27 | 28 | def __init__(self, message: str): 29 | super(InternalServerErrorException, self).__init__(500, message) 30 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-antenna-stick/create-antenna-stick.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateAntennaStickComponent } from './create-antenna-stick.component'; 4 | 5 | describe('CreateAntennaStickComponent', () => { 6 | let component: CreateAntennaStickComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CreateAntennaStickComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CreateAntennaStickComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/loadout-grid-selector/loadout-grid-selector.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoadoutGridSelectorComponent } from './loadout-grid-selector.component'; 4 | 5 | describe('LoadoutGridSelectorComponent', () => { 6 | let component: LoadoutGridSelectorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoadoutGridSelectorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoadoutGridSelectorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /vm_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "deb http://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main" > /etc/apt/sources.list.d/pgdg.list 4 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - 5 | 6 | apt update 7 | apt install -y python3-pip python3-dev build-essential libssl-dev libffi-dev python3-setuptools libpq-dev python-psycopg2 postgresql-11 postgresql-client-11 8 | 9 | curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - 10 | apt install -y nodejs 11 | 12 | pip3 install -r /vagrant/backend/requirements.txt 13 | pip3 install uwsgi connexion[swagger-ui] 14 | 15 | sudo -u postgres -H sh -c "psql -c \"CREATE ROLE rl_loadout WITH LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOREPLICATION CONNECTION LIMIT -1 PASSWORD 'dev'\"" 16 | sudo -u postgres -H sh -c "psql -c \"CREATE DATABASE rl_loadout WITH OWNER = rl_loadout ENCODING = 'UTF8' CONNECTION LIMIT = -1;\"" 17 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/items/antenna-sticks/antenna-sticks.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { ItemListComponent } from '../../item-list/item-list.component'; 3 | import { CreateAntennaStickComponent } from '../../dialog/create-antenna-stick/create-antenna-stick.component'; 4 | import { AntennaSticksService } from '../../../../service/items/antenna-sticks.service'; 5 | 6 | @Component({ 7 | selector: 'app-antenna-sticks', 8 | templateUrl: './antenna-sticks.component.html', 9 | styleUrls: ['./antenna-sticks.component.scss'] 10 | }) 11 | export class AntennaSticksComponent implements OnInit { 12 | 13 | @ViewChild('itemListComponent', {static: true}) 14 | itemListComponent: ItemListComponent; 15 | 16 | dialogComponent = CreateAntennaStickComponent; 17 | 18 | constructor(public antennaSticksService: AntennaSticksService) { 19 | } 20 | 21 | ngOnInit() { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | // @ts-ignore 7 | version: require('../../package.json').version + '-DEV', 8 | production: false, 9 | assetHost: 'https://storage.googleapis.com/rl-loadout-dev', 10 | backend: 'http://localhost:10000' 11 | }; 12 | 13 | /* 14 | * For easier debugging in development mode, you can import the following file 15 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 16 | * 17 | * This import should be commented out in production mode because it will have a negative impact 18 | * on performance if an error is thrown. 19 | */ 20 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 21 | -------------------------------------------------------------------------------- /frontend/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /tools/batch_download_textures.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import argparse 3 | import os 4 | import urllib.request 5 | from multiprocessing.pool import ThreadPool 6 | 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument('--bucket', '-b', type=str, required=True) 9 | args = parser.parse_args() 10 | 11 | url = f'https://www.googleapis.com/storage/v1/b/{args.bucket}/o' 12 | 13 | objects = requests.get(url).json()['items'] 14 | objects = list( 15 | filter(lambda x: x['name'].startswith('textures/') and x['name'].endswith('.tga') and 16 | not x['name'].endswith('_S.tga') and len(x['name']) > 9, objects)) 17 | 18 | if not os.path.exists('textures'): 19 | os.makedirs('textures') 20 | 21 | 22 | def download(obj): 23 | print(f'Downloading {obj["name"]}...') 24 | urllib.request.urlretrieve(obj['mediaLink'], obj['name']) 25 | print(f'Downloaded {obj["name"]}') 26 | 27 | 28 | with ThreadPool(10) as p: 29 | p.map(download, objects) 30 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/product-upload/product-upload.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ProductService } from '../../../service/product.service'; 3 | 4 | @Component({ 5 | selector: 'app-product-upload', 6 | templateUrl: './product-upload.component.html', 7 | styleUrls: ['./product-upload.component.scss'] 8 | }) 9 | export class ProductUploadComponent implements OnInit { 10 | 11 | isLoading = false; 12 | message = ''; 13 | 14 | constructor(private productService: ProductService) { 15 | } 16 | 17 | ngOnInit() { 18 | } 19 | 20 | handleFileInput(files: FileList) { 21 | this.isLoading = true; 22 | this.message = ''; 23 | this.productService.uploadCsv(files.item(0)).subscribe(v => { 24 | this.isLoading = false; 25 | this.message = 'Done!'; 26 | }, error => { 27 | this.isLoading = false; 28 | this.message = 'Failed! Check the logs.'; 29 | console.error(error); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/blueprints/auth.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_jwt_extended import create_access_token 3 | from dao import UserDao 4 | from auth import verify_password 5 | from utils.network.decorators import json_required_params 6 | from utils.network.exc import UnauthorizedException 7 | 8 | auth_blueprint = Blueprint('auth', __name__,) 9 | user_dao = UserDao() 10 | 11 | 12 | @auth_blueprint.route('/auth', methods=['POST']) 13 | @json_required_params(['username', 'password']) 14 | def auth(): 15 | username = request.json['username'] 16 | password = request.json['password'] 17 | 18 | user = user_dao.get_by_username(username) 19 | 20 | if user is None: 21 | raise UnauthorizedException('Bad username or password') 22 | 23 | if not verify_password(user.password, password): 24 | raise UnauthorizedException('Bad username or password') 25 | 26 | access_token = create_access_token(identity=username) 27 | return jsonify(access_token=access_token), 200 28 | -------------------------------------------------------------------------------- /backend/entity/api_key.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, Boolean, Integer 2 | from .base import Base 3 | 4 | 5 | class ApiKey(Base): 6 | __tablename__ = 'api_key' 7 | 8 | id = Column(Integer, primary_key=True) 9 | key = Column(String(32), nullable=False, unique=True) 10 | name = Column(String(255), nullable=False) 11 | description = Column(String(255), nullable=False) 12 | active = Column(Boolean, nullable=False, default=False) 13 | 14 | def to_dict(self): 15 | return { 16 | 'id': self.id, 17 | 'key': self.key, 18 | 'name': self.name, 19 | 'description': self.description, 20 | 'active': self.active, 21 | } 22 | 23 | def apply_dict(self, values): 24 | self.id = values.get('id', None) 25 | self.key = values.get('key', None) 26 | self.name = values.get('name', None) 27 | self.description = values.get('description', None) 28 | self.active = values.get('active', False) 29 | -------------------------------------------------------------------------------- /frontend/src/app/service/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { environment } from '../../environments/environment'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import { Observable } from 'rxjs'; 5 | import { Product } from '../model/product'; 6 | 7 | const HOST = `${environment.backend}/internal`; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class ProductService { 13 | 14 | constructor(private httpClient: HttpClient) { 15 | } 16 | 17 | uploadCsv(file: File): Observable { 18 | const endpoint = `${HOST}/products/upload`; 19 | const formData: FormData = new FormData(); 20 | formData.append('file', file, file.name); 21 | return this.httpClient.post(endpoint, formData); 22 | } 23 | 24 | get(id: number): Observable { 25 | return this.httpClient.get(`${HOST}/products/${id}`); 26 | } 27 | 28 | getAll(id: number): Observable { 29 | return this.httpClient.get(`${HOST}/products`); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tools/convert_textures.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | from PIL import Image 4 | from multiprocessing import Pool 5 | from functools import partial 6 | 7 | 8 | def convert(file, args): 9 | print(f'Processing {file}...') 10 | image = Image.open(os.path.join(args.dir, file)) 11 | 12 | image.save(os.path.join('converted', file.replace('.tga', '.png'))) 13 | image.thumbnail((image.size[0] / 2, image.size[1] / 2), Image.LANCZOS) 14 | image.save(os.path.join('converted', file.replace('.tga', '_S.tga'))) 15 | image.save(os.path.join('converted', file.replace('.tga', '_S.png'))) 16 | 17 | 18 | if __name__ == '__main__': 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument('--dir', '-d', type=str, required=True) 21 | args = parser.parse_args() 22 | 23 | if not os.path.exists('converted'): 24 | os.makedirs('converted') 25 | 26 | files = filter(lambda x: x.endswith('.tga'), os.listdir(args.dir)) 27 | 28 | with Pool(10) as p: 29 | p.map(partial(convert, args=args), files) 30 | -------------------------------------------------------------------------------- /frontend/src/app/service/abstract-item-service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 2 | import { environment } from '../../environments/environment'; 3 | import { Observable } from 'rxjs'; 4 | 5 | const HOST = `${environment.backend}/internal`; 6 | const HEADERS = new HttpHeaders({'Content-Type': 'application/json'}); 7 | 8 | export abstract class AbstractItemService { 9 | protected constructor(private path: string, 10 | private httpClient: HttpClient) { 11 | } 12 | 13 | getAll(): Observable { 14 | return this.httpClient.get(`${HOST}/${this.path}`); 15 | } 16 | 17 | add(item: T): Observable { 18 | return this.httpClient.post(`${HOST}/${this.path}`, item, {headers: HEADERS}); 19 | } 20 | 21 | delete(id: any): Observable { 22 | return this.httpClient.delete(`${HOST}/${this.path}/${id}`); 23 | } 24 | 25 | update(id: any, item: T): Observable { 26 | return this.httpClient.put(`${HOST}/${this.path}/${id}`, item, {headers: HEADERS}); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MatDialog } from '@angular/material/dialog'; 3 | import { AboutDialogComponent } from '../about-dialog/about-dialog.component'; 4 | import { environment } from '../../../../environments/environment'; 5 | import { TextureViewerComponent } from '../debug/texture-viewer/texture-viewer.component'; 6 | import { Router } from '@angular/router'; 7 | 8 | @Component({ 9 | selector: 'app-home', 10 | templateUrl: './home.component.html', 11 | styleUrls: ['./home.component.scss'] 12 | }) 13 | export class HomeComponent implements OnInit { 14 | 15 | isDev = !environment.production; 16 | 17 | constructor(private dialog: MatDialog, 18 | public router: Router) { } 19 | 20 | ngOnInit() { 21 | } 22 | 23 | openAbout() { 24 | this.dialog.open(AboutDialogComponent, { 25 | width: '400px' 26 | }); 27 | } 28 | 29 | openTextureViewer() { 30 | this.dialog.open(TextureViewerComponent, { 31 | width: '1000px' 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/database.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.engine.url import URL 4 | from sqlalchemy.orm import sessionmaker, scoped_session 5 | from config import config 6 | from entity import Base 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class Db(object): 12 | def __init__(self): 13 | self.url = URL( 14 | drivername=config.get('database', 'driver'), 15 | username=config.get('database', 'username'), 16 | password=config.get('database', 'password'), 17 | host=config.get('database', 'host'), 18 | port=int(config.get('database', 'port')), 19 | database=config.get('database', 'database') 20 | ) 21 | 22 | self.engine = create_engine(self.url) 23 | 24 | if config.get('database', 'create_all').lower() == 'true': 25 | Base.metadata.create_all(self.engine) 26 | 27 | self.Session = scoped_session(sessionmaker(bind=self.engine)) 28 | 29 | def commit(self): 30 | self.Session().commit() 31 | 32 | 33 | database = Db() 34 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/item-list/item-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | {{item[line1]}} 8 | {{item[line2]}} 9 | 10 | 13 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 25 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from '../../../auth/auth.service'; 3 | import { Router } from '@angular/router'; 4 | import { MatSnackBar } from '@angular/material/snack-bar'; 5 | import { handleErrorSnackbar } from '../../../utils/network'; 6 | 7 | @Component({ 8 | selector: 'app-login', 9 | templateUrl: './login.component.html', 10 | styleUrls: ['./login.component.scss'] 11 | }) 12 | export class LoginComponent implements OnInit { 13 | 14 | username: string; 15 | password: string; 16 | 17 | constructor(private authService: AuthService, 18 | private router: Router, 19 | private snackBar: MatSnackBar) { 20 | } 21 | 22 | ngOnInit() { 23 | if (this.authService.isAuthenticated()) { 24 | this.router.navigate(['admin']).then(); 25 | } 26 | } 27 | 28 | login() { 29 | this.authService.login(this.username, this.password).subscribe(() => { 30 | this.router.navigate(['admin']).then(); 31 | }, error => { 32 | handleErrorSnackbar(error, this.snackBar); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tools/convert_thumbnails.py: -------------------------------------------------------------------------------- 1 | # Export and convert all thumbnails 2 | 3 | import argparse 4 | import os 5 | from PIL import Image 6 | 7 | 8 | def convert_to_jpeg(file, output): 9 | if os.path.exists(output): 10 | return 11 | print(f'Converting {file}...') 12 | im = Image.open(file) 13 | rgb_im = im.convert('RGB') 14 | rgb_im.save(output) 15 | 16 | 17 | parser = argparse.ArgumentParser() 18 | parser.add_argument('-d', '--directory', type=str, required=True) 19 | parser.add_argument('-o', '--output', type=str, required=True) 20 | args = parser.parse_args() 21 | 22 | os.makedirs(args.output, exist_ok=True) 23 | 24 | for directory in os.listdir(args.directory): 25 | directory = os.path.join(args.directory, directory) 26 | if not os.path.isdir(directory): 27 | continue 28 | 29 | if not directory.endswith('_T_SF'): 30 | continue 31 | 32 | for file in os.listdir(os.path.join(directory, 'Texture2D')): 33 | if file.endswith('.tga'): 34 | convert_to_jpeg(os.path.join(directory, 'Texture2D', file), 35 | os.path.join(args.output, file.replace('.tga', '.jpg'))) 36 | -------------------------------------------------------------------------------- /frontend/src/app/service/api-keys.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { ApiKey } from '../model/api-key'; 4 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 5 | import { environment } from '../../environments/environment'; 6 | 7 | const HOST = `${environment.backend}/internal`; 8 | const HEADERS = new HttpHeaders({'Content-Type': 'application/json'}); 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class ApiKeysService { 14 | 15 | constructor(private httpClient: HttpClient) { 16 | } 17 | 18 | getKeys(): Observable { 19 | return this.httpClient.get(`${HOST}/api-keys`); 20 | } 21 | 22 | createKey(apiKey: ApiKey): Observable { 23 | return this.httpClient.post(`${HOST}/api-keys`, apiKey, {headers: HEADERS}); 24 | } 25 | 26 | deleteKey(id: number): Observable { 27 | return this.httpClient.delete(`${HOST}/api-keys/${id}`); 28 | } 29 | 30 | updateKey(apiKey: ApiKey): Observable { 31 | return this.httpClient.put(`${HOST}/api-keys/${apiKey.id}`, apiKey, {headers: HEADERS}); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/rl-loadout'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rocket Loadout | Create car designs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7 | import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; 8 | import { HomeModule } from './home/home.module'; 9 | import { AuthInterceptor } from './auth/auth.interceptor'; 10 | import { NotifierModule } from 'angular-notifier'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | AppComponent 15 | ], 16 | imports: [ 17 | BrowserModule, 18 | AppRoutingModule, 19 | BrowserAnimationsModule, 20 | HttpClientModule, 21 | NotifierModule.withConfig({ 22 | position: { 23 | horizontal: { 24 | position: 'right' 25 | } 26 | } 27 | }), 28 | HomeModule 29 | ], 30 | providers: [ 31 | { 32 | provide: HTTP_INTERCEPTORS, 33 | useClass: AuthInterceptor, 34 | multi: true 35 | } 36 | ], 37 | bootstrap: [AppComponent] 38 | }) 39 | export class AppModule { 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/api-keys/api-keys.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | {{key.name}} 7 | {{key.description}} 8 | {{key.key}} 9 | 10 | 14 | 17 | 20 | 21 | 22 | 23 | 24 |
25 | 28 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/api-keys/create-api-key/create-api-key.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ApiKeysService } from '../../../../service/api-keys.service'; 3 | import { MatDialogRef } from '@angular/material/dialog'; 4 | import { MatSnackBar } from '@angular/material/snack-bar'; 5 | import { ApiKey } from '../../../../model/api-key'; 6 | import { handleErrorSnackbar } from '../../../../utils/network'; 7 | 8 | @Component({ 9 | selector: 'app-create-api-key', 10 | templateUrl: './create-api-key.component.html', 11 | styleUrls: ['./create-api-key.component.scss'] 12 | }) 13 | export class CreateApiKeyComponent implements OnInit { 14 | 15 | apiKey: ApiKey = new ApiKey(); 16 | 17 | constructor(private apiKeysService: ApiKeysService, 18 | private dialogRef: MatDialogRef, 19 | private snackBar: MatSnackBar) { } 20 | 21 | ngOnInit() { 22 | } 23 | 24 | cancel() { 25 | this.dialogRef.close(); 26 | } 27 | 28 | save() { 29 | this.apiKeysService.createKey(this.apiKey).subscribe(newKey => this.dialogRef.close(newKey), 30 | error => handleErrorSnackbar(error, this.snackBar)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/shared/confirm-dialog/confirm-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'app-confirm-dialog', 6 | templateUrl: './confirm-dialog.component.html', 7 | styleUrls: ['./confirm-dialog.component.scss'] 8 | }) 9 | export class ConfirmDialogComponent implements OnInit { 10 | 11 | prompt: string; 12 | 13 | constructor(private dialogRef: MatDialogRef, 14 | @Inject(MAT_DIALOG_DATA) public data: string) { 15 | this.prompt = data; 16 | } 17 | 18 | ngOnInit() { 19 | } 20 | 21 | confirm() { 22 | this.dialogRef.close(true); 23 | } 24 | } 25 | 26 | export function confirmMaterial(message: string, dialog: MatDialog, yes?: () => void, no?: () => void) { 27 | const dialogRef = dialog.open(ConfirmDialogComponent, { 28 | width: '500px', 29 | data: message 30 | }); 31 | 32 | dialogRef.afterClosed().subscribe(result => { 33 | if (result) { 34 | if (yes) { 35 | yes(); 36 | } 37 | } else { 38 | if (no) { 39 | no(); 40 | } 41 | } 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/loadout-grid-selector/loadout-grid-selector.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 8 | close 9 | Icon 10 | {{item.name}} 21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/loadout-grid-selector/loadout-grid-selector.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Item, Quality } from 'rl-loadout-lib'; 3 | import { environment } from '../../../../environments/environment'; 4 | 5 | @Component({ 6 | selector: 'app-loadout-grid-selector', 7 | templateUrl: './loadout-grid-selector.component.html', 8 | styleUrls: ['./loadout-grid-selector.component.scss'] 9 | }) 10 | export class LoadoutGridSelectorComponent implements OnInit { 11 | 12 | assetHost = environment.assetHost; 13 | 14 | items: Item[]; 15 | 16 | onSelect: (item: Item) => void; 17 | 18 | qCommon = Quality.COMMON; 19 | qUncommon = Quality.UNCOMMON; 20 | qRare = Quality.RARE; 21 | qVeryRare = Quality.VERY_RARE; 22 | qImport = Quality.IMPORT; 23 | qExotic = Quality.EXOTIC; 24 | qBlackMarket = Quality.BLACK_MARKET; 25 | qLimited = Quality.LIMITED; 26 | qPremium = Quality.PREMIUM; 27 | 28 | selectedItem: Item = new Item(0, '', '', 0, false); 29 | 30 | filter: string; 31 | 32 | constructor() { } 33 | 34 | ngOnInit() { 35 | } 36 | 37 | selectItem(item: Item) { 38 | this.selectedItem = item; 39 | if (this.onSelect) { 40 | this.onSelect(item); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'rl-loadout'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app.title).toEqual('rl-loadout'); 27 | }); 28 | 29 | it('should render title in a h1 tag', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.debugElement.nativeElement; 33 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to rl-loadout!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/canvas/canvas.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../theme"; 2 | 3 | .canvas-container { 4 | height: calc(100% - #{$toolbar-height}); 5 | overflow:hidden; 6 | width:auto; 7 | position: relative; 8 | } 9 | 10 | canvas { 11 | width: 100% !important; 12 | height: 100% !important; 13 | position: absolute; 14 | } 15 | 16 | .full-overlay { 17 | position: fixed; 18 | top: 0; 19 | right: 0; 20 | left: 0; 21 | bottom: 0; 22 | background-color: rgba(0, 0, 0, 0.5); 23 | z-index: 400; 24 | display: flex; 25 | flex-flow: column; 26 | justify-content: center; 27 | align-items: center; 28 | } 29 | 30 | .full-overlay > span, .loading-overlay > span { 31 | font-size: 20px; 32 | margin-bottom: 16px; 33 | } 34 | 35 | .full-overlay > mat-progress-bar, .loading-overlay > mat-progress-bar { 36 | max-width: 300px; 37 | } 38 | 39 | .loading-overlay { 40 | z-index: 400; 41 | position: absolute; 42 | width: 100%; 43 | height: 100%; 44 | top: 0; 45 | left: 0; 46 | display: flex; 47 | flex-flow: column; 48 | justify-content: center; 49 | align-items: center; 50 | background-color: rgba(0, 0, 0, 0.5); 51 | } 52 | 53 | .canvas-container > .dg-container { 54 | z-index: 100; 55 | position: absolute; 56 | right: 1em; 57 | } 58 | -------------------------------------------------------------------------------- /cloud_functions/compress_model/index.js: -------------------------------------------------------------------------------- 1 | const gltfPipeline = require('gltf-pipeline'); 2 | const fs = require('fs'); 3 | const os = require('os'); 4 | const {Storage} = require('@google-cloud/storage'); 5 | 6 | 7 | const storage = new Storage(); 8 | 9 | const gltfOptions = { 10 | dracoOptions: { 11 | compressionLevel: 7 12 | }, 13 | keepUnusedElements: true 14 | }; 15 | 16 | /** 17 | * Triggered from a change to a Cloud Storage bucket. 18 | * 19 | * @param {!Object} event Event payload. 20 | * @param {!Object} context Metadata for the event. 21 | */ 22 | exports.compress = async (event, context) => { 23 | if (!event.name.endsWith('.glb') || event.name.endsWith('.draco.glb')) { 24 | return; 25 | } 26 | 27 | const tmpDir = os.tmpdir(); 28 | 29 | const options = { 30 | destination: tmpDir + '/model.glb', 31 | }; 32 | 33 | const bucket = storage.bucket(event.bucket); 34 | 35 | await bucket.file(event.name).download(options); 36 | 37 | const glb = fs.readFileSync(tmpDir + '/model.glb'); 38 | const results = await gltfPipeline.processGlb(glb, gltfOptions); 39 | fs.writeFileSync(tmpDir + '/model.draco.glb', results.glb); 40 | 41 | // Uploads a local file to the bucket 42 | await bucket.upload(tmpDir + '/model.draco.glb', { 43 | destination: event.name.replace('.glb', '.draco.glb') 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /backend/blueprints/api.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from dao import BodyDao, WheelDao, TopperDao, AntennaDao, DecalDao 3 | from _version import __version__ 4 | 5 | api_blueprint = Blueprint('main_api', __name__, url_prefix='/internal') 6 | body_dao = BodyDao() 7 | wheel_dao = WheelDao() 8 | topper_dao = TopperDao() 9 | antenna_dao = AntennaDao() 10 | decal_dao = DecalDao() 11 | 12 | 13 | @api_blueprint.route('/status', methods=['GET']) 14 | def status(): 15 | return jsonify({ 16 | 'version': __version__ 17 | }) 18 | 19 | 20 | @api_blueprint.route('/all', methods=['GET']) 21 | def get_all(): 22 | result = { 23 | 'bodies': [item.Body.product_joined_to_dict(item.Product) for item in body_dao.get_all_join_product()], 24 | 'wheels': [item.Wheel.product_joined_to_dict(item.Product) for item in wheel_dao.get_all_join_product()], 25 | 'toppers': [item.Topper.product_joined_to_dict(item.Product) for item in topper_dao.get_all_join_product()], 26 | 'antennas': [item.Antenna.product_joined_to_dict(item.Product) for item in antenna_dao.get_all_join_product()] 27 | } 28 | 29 | body_id = request.args.get('body', default=None) 30 | 31 | if body_id is not None: 32 | result['decals'] = [item.Decal.product_joined_to_dict(item.Product) for item in decal_dao.get_all_for_body(body_id)] 33 | 34 | return jsonify(result) 35 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-antenna-stick/create-antenna-stick.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { MatSnackBar } from '@angular/material/snack-bar'; 4 | import { CloudStorageService } from '../../../../service/cloud-storage.service'; 5 | import { AntennaStick } from 'rl-loadout-lib'; 6 | import { CreateDialog } from '../create-dialog'; 7 | import { AntennaSticksService } from '../../../../service/items/antenna-sticks.service'; 8 | import { ProductService } from '../../../../service/product.service'; 9 | 10 | @Component({ 11 | selector: 'app-create-antenna-stick', 12 | templateUrl: './create-antenna-stick.component.html', 13 | styleUrls: ['./create-antenna-stick.component.scss'] 14 | }) 15 | export class CreateAntennaStickComponent extends CreateDialog { 16 | 17 | constructor(dialogRef: MatDialogRef, 18 | cloudService: CloudStorageService, 19 | antennaSticksService: AntennaSticksService, 20 | snackBar: MatSnackBar, 21 | productService: ProductService, 22 | @Inject(MAT_DIALOG_DATA) data: AntennaStick) { 23 | super(dialogRef, cloudService, snackBar, data, productService, antennaSticksService); 24 | this.item = new AntennaStick(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/color-selector/color-selector.component.scss: -------------------------------------------------------------------------------- 1 | .color-selector-container { 2 | padding: 16px; 3 | height: 100%; 4 | box-shadow: -12px 0px 35px 15px rgba(0, 0, 0, 0.4); 5 | position: relative; 6 | z-index: 50; 7 | box-sizing: border-box; 8 | overflow-y: auto; 9 | } 10 | 11 | .color-label { 12 | margin-right: 8px; 13 | } 14 | 15 | .color-picker-button { 16 | width: 24px; 17 | height: 24px; 18 | border-radius: 5px; 19 | border: solid 1px black; 20 | cursor: pointer; 21 | } 22 | 23 | .primary-color-container, .accent-color-container, .paint-color-container { 24 | display: flex; 25 | flex-flow: row; 26 | flex-wrap: wrap; 27 | width: 260px; 28 | } 29 | 30 | .paint-color-container { 31 | width: auto; 32 | } 33 | 34 | .expansion-actions { 35 | margin-top: 8px; 36 | } 37 | 38 | .accent-color-container { 39 | width: 390px; 40 | } 41 | 42 | .color-expansion-content { 43 | padding-top: 16px; 44 | } 45 | 46 | ::ng-deep .dark-color .mat-expansion-panel-header-title { 47 | color: white; 48 | } 49 | 50 | ::ng-deep .dark-color .mat-expansion-indicator::after { 51 | color: rgba(255, 255, 255, 0.7); 52 | } 53 | 54 | ::ng-deep .bright-color .mat-expansion-panel-header-title { 55 | color: black; 56 | } 57 | 58 | ::ng-deep .bright-color .mat-expansion-indicator::after { 59 | color: rgba(0, 0, 0, 0.7); 60 | } 61 | 62 | mat-panel-title { 63 | align-items: center; 64 | } 65 | -------------------------------------------------------------------------------- /version_check.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | 5 | class VersionMismatchException(Exception): 6 | def __init__(self, message): 7 | super(VersionMismatchException, self).__init__(message) 8 | 9 | 10 | def get_frontend_version(): 11 | with open('frontend/package.json') as f: 12 | package = json.load(f) 13 | package_version = package['version'] 14 | 15 | with open('frontend/package-lock.json') as f: 16 | package_lock = json.load(f) 17 | package_lock_version = package_lock['version'] 18 | 19 | return package_version, package_lock_version 20 | 21 | 22 | def get_backend_version(): 23 | with open('backend/_version.py') as f: 24 | version_line = f.readline() 25 | return re.match("__version__\s?=\s?'([0-9]+\.[0-9]+\.[0-9]+)'", version_line).group(1) 26 | 27 | 28 | frontend_versions = get_frontend_version() 29 | backend_version = get_backend_version() 30 | 31 | if frontend_versions[0] != frontend_versions[1]: 32 | raise VersionMismatchException(f'Version {frontend_versions[0]} in package.json does not match version' 33 | f' {frontend_versions[1]} in package-lock.json') 34 | 35 | if frontend_versions[0] != backend_version: 36 | raise VersionMismatchException(f'Frontend version {frontend_versions[0]} does not match' 37 | f' backend version {backend_version}') 38 | 39 | print('Version check ok.') 40 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/main/main.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Admin 4 | 5 | 6 | 7 | 8 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /backend/blueprints/bodies.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_jwt_extended import jwt_required 3 | from utils.network.decorators import json_required_params, commit_after 4 | from database import database 5 | from entity import Body 6 | from dao import BodyDao 7 | from utils.network.exc import NotFoundException 8 | 9 | bodies_blueprint = Blueprint('bodies', __name__, url_prefix='/internal/bodies') 10 | body_dao = BodyDao() 11 | 12 | 13 | @bodies_blueprint.route('', methods=['GET']) 14 | def get_bodies(): 15 | bodies = body_dao.get_all_join_product() 16 | return jsonify([body.Body.product_joined_to_dict(body.Product) for body in bodies]) 17 | 18 | 19 | @bodies_blueprint.route('', methods=['POST']) 20 | @jwt_required 21 | @json_required_params(['id', 'icon', 'quality', 'paintable', 'model']) 22 | def add_body(): 23 | body = Body() 24 | body.apply_dict(request.json) 25 | body_dao.add(body) 26 | database.commit() 27 | return jsonify(body.to_dict()) 28 | 29 | 30 | @bodies_blueprint.route('/', methods=['DELETE']) 31 | @jwt_required 32 | @commit_after 33 | def delete_body(body_id): 34 | body_dao.delete(body_id) 35 | return '', 200 36 | 37 | 38 | @bodies_blueprint.route('/', methods=['PUT']) 39 | @jwt_required 40 | @commit_after 41 | def update_body(body_id): 42 | item = body_dao.get(body_id) 43 | 44 | if item is None: 45 | raise NotFoundException('Body not found') 46 | 47 | item.update(request.json) 48 | return '', 200 49 | -------------------------------------------------------------------------------- /backend/blueprints/wheels.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_jwt_extended import jwt_required 3 | from utils.network.decorators import json_required_params, commit_after 4 | from database import database 5 | from entity import Wheel 6 | from dao import WheelDao 7 | from utils.network.exc import NotFoundException 8 | 9 | wheels_blueprint = Blueprint('wheels', __name__, url_prefix='/internal/wheels') 10 | wheel_dao = WheelDao() 11 | 12 | 13 | @wheels_blueprint.route('', methods=['GET']) 14 | def get_wheels(): 15 | wheels = wheel_dao.get_all_join_product() 16 | return jsonify([item.Wheel.product_joined_to_dict(item.Product) for item in wheels]) 17 | 18 | 19 | @wheels_blueprint.route('', methods=['POST']) 20 | @jwt_required 21 | @json_required_params(['id', 'icon', 'quality', 'paintable', 'model']) 22 | def add_wheel(): 23 | wheel = Wheel() 24 | wheel.apply_dict(request.json) 25 | wheel_dao.add(wheel) 26 | database.commit() 27 | return jsonify(wheel.to_dict()) 28 | 29 | 30 | @wheels_blueprint.route('/', methods=['DELETE']) 31 | @jwt_required 32 | @commit_after 33 | def delete_wheel(wheel_id): 34 | wheel_dao.delete(wheel_id) 35 | return '', 200 36 | 37 | 38 | @wheels_blueprint.route('/', methods=['PUT']) 39 | @jwt_required 40 | @commit_after 41 | def update(wheel_id): 42 | item = wheel_dao.get(wheel_id) 43 | 44 | if item is None: 45 | raise NotFoundException('Wheel not found') 46 | 47 | item.update(request.json) 48 | return '', 200 49 | -------------------------------------------------------------------------------- /backend/entity/topper.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from sqlalchemy import Column, String 3 | from .base import Base 4 | from .item import BaseItem 5 | 6 | 7 | class Topper(Base, BaseItem): 8 | __tablename__ = 'topper' 9 | model = Column(String(255), nullable=False) 10 | base_texture = Column(String(255), nullable=True) 11 | rgba_map = Column(String(255), nullable=True) 12 | normal_map = Column(String(255), nullable=True) 13 | 14 | def apply_dict(self, item_dict: Dict): 15 | super().apply_dict(item_dict) 16 | self.model = item_dict.get('model', None) 17 | self.base_texture = item_dict.get('base_texture', None) 18 | self.rgba_map = item_dict.get('rgba_map', None) 19 | self.normal_map = item_dict.get('normal_map', None) 20 | 21 | def to_dict(self) -> Dict: 22 | d = super(Topper, self).to_dict() 23 | 24 | d['model'] = self.model 25 | d['base_texture'] = self.base_texture 26 | d['rgba_map'] = self.rgba_map 27 | d['normal_map'] = self.normal_map 28 | 29 | return d 30 | 31 | def update(self, item_dict: Dict): 32 | super(Topper, self).update(item_dict) 33 | if 'model' in item_dict: 34 | self.model = item_dict['model'] 35 | if 'base_texture' in item_dict: 36 | self.base_texture = item_dict['base_texture'] 37 | if 'rgba_map' in item_dict: 38 | self.rgba_map = item_dict['rgba_map'] 39 | if 'normal_map' in item_dict: 40 | self.normal_map = item_dict['normal_map'] 41 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/loadout-grid-selector/loadout-grid-selector.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../theme"; 2 | 3 | .grid-container { 4 | background-color: $background; 5 | height: 100%; 6 | padding: 10px; 7 | overflow-y: auto; 8 | box-shadow: -12px 0px 35px 15px rgba(0, 0, 0, 0.4); 9 | position: relative; 10 | z-index: 50; 11 | box-sizing: border-box; 12 | } 13 | 14 | mat-grid-list { 15 | width: 500px; 16 | } 17 | 18 | mat-grid-tile { 19 | background-color: black; 20 | } 21 | 22 | mat-grid-tile img { 23 | width: 100%; 24 | height: 100%; 25 | } 26 | 27 | mat-grid-tile:hover { 28 | cursor: pointer; 29 | } 30 | 31 | mat-grid-tile-footer.uncommon { 32 | background-color: rgba(73, 162, 195, 0.4); 33 | } 34 | 35 | mat-grid-tile-footer.rare { 36 | background-color: rgba(99, 116, 195, 0.4); 37 | } 38 | 39 | mat-grid-tile-footer.very-rare { 40 | background-color: rgba(123, 95, 195, 0.4); 41 | } 42 | 43 | mat-grid-tile-footer.import { 44 | background-color: rgba(195, 81, 73, 0.4); 45 | } 46 | 47 | mat-grid-tile-footer.exotic { 48 | background-color: rgba(195, 180, 91, 0.4); 49 | } 50 | 51 | mat-grid-tile-footer.black-market { 52 | background-color: rgba(195, 0, 195, 0.4); 53 | } 54 | 55 | mat-grid-tile-footer.limited { 56 | background-color: rgba(195, 102, 45, 0.4); 57 | } 58 | 59 | mat-grid-tile-footer.premium { 60 | background-color: rgba(82, 195, 144, 0.4); 61 | } 62 | 63 | .selected { 64 | box-shadow: 0 0 14px 0 map-get($app-primary, 700); 65 | } 66 | 67 | mat-form-field { 68 | width: 100%; 69 | } 70 | -------------------------------------------------------------------------------- /backend/blueprints/toppers.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_jwt_extended import jwt_required 3 | from utils.network.decorators import json_required_params, commit_after 4 | from database import database 5 | from entity import Topper 6 | from dao import TopperDao 7 | from utils.network.exc import NotFoundException 8 | 9 | toppers_blueprint = Blueprint('toppers', __name__, url_prefix='/internal/toppers') 10 | topper_dao = TopperDao() 11 | 12 | 13 | @toppers_blueprint.route('', methods=['GET']) 14 | def get_toppers(): 15 | toppers = topper_dao.get_all_join_product() 16 | return jsonify([item.Topper.product_joined_to_dict(item.Product) for item in toppers]) 17 | 18 | 19 | @toppers_blueprint.route('', methods=['POST']) 20 | @jwt_required 21 | @json_required_params(['id', 'icon', 'quality', 'paintable', 'model']) 22 | def add_topper(): 23 | topper = Topper() 24 | topper.apply_dict(request.json) 25 | topper_dao.add(topper) 26 | database.commit() 27 | return jsonify(topper.to_dict()) 28 | 29 | 30 | @toppers_blueprint.route('/', methods=['DELETE']) 31 | @jwt_required 32 | @commit_after 33 | def delete_topper(topper_id): 34 | topper_dao.delete(topper_id) 35 | return '', 200 36 | 37 | 38 | @toppers_blueprint.route('/', methods=['PUT']) 39 | @jwt_required 40 | @commit_after 41 | def update(topper_id): 42 | item = topper_dao.get(topper_id) 43 | 44 | if item is None: 45 | raise NotFoundException('Topper not found') 46 | 47 | item.update(request.json) 48 | return '', 200 49 | -------------------------------------------------------------------------------- /frontend/src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { CanvasComponent } from './components/canvas/canvas.component'; 4 | import { HomeComponent } from './components/home/home.component'; 5 | import { LoadoutToolbarComponent } from './components/loadout-toolbar/loadout-toolbar.component'; 6 | import { LoadoutGridSelectorComponent } from './components/loadout-grid-selector/loadout-grid-selector.component'; 7 | import { ColorSelectorComponent } from './components/color-selector/color-selector.component'; 8 | import { AboutDialogComponent } from './components/about-dialog/about-dialog.component'; 9 | import { TextureViewerComponent } from './components/debug/texture-viewer/texture-viewer.component'; 10 | import { SharedMaterialModule } from '../shared-material/shared-material.module'; 11 | import { ColorPickerModule } from 'ngx-color-picker'; 12 | import { ItemFilterPipe } from './pipes/item-filter.pipe'; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | CanvasComponent, 17 | HomeComponent, 18 | LoadoutToolbarComponent, 19 | LoadoutGridSelectorComponent, 20 | ColorSelectorComponent, 21 | AboutDialogComponent, 22 | TextureViewerComponent, 23 | ItemFilterPipe 24 | ], 25 | imports: [ 26 | CommonModule, 27 | SharedMaterialModule, 28 | ColorPickerModule 29 | ], 30 | entryComponents: [ 31 | LoadoutGridSelectorComponent, 32 | ColorSelectorComponent, 33 | AboutDialogComponent, 34 | TextureViewerComponent 35 | ] 36 | }) 37 | export class HomeModule { } 38 | -------------------------------------------------------------------------------- /backend/utils/network/decorators.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List 3 | from functools import wraps 4 | from flask import request 5 | from database import database 6 | from utils.network.exc import BadRequestException, InternalServerErrorException 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | def json_required_params(params: List[str]): 12 | """ 13 | Checks if the provided params are provided in the json body. 14 | :param params: 15 | """ 16 | 17 | def decorator(function): 18 | @wraps(function) 19 | def wrapper(*args, **kwargs): 20 | if not request.is_json: 21 | raise BadRequestException('Missing JSON in request') 22 | for param in params: 23 | if param not in request.json or request.json[param] == '': 24 | raise BadRequestException(f'Missing {param} parameter in JSON') 25 | return function(*args, **kwargs) 26 | 27 | return wrapper 28 | 29 | return decorator 30 | 31 | 32 | def commit_after(function): 33 | @wraps(function) 34 | def wrapper(*args, **kwargs): 35 | response = function(*args, **kwargs) 36 | if database.Session.is_active: 37 | try: 38 | database.Session.commit() 39 | except Exception as e: 40 | log.error('exception occurred during commit', exc_info=e) 41 | database.Session.rollback() 42 | raise InternalServerErrorException('Database exception occurred, check the logs') 43 | return response 44 | 45 | return wrapper 46 | -------------------------------------------------------------------------------- /backend/entity/decal.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from sqlalchemy import Column, String, Integer, ForeignKey 3 | from sqlalchemy.orm import relationship 4 | from .base import Base 5 | from .item import BaseItem 6 | 7 | 8 | class Decal(Base, BaseItem): 9 | __tablename__ = 'decal' 10 | base_texture = Column(String(255), nullable=True) 11 | rgba_map = Column(String(255), nullable=False) 12 | body_id = Column(Integer, ForeignKey('body.id'), nullable=True) 13 | body = relationship('Body', back_populates='decals') 14 | 15 | def apply_dict(self, item_dict: Dict): 16 | super(Decal, self).apply_dict(item_dict) 17 | self.base_texture = item_dict.get('base_texture', None) 18 | self.rgba_map = item_dict.get('rgba_map', None) 19 | self.body_id = item_dict.get('body_id', None) 20 | 21 | def to_dict(self) -> Dict: 22 | """Return object data in easily serializable format""" 23 | return { 24 | 'id': self.id, 25 | 'quality': self.quality, 26 | 'icon': self.icon, 27 | 'paintable': self.paintable, 28 | 'base_texture': self.base_texture, 29 | 'rgba_map': self.rgba_map, 30 | 'body_id': self.body_id 31 | } 32 | 33 | def update(self, item_dict: Dict): 34 | super(Decal, self).update(item_dict) 35 | if 'base_texture' in item_dict: 36 | self.base_texture = item_dict['base_texture'] 37 | if 'rgba_map' in item_dict: 38 | self.rgba_map = item_dict['rgba_map'] 39 | if 'body_id' in item_dict: 40 | self.body_id = item_dict['body_id'] 41 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-body/create-body.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { Body, Quality } from 'rl-loadout-lib'; 3 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 4 | import { MatSnackBar } from '@angular/material/snack-bar'; 5 | import { CloudStorageService } from '../../../../service/cloud-storage.service'; 6 | import { CreateDialog } from '../create-dialog'; 7 | import { BodiesService } from '../../../../service/items/bodies.service'; 8 | import { ProductService } from '../../../../service/product.service'; 9 | 10 | @Component({ 11 | selector: 'app-create-body', 12 | templateUrl: './create-body.component.html', 13 | styleUrls: ['./create-body.component.scss'] 14 | }) 15 | export class CreateBodyComponent extends CreateDialog { 16 | 17 | productType = 'body'; 18 | 19 | constructor(dialogRef: MatDialogRef, 20 | cloudService: CloudStorageService, 21 | bodiesService: BodiesService, 22 | snackBar: MatSnackBar, 23 | productService: ProductService, 24 | @Inject(MAT_DIALOG_DATA) data: Body) { 25 | super(dialogRef, cloudService, snackBar, data, productService, bodiesService); 26 | this.item = new Body( 27 | undefined, undefined, '', Quality.COMMON, false 28 | ); 29 | } 30 | 31 | selectProduct($event: string) { 32 | super.selectProduct($event); 33 | 34 | const model = this.selectedObjects.find(value => !value.endsWith('draco.glb') && value.endsWith('.glb')); 35 | if (model != undefined) { 36 | this.item.model = model; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-wheel/create-wheel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { Wheel, Quality } from 'rl-loadout-lib'; 3 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 4 | import { MatSnackBar } from '@angular/material/snack-bar'; 5 | import { CloudStorageService } from '../../../../service/cloud-storage.service'; 6 | import { CreateDialog } from '../create-dialog'; 7 | import { WheelsService } from '../../../../service/items/wheels.service'; 8 | import { ProductService } from '../../../../service/product.service'; 9 | 10 | @Component({ 11 | selector: 'app-create-wheel', 12 | templateUrl: './create-wheel.component.html', 13 | styleUrls: ['./create-wheel.component.scss'] 14 | }) 15 | export class CreateWheelComponent extends CreateDialog { 16 | 17 | productType = 'wheel'; 18 | 19 | constructor(dialogRef: MatDialogRef, 20 | cloudService: CloudStorageService, 21 | wheelsService: WheelsService, 22 | snackBar: MatSnackBar, 23 | productService: ProductService, 24 | @Inject(MAT_DIALOG_DATA) data: Wheel) { 25 | super(dialogRef, cloudService, snackBar, data, productService, wheelsService); 26 | this.item = new Wheel( 27 | undefined, undefined, '', Quality.COMMON, false 28 | ); 29 | } 30 | 31 | selectProduct($event: string) { 32 | super.selectProduct($event); 33 | 34 | const model = this.selectedObjects.find(value => !value.endsWith('draco.glb') && value.endsWith('.glb')); 35 | if (model != undefined) { 36 | this.item.model = model; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/blueprints/antenna_sticks.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_jwt_extended import jwt_required 3 | from utils.network.decorators import json_required_params, commit_after 4 | from database import database 5 | from entity import AntennaStick 6 | from dao import AntennaDao 7 | from utils.network.exc import NotFoundException 8 | 9 | antenna_sticks_blueprint = Blueprint('antenna_sticks', __name__, url_prefix='/internal/antenna-sticks') 10 | antenna_dao = AntennaDao() 11 | 12 | 13 | @antenna_sticks_blueprint.route('', methods=['GET']) 14 | def get_antenna_sticks(): 15 | antenna_sticks = antenna_dao.get_sticks() 16 | return jsonify([item.to_dict() for item in antenna_sticks]) 17 | 18 | 19 | @antenna_sticks_blueprint.route('', methods=['POST']) 20 | @jwt_required 21 | @json_required_params(['model']) 22 | def add_antenna_stick(): 23 | antenna_stick = AntennaStick() 24 | antenna_stick.apply_dict(request.json) 25 | antenna_dao.add_stick(antenna_stick) 26 | database.commit() 27 | return jsonify(antenna_stick.to_dict()) 28 | 29 | 30 | @antenna_sticks_blueprint.route('/', methods=['DELETE']) 31 | @jwt_required 32 | @commit_after 33 | def delete_antenna_stick(antenna_stick_id): 34 | antenna_dao.delete_stick(antenna_stick_id) 35 | return '', 200 36 | 37 | 38 | @antenna_sticks_blueprint.route('/', methods=['PUT']) 39 | @jwt_required 40 | @commit_after 41 | def update_antenna_stick(antenna_stick_id): 42 | item = antenna_dao.get_stick(antenna_stick_id) 43 | 44 | if item is None: 45 | raise NotFoundException('Antenna stick not found') 46 | 47 | item.update(request.json) 48 | return '', 200 49 | -------------------------------------------------------------------------------- /backend/blueprints/api_keys.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_jwt_extended import jwt_required 3 | from utils.network.decorators import json_required_params, commit_after 4 | from database import database 5 | from entity import ApiKey 6 | from dao import ApiKeyDao 7 | from api.api import generate_api_key 8 | from utils.network.exc import NotFoundException 9 | 10 | api_keys_blueprint = Blueprint('api_keys', __name__, url_prefix='/internal/api-keys') 11 | key_dao = ApiKeyDao() 12 | 13 | 14 | @api_keys_blueprint.route('', methods=['GET']) 15 | @jwt_required 16 | def get(): 17 | keys = key_dao.get_all() 18 | return jsonify([item.to_dict() for item in keys]) 19 | 20 | 21 | @api_keys_blueprint.route('', methods=['POST']) 22 | @jwt_required 23 | @json_required_params(['name', 'description', 'active']) 24 | def add(): 25 | key = ApiKey() 26 | key.apply_dict(request.json) 27 | key.key = generate_api_key() 28 | key_dao.add(key) 29 | database.commit() 30 | return jsonify(key.to_dict()) 31 | 32 | 33 | @api_keys_blueprint.route('/', methods=['DELETE']) 34 | @jwt_required 35 | @commit_after 36 | def delete(key_id): 37 | key_dao.delete(key_id) 38 | return '', 200 39 | 40 | 41 | @api_keys_blueprint.route('/', methods=['PUT']) 42 | @jwt_required 43 | @json_required_params(['name', 'description', 'active']) 44 | def put(key_id): 45 | key = key_dao.get(key_id) 46 | 47 | if key is None: 48 | raise NotFoundException('API key not found') 49 | 50 | key.name = request.json['name'] 51 | key.description = request.json['description'] 52 | key.active = request.json['active'] 53 | 54 | database.commit() 55 | 56 | return '', 200 57 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-topper/create-topper.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { Topper, Quality } from 'rl-loadout-lib'; 3 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 4 | import { MatSnackBar } from '@angular/material/snack-bar'; 5 | import { CloudStorageService } from '../../../../service/cloud-storage.service'; 6 | import { CreateDialog } from '../create-dialog'; 7 | import { ToppersService } from '../../../../service/items/toppers.service'; 8 | import { ProductService } from '../../../../service/product.service'; 9 | 10 | @Component({ 11 | selector: 'app-create-topper', 12 | templateUrl: './create-topper.component.html', 13 | styleUrls: ['./create-topper.component.scss'] 14 | }) 15 | export class CreateTopperComponent extends CreateDialog { 16 | 17 | productType = 'topper'; 18 | 19 | constructor(dialogRef: MatDialogRef, 20 | cloudService: CloudStorageService, 21 | toppersService: ToppersService, 22 | snackBar: MatSnackBar, 23 | productService: ProductService, 24 | @Inject(MAT_DIALOG_DATA) data: Topper) { 25 | super(dialogRef, cloudService, snackBar, data, productService, toppersService); 26 | this.item = new Topper( 27 | undefined, undefined, '', Quality.COMMON, false, undefined, undefined, undefined 28 | ); 29 | } 30 | 31 | selectProduct($event: string) { 32 | super.selectProduct($event); 33 | 34 | const model = this.selectedObjects.find(value => !value.endsWith('draco.glb') && value.endsWith('.glb')); 35 | if (model != undefined) { 36 | this.item.model = model; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/entity/item.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from sqlalchemy import Column, Integer, String, Boolean, ForeignKey 3 | from sqlalchemy.ext.declarative import declared_attr 4 | from entity.product import Product 5 | 6 | 7 | class BaseItem: 8 | __tablename__ = '' 9 | 10 | @declared_attr 11 | def id(cls): 12 | return Column(Integer, ForeignKey('product.id'), primary_key=True) 13 | 14 | quality = Column(Integer, nullable=False) 15 | icon = Column(String(255), nullable=False) 16 | paintable = Column(Boolean, nullable=False, default=False) 17 | 18 | def apply_dict(self, item_dict: Dict): 19 | self.id = item_dict.get('id', None) 20 | self.quality = item_dict.get('quality', None) 21 | self.icon = item_dict.get('icon', None) 22 | self.paintable = item_dict.get('paintable', None) 23 | 24 | def to_dict(self) -> Dict: 25 | """Return object data in easily serializable format""" 26 | return { 27 | 'id': self.id, 28 | 'quality': self.quality, 29 | 'icon': self.icon, 30 | 'paintable': self.paintable 31 | } 32 | 33 | def update(self, item_dict: Dict): 34 | if 'id' in item_dict: 35 | self.id = item_dict['id'] 36 | if 'quality' in item_dict: 37 | self.quality = item_dict['quality'] 38 | if 'icon' in item_dict: 39 | self.icon = item_dict['icon'] 40 | if 'paintable' in item_dict: 41 | self.paintable = item_dict['paintable'] 42 | 43 | def product_joined_to_dict(self, product: Product): 44 | d = self.to_dict() 45 | d['name'] = product.name 46 | d['product_name'] = product.product_name 47 | return d 48 | -------------------------------------------------------------------------------- /frontend/src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | import { Router } from '@angular/router'; 4 | import { Observable } from 'rxjs'; 5 | import { environment } from '../../environments/environment'; 6 | import { tap } from 'rxjs/operators'; 7 | import { parseJwt } from '../utils/network'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class AuthService { 13 | 14 | constructor(private httpClient: HttpClient, 15 | private router: Router) { 16 | } 17 | 18 | login(username: string, password: string): Observable { 19 | const json = JSON.stringify({username, password}); 20 | 21 | const headers = new HttpHeaders({'Content-Type': 'application/json'}); 22 | 23 | return this.httpClient.post(`${environment.backend}/auth`, json, {headers}).pipe( 24 | tap( 25 | response => { 26 | localStorage.setItem('username', username); 27 | localStorage.setItem('token', response.access_token); 28 | }, 29 | () => { 30 | localStorage.clear(); 31 | } 32 | ) 33 | ); 34 | } 35 | 36 | logout(): void { 37 | localStorage.clear(); 38 | this.router.navigate(['/']).then(); 39 | } 40 | 41 | isAuthenticated(): boolean { 42 | const token = localStorage.getItem('token'); 43 | 44 | if (token == undefined) { 45 | return false; 46 | } 47 | 48 | const jwt = parseJwt(token); 49 | return Date.now() < jwt.exp * 1000; 50 | } 51 | 52 | getUsername(): string { 53 | return localStorage.getItem('username'); 54 | } 55 | 56 | getToken(): string { 57 | return localStorage.getItem('token'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /backend/rocket/ids.py: -------------------------------------------------------------------------------- 1 | BODY_GREY_CAR_ID = 597 # DeLorean Time Machine 2 | BODY_DARK_CAR_ID = 803 # '16 Batmobile 3 | BODY_BERRY_ID = 2665 # The Dark Knight Rises Tumbler 4 | BODY_EGGPLANT_ID = 2666 # '89 Batmobile 5 | BODY_MAPLE_ID = 2919 # Jurassic Jeep® Wrangler 6 | BODY_RYE_TIER1_ID = 3155 # Maverick 7 | BODY_RYE_TIER2_ID = 3156 # Maverick G1 8 | BODY_RYE_TIER3_ID = 3157 # Maverick GXT 9 | BODY_ENSPIER_TIER3_ID = 3594 # Artemis GXT 10 | BODY_ENSPIER_TIER1_ID = 3614 # Artemis 11 | BODY_ENSPIER_TIER2_ID = 3622 # Artemis G1 12 | BODY_MANGO_TIER3_ID = 3875 # Guardian GXT 13 | BODY_MANGO_TIER1_ID = 3879 # Guardian 14 | BODY_MANGO_TIER2_ID = 3880 # Guardian G1 15 | BODY_FELINE_ID = 4014 # K.I.T.T. 16 | BODY_SLIME_ID = 4155 # Ecto-1 17 | BODY_MELON_TIER1_ID = 4318 # Mudcat 18 | BODY_MELON_TIER2_ID = 4319 # Mudcat G1 19 | BODY_MELON_TIER3_ID = 4320 # Mudcat GXT 20 | BODY_DURIAN_TIER3_ID = 4367 # Chikara GXT 21 | BODY_DURIAN_TIER1_ID = 4472 # Chikara 22 | BODY_DURIAN_TIER2_ID = 4473 # Chikara G1 23 | 24 | 25 | def tier_floor(body_id: int): 26 | # maverick 27 | if body_id == BODY_RYE_TIER2_ID or body_id == BODY_RYE_TIER3_ID: 28 | return BODY_RYE_TIER1_ID 29 | # artemis 30 | if body_id == BODY_ENSPIER_TIER2_ID or body_id == BODY_ENSPIER_TIER3_ID: 31 | return BODY_ENSPIER_TIER1_ID 32 | # Guardian 33 | if body_id == BODY_MANGO_TIER2_ID or body_id == BODY_MANGO_TIER3_ID: 34 | return BODY_MANGO_TIER1_ID 35 | # Mudcat 36 | if body_id == BODY_MELON_TIER2_ID or body_id == BODY_MELON_TIER3_ID: 37 | return BODY_MELON_TIER1_ID 38 | # Chikara 39 | if body_id == BODY_DURIAN_TIER2_ID or body_id == BODY_DURIAN_TIER3_ID: 40 | return BODY_DURIAN_TIER1_ID 41 | return body_id 42 | -------------------------------------------------------------------------------- /backend/blueprints/decals.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_jwt_extended import jwt_required 3 | from utils.network.decorators import json_required_params, commit_after 4 | from database import database 5 | from entity import Decal 6 | from dao import DecalDao, BodyDao 7 | from utils.network.exc import NotFoundException 8 | 9 | decals_blueprint = Blueprint('decals', __name__, url_prefix='/internal/decals') 10 | decal_dao = DecalDao() 11 | body_dao = BodyDao() 12 | 13 | 14 | @decals_blueprint.route('', methods=['GET']) 15 | def get_decals(): 16 | body_id = request.args.get('body', default=None) 17 | decals = decal_dao.get_all_for_body(body_id) 18 | return jsonify([item.Decal.product_joined_to_dict(item.Product) for item in decals]) 19 | 20 | 21 | @decals_blueprint.route('', methods=['POST']) 22 | @jwt_required 23 | @json_required_params(['icon', 'quality', 'paintable', 'id', 'rgba_map']) 24 | def add_decal(): 25 | decal = Decal() 26 | decal.apply_dict(request.json) 27 | 28 | if decal.body_id: 29 | body = body_dao.get(decal.body_id) 30 | if body is not None: 31 | decal.body = body 32 | 33 | decal_dao.add(decal) 34 | database.commit() 35 | return jsonify(decal.to_dict()) 36 | 37 | 38 | @decals_blueprint.route('/', methods=['DELETE']) 39 | @jwt_required 40 | @commit_after 41 | def delete_decal(decal_id): 42 | decal_dao.delete(decal_id) 43 | return '', 200 44 | 45 | 46 | @decals_blueprint.route('/', methods=['PUT']) 47 | @jwt_required 48 | @commit_after 49 | def update(decal_id): 50 | item = decal_dao.get(decal_id) 51 | 52 | if item is None: 53 | raise NotFoundException('Decal not found') 54 | 55 | item.update(request.json) 56 | return '', 200 57 | -------------------------------------------------------------------------------- /backend/blueprints/antennas.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_jwt_extended import jwt_required 3 | from utils.network.decorators import json_required_params, commit_after 4 | from database import database 5 | from entity import Antenna 6 | from dao import AntennaDao 7 | from utils.network.exc import NotFoundException 8 | 9 | antennas_blueprint = Blueprint('antennas', __name__, url_prefix='/internal/antennas') 10 | antenna_dao = AntennaDao() 11 | 12 | 13 | @antennas_blueprint.route('', methods=['GET']) 14 | def get_antennas(): 15 | antennas = antenna_dao.get_all_join_product() 16 | return jsonify([item.Antenna.product_joined_to_dict(item.Product) for item in antennas]) 17 | 18 | 19 | @antennas_blueprint.route('', methods=['POST']) 20 | @jwt_required 21 | @json_required_params(['id', 'icon', 'quality', 'paintable', 'model', 'stick_id']) 22 | def add_antenna(): 23 | antenna = Antenna() 24 | antenna.apply_dict(request.json) 25 | stick = database.get_antenna_stick(antenna.stick_id) 26 | if stick is None: 27 | raise NotFoundException('Antenna stick ID does not exist') 28 | antenna.stick = stick 29 | antenna_dao.add(antenna) 30 | database.commit() 31 | return jsonify(antenna.to_dict()) 32 | 33 | 34 | @antennas_blueprint.route('/', methods=['DELETE']) 35 | @jwt_required 36 | @commit_after 37 | def delete_antenna(antenna_id): 38 | antenna_dao.delete(antenna_id) 39 | return '', 200 40 | 41 | 42 | @antennas_blueprint.route('/', methods=['PUT']) 43 | @jwt_required 44 | @commit_after 45 | def update_antenna(antenna_id): 46 | item = antenna_dao.get(antenna_id) 47 | 48 | if item is None: 49 | raise NotFoundException('Antenna stick not found') 50 | 51 | item.update(request.json) 52 | return '', 200 53 | -------------------------------------------------------------------------------- /cloud_functions/compress_model/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | .env.test 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # next.js build output 75 | .next 76 | 77 | # nuxt.js build output 78 | .nuxt 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless/ 85 | 86 | # FuseBox cache 87 | .fusebox/ 88 | 89 | # DynamoDB Local files 90 | .dynamodb/ 91 | 92 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/api-keys/api-keys.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ApiKeysService } from '../../../service/api-keys.service'; 3 | import { ApiKey } from '../../../model/api-key'; 4 | import { MatDialog } from '@angular/material/dialog'; 5 | import { CreateApiKeyComponent } from './create-api-key/create-api-key.component'; 6 | import { confirmMaterial } from '../../../shared/confirm-dialog/confirm-dialog.component'; 7 | import { copyMessage } from '../../../utils/util'; 8 | 9 | @Component({ 10 | selector: 'app-api-keys', 11 | templateUrl: './api-keys.component.html', 12 | styleUrls: ['./api-keys.component.scss'] 13 | }) 14 | export class ApiKeysComponent implements OnInit { 15 | 16 | keys: ApiKey[] = []; 17 | 18 | copy = copyMessage; 19 | 20 | constructor(private apiKeysService: ApiKeysService, 21 | private dialog: MatDialog) { 22 | } 23 | 24 | ngOnInit() { 25 | this.apiKeysService.getKeys().subscribe(keys => this.keys = keys); 26 | } 27 | 28 | openCreateDialog() { 29 | const dialogRef = this.dialog.open(CreateApiKeyComponent, {width: '500px'}); 30 | dialogRef.afterClosed().subscribe(newKey => { 31 | if (newKey != undefined) { 32 | this.keys.push(newKey); 33 | } 34 | }); 35 | } 36 | 37 | deleteKey(key: ApiKey) { 38 | confirmMaterial(`Delete API key ${key.name}?`, this.dialog, () => { 39 | this.apiKeysService.deleteKey(key.id).subscribe(() => { 40 | this.keys.splice(this.keys.indexOf(key), 1); 41 | }); 42 | }); 43 | } 44 | 45 | toggleKey(key: ApiKey) { 46 | let msg = key.active ? 'Deactivate' : 'Activate'; 47 | msg = msg + ` ${key.name}?`; 48 | 49 | confirmMaterial(msg, this.dialog, () => { 50 | key.active = !key.active; 51 | this.apiKeysService.updateKey(key).subscribe(); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cloud_functions/convert_texture/main.py: -------------------------------------------------------------------------------- 1 | import io 2 | from PIL import Image 3 | from google.cloud.storage import Client 4 | 5 | client = Client() 6 | 7 | 8 | def convert(event, context): 9 | """Triggered by a change to a Cloud Storage bucket. 10 | Args: 11 | event (dict): Event payload. 12 | context (google.cloud.functions.Context): Metadata for the event. 13 | """ 14 | 15 | if not event['name'].endswith('.tga') or event['name'].endswith('_S.tga'): 16 | return 17 | 18 | print(f"Converting file: {event['name']}") 19 | 20 | bucket = client.get_bucket(event['bucket']) 21 | blob = bucket.get_blob(event['name']) 22 | image_data = blob.download_as_string() 23 | image = Image.open(io.BytesIO(image_data)) 24 | 25 | with io.BytesIO() as output: 26 | new_name = event['name'].replace('.tga', '.png') 27 | print(f'Uploading {new_name}') 28 | image.save(output, format="PNG") 29 | output.seek(0) 30 | new_blob = bucket.blob(new_name) 31 | new_blob.upload_from_file(output, content_type='image/png') 32 | 33 | small_size = (image.size[0] / 2, image.size[1] / 2) 34 | image.thumbnail(small_size, Image.LANCZOS) 35 | 36 | with io.BytesIO() as output: 37 | new_name = event['name'].replace('.tga', '_S.png') 38 | print(f'Uploading {new_name}') 39 | image.save(output, format="PNG") 40 | output.seek(0) 41 | new_blob = bucket.blob(new_name) 42 | new_blob.upload_from_file(output, content_type='image/png') 43 | 44 | with io.BytesIO() as output: 45 | new_name = event['name'].replace('.tga', '_S.tga') 46 | print(f'Uploading {new_name}') 47 | image.save(output, format="TGA") 48 | output.seek(0) 49 | new_blob = bucket.blob(new_name) 50 | new_blob.upload_from_file(output, content_type='application/octet-stream') 51 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rl-loadout", 3 | "version": "0.9.6", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^9.0.0-rc.5", 15 | "@angular/cdk": "^9.0.0-rc.5", 16 | "@angular/common": "~9.0.0-rc.5", 17 | "@angular/compiler": "~9.0.0-rc.5", 18 | "@angular/core": "~9.0.0-rc.5", 19 | "@angular/forms": "~9.0.0-rc.5", 20 | "@angular/material": "^9.0.0-rc.5", 21 | "@angular/platform-browser": "~9.0.0-rc.5", 22 | "@angular/platform-browser-dynamic": "~9.0.0-rc.5", 23 | "@angular/router": "~9.0.0-rc.5", 24 | "@types/dat-gui": "^0.6.3", 25 | "@types/stats.js": "^0.17.0", 26 | "angular-notifier": "^6.0.1", 27 | "dat.gui": "^0.7.6", 28 | "ngx-color-picker": "^8.2.0", 29 | "ngx-mat-select-search": "^2.0.0", 30 | "rl-loadout-lib": "file:../lib", 31 | "rxjs": "~6.5.3", 32 | "stats.js": "^0.17.0", 33 | "three": "^0.110.0", 34 | "tslib": "^1.10.0", 35 | "zone.js": "~0.10.2" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/build-angular": "~0.900.0-rc.5", 39 | "@angular/cli": "~9.0.0-rc.5", 40 | "@angular/compiler-cli": "~9.0.0-rc.5", 41 | "@angular/language-service": "~9.0.0-rc.5", 42 | "@types/node": "^12.11.1", 43 | "@types/jasmine": "~3.4.4", 44 | "@types/jasminewd2": "~2.0.3", 45 | "codelyzer": "^5.1.2", 46 | "jasmine-core": "~3.5.0", 47 | "jasmine-spec-reporter": "~4.2.1", 48 | "karma": "~4.3.0", 49 | "karma-chrome-launcher": "~3.1.0", 50 | "karma-coverage-istanbul-reporter": "~2.1.0", 51 | "karma-jasmine": "~2.0.1", 52 | "karma-jasmine-html-reporter": "^1.4.2", 53 | "protractor": "~5.4.2", 54 | "ts-node": "~7.0.1", 55 | "tslint": "~5.20.0", 56 | "typescript": "~3.6.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/blueprints/product.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from flask import Blueprint, request, jsonify 3 | from flask_jwt_extended import jwt_required 4 | from dao import ProductDao 5 | from entity import Product 6 | from database import database 7 | from utils.network.exc import NotFoundException, BadRequestException 8 | 9 | products_blueprint = Blueprint('product', __name__, url_prefix='/internal/products') 10 | product_dao = ProductDao() 11 | 12 | 13 | @products_blueprint.route('upload', methods=['POST']) 14 | @jwt_required 15 | def upload(): 16 | f = request.files['file'] 17 | df = pd.read_csv(f, encoding='ISO-8859-1', header=None, names=['id', 'type', 'product', 'name']) 18 | df['product'] = df['product'].str.replace('Product_TA ProductsDB.Products.', '') 19 | 20 | for index, row in df.iterrows(): 21 | product_id = int(row['id']) 22 | product = product_dao.get(product_id) 23 | 24 | if product is None: 25 | product = Product(id=product_id, type=row['type'], product_name=row['product'], name=row['name']) 26 | product_dao.add(product) 27 | else: 28 | product.type = row['type'] 29 | product.product_name = row['product'] 30 | product.name = row['name'] 31 | 32 | database.commit() 33 | 34 | return '', 200 35 | 36 | 37 | @products_blueprint.route('', methods=['GET']) 38 | @jwt_required 39 | def get_all(): 40 | products = product_dao.get_all() 41 | return jsonify([product.to_dict() for product in products]) 42 | 43 | 44 | @products_blueprint.route('/', methods=['GET']) 45 | @jwt_required 46 | def get(product_id): 47 | try: 48 | product_id = int(product_id) 49 | except ValueError: 50 | raise BadRequestException('product id must be an integer') 51 | 52 | product = product_dao.get(product_id) 53 | 54 | if product is None: 55 | raise NotFoundException('Product not found') 56 | 57 | return jsonify(product.to_dict()) 58 | -------------------------------------------------------------------------------- /backend/entity/wheel.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from sqlalchemy import Column, String 3 | from .base import Base 4 | from .item import BaseItem 5 | 6 | 7 | class Wheel(Base, BaseItem): 8 | __tablename__ = 'wheel' 9 | model = Column(String(255), nullable=False) 10 | rim_base = Column(String(255), nullable=True) 11 | rim_rgb_map = Column(String(255), nullable=True) 12 | rim_n = Column(String(255), nullable=True) 13 | tire_base = Column(String(255), nullable=True) 14 | tire_n = Column(String(255), nullable=True) 15 | 16 | def apply_dict(self, item_dict: Dict): 17 | super().apply_dict(item_dict) 18 | self.model = item_dict.get('model', None) 19 | self.rim_base = item_dict.get('rim_base', None) 20 | self.rim_rgb_map = item_dict.get('rim_rgb_map', None) 21 | self.rim_n = item_dict.get('rim_n', None) 22 | self.tire_base = item_dict.get('tire_base', None) 23 | self.tire_n = item_dict.get('tire_n', None) 24 | 25 | def to_dict(self) -> Dict: 26 | d = super(Wheel, self).to_dict() 27 | 28 | d['model'] = self.model 29 | d['rim_base'] = self.rim_base 30 | d['rim_rgb_map'] = self.rim_rgb_map 31 | d['rim_n'] = self.rim_n 32 | d['tire_base'] = self.tire_base 33 | d['tire_n'] = self.tire_n 34 | 35 | return d 36 | 37 | def update(self, item_dict: Dict): 38 | super(Wheel, self).update(item_dict) 39 | if 'model' in item_dict: 40 | self.model = item_dict['model'] 41 | if 'rim_base' in item_dict: 42 | self.rim_base = item_dict['rim_base'] 43 | if 'rim_rgb_map' in item_dict: 44 | self.rim_rgb_map = item_dict['rim_rgb_map'] 45 | if 'rim_n' in item_dict: 46 | self.rim_n = item_dict['rim_n'] 47 | if 'tire_base' in item_dict: 48 | self.tire_base = item_dict['tire_base'] 49 | if 'tire_n' in item_dict: 50 | self.tire_n = item_dict['tire_n'] 51 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/debug/texture-viewer/texture-viewer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; 2 | import { TextureService } from '../../../../service/texture.service'; 3 | import { MatSnackBar } from '@angular/material/snack-bar'; 4 | 5 | @Component({ 6 | selector: 'app-texture-viewer', 7 | templateUrl: './texture-viewer.component.html', 8 | styleUrls: ['./texture-viewer.component.scss'] 9 | }) 10 | export class TextureViewerComponent implements OnInit { 11 | 12 | @ViewChild('canvas', {static: true}) 13 | canvasRef: ElementRef; 14 | canvas: HTMLCanvasElement; 15 | context: CanvasRenderingContext2D; 16 | canvasWidth = 900; 17 | canvasHeight = 900; 18 | 19 | textures: string[]; 20 | 21 | selected: string; 22 | 23 | constructor(private textureService: TextureService, 24 | private snackBar: MatSnackBar) { 25 | } 26 | 27 | ngOnInit() { 28 | this.textures = this.textureService.getKeys(); 29 | this.canvas = this.canvasRef.nativeElement; 30 | this.context = this.canvas.getContext('2d'); 31 | } 32 | 33 | selectTexture() { 34 | const texture = this.textureService.get(this.selected); 35 | 36 | if (texture == undefined) { 37 | this.snackBar.open('texture is undefined', undefined, {duration: 2000}); 38 | return; 39 | } 40 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 41 | 42 | const width = texture.image.width; 43 | const height = texture.image.height; 44 | 45 | this.canvas.width = width; 46 | this.canvas.height = height; 47 | 48 | this.canvasWidth = Math.min(width, 900); 49 | this.canvasHeight = this.canvasWidth * (height / width); 50 | 51 | // @ts-ignore 52 | if (texture.isDataTexture) { 53 | const imageData = new ImageData(new Uint8ClampedArray(texture.image.data), width, height); 54 | this.context.putImageData(imageData, 0, 0); 55 | } else { 56 | this.context.drawImage(texture.image, 0, 0); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /backend/server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import timedelta 3 | from flask import jsonify 4 | from flask_cors import CORS 5 | from flask_jwt_extended import JWTManager 6 | from werkzeug.exceptions import HTTPException, default_exceptions 7 | import connexion 8 | from utils.network import log_endpoints 9 | from utils.network.exc import HttpException 10 | from config import config 11 | from database import database 12 | from logging_config import logging_config 13 | from blueprints import blueprints 14 | from _version import __version__ 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | connexion_app = connexion.App(__name__, arguments={ 19 | 'server_host': config.get('server', 'host'), 20 | 'version': __version__ 21 | }) 22 | connexion_app.add_api('api_swagger.yml') 23 | app = connexion_app.app 24 | 25 | for blueprint in blueprints: 26 | app.register_blueprint(blueprint) 27 | 28 | app.config['JWT_SECRET_KEY'] = config.get('server', 'jwt_secret') 29 | app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(days=1) 30 | jwt = JWTManager(app) 31 | CORS(app) 32 | 33 | 34 | @app.teardown_request 35 | def teardown_request(exception): 36 | if exception: 37 | database.Session.rollback() 38 | database.Session.remove() 39 | 40 | 41 | @app.errorhandler(HttpException) 42 | def handle_http_exception(e: HttpException): 43 | return jsonify({ 44 | 'status': e.code, 45 | 'detail': e.message 46 | }), e.code 47 | 48 | 49 | @app.errorhandler(Exception) 50 | def handle_error(e): 51 | code = 500 52 | if isinstance(e, HTTPException): 53 | code = e.code 54 | return jsonify({ 55 | 'status': code, 56 | 'detail': str(e) 57 | }), code 58 | 59 | 60 | if __name__ == '__main__': 61 | logging_config() 62 | port = int(config.get('server', 'port')) 63 | log.info(f'Running rl-loadout {__version__} on port {port}') 64 | log_endpoints(log, app) 65 | 66 | for ex in default_exceptions: 67 | app.register_error_handler(ex, handle_error) 68 | 69 | connexion_app.run(host='0.0.0.0', port=port) 70 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-antenna/create-antenna.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { Quality, Antenna, AntennaStick } from 'rl-loadout-lib'; 3 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 4 | import { MatSnackBar } from '@angular/material/snack-bar'; 5 | import { CloudStorageService } from '../../../../service/cloud-storage.service'; 6 | import { CreateDialog } from '../create-dialog'; 7 | import { AntennasService } from '../../../../service/items/antennas.service'; 8 | import { AntennaSticksService } from '../../../../service/items/antenna-sticks.service'; 9 | import { ProductService } from '../../../../service/product.service'; 10 | 11 | @Component({ 12 | selector: 'app-create-antenna', 13 | templateUrl: './create-antenna.component.html', 14 | styleUrls: ['./create-antenna.component.scss'] 15 | }) 16 | export class CreateAntennaComponent extends CreateDialog implements OnInit { 17 | 18 | productType = 'antenna'; 19 | 20 | sticks: AntennaStick[]; 21 | 22 | constructor(dialogRef: MatDialogRef, 23 | cloudService: CloudStorageService, 24 | antennasService: AntennasService, 25 | private antennaSticksService: AntennaSticksService, 26 | productService: ProductService, 27 | snackBar: MatSnackBar, 28 | @Inject(MAT_DIALOG_DATA) data: Antenna) { 29 | super(dialogRef, cloudService, snackBar, data, productService, antennasService); 30 | this.item = new Antenna( 31 | undefined, undefined, '', Quality.COMMON, false 32 | ); 33 | } 34 | 35 | ngOnInit() { 36 | super.ngOnInit(); 37 | this.antennaSticksService.getAll().subscribe(sticks => this.sticks = sticks); 38 | } 39 | 40 | selectProduct($event: string) { 41 | super.selectProduct($event); 42 | 43 | const model = this.selectedObjects.find(value => !value.endsWith('draco.glb') && value.endsWith('.glb')); 44 | if (model != undefined) { 45 | this.item.model = model; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/app/home/components/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Rocket Loadout
Alpha
5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | RGB splitter 26 | 27 | -------------------------------------------------------------------------------- /tools/extract_static_skins.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | import subprocess 4 | import shutil 5 | from multiprocessing.pool import ThreadPool 6 | from functools import partial 7 | 8 | 9 | def umodel_extract(path, file): 10 | print(f'Extracting {file}...') 11 | command = ['umodel', '-game=rocketleague', f'-path={path}', '-export', file] 12 | print(f'Running {" ".join(command)}') 13 | p = subprocess.Popen(command, stdout=subprocess.PIPE) 14 | for line in p.stdout: 15 | print(line.decode("utf-8"), end='') 16 | p.wait(timeout=300) 17 | 18 | if p.returncode != 0: 19 | print(f'umodel returned with non-zero exit code: {p.returncode}') 20 | 21 | return p.returncode == 0 22 | 23 | 24 | def process_file(file, args): 25 | output_dir = os.path.join(args.output, file.replace('_SF.upk', '')) 26 | 27 | if os.path.exists(output_dir): 28 | return 29 | 30 | os.makedirs(output_dir, exist_ok=True) 31 | 32 | if not umodel_extract(args.directory, file): 33 | return 34 | 35 | texture_folder = os.path.join('UmodelExport', os.path.splitext(file)[0], 'Texture2D') 36 | if not os.path.exists(texture_folder): 37 | return 38 | 39 | for texture in os.listdir(texture_folder): 40 | if texture.endswith('.tga'): 41 | shutil.copyfile(os.path.join(texture_folder, texture), os.path.join(output_dir, texture)) 42 | 43 | 44 | if __name__ == '__main__': 45 | parser = argparse.ArgumentParser() 46 | parser.add_argument('-d', '--directory', type=str, required=True) 47 | parser.add_argument('-o', '--output', type=str, required=True) 48 | parser.add_argument('-f', '--from-item', type=str, required=False) 49 | args = parser.parse_args() 50 | 51 | files = os.listdir(args.directory) 52 | 53 | if args.from_item is not None: 54 | files = files[files.index(args.from_item):] 55 | 56 | os.makedirs(args.output, exist_ok=True) 57 | 58 | with ThreadPool(10) as p: 59 | for _ in p.imap_unordered(partial(process_file, args=args), filter( 60 | lambda x: x.lower().startswith('skin_') and not x.endswith('_T_SF.upk'), files)): 61 | pass 62 | -------------------------------------------------------------------------------- /tools/update_checklist.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pandas as pd 3 | import gspread 4 | from oauth2client.service_account import ServiceAccountCredentials 5 | 6 | 7 | def get_sheet(key_file, sheet_name, worksheet): 8 | """ 9 | Append a row to the google sheet. 10 | 11 | :param key_file: key file to use for auth 12 | :param sheet_name: the name of the sheet 13 | :param worksheet: the name of the worksheet 14 | :param row: the row to append 15 | """ 16 | print('Authorizing gspread...') 17 | scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive'] 18 | credentials = ServiceAccountCredentials.from_json_keyfile_name(key_file, scope) 19 | gc = gspread.authorize(credentials) 20 | 21 | return gc.open(sheet_name).worksheet(worksheet) 22 | 23 | 24 | def append_rows(sheet, values, value_input_option='RAW'): 25 | """Adds multiple rows to the worksheet with values populated. 26 | The input should be a list of lists, with the lists each 27 | containing one row's values. 28 | Widens the worksheet if there are more values than columns. 29 | :param values: List of row lists. 30 | """ 31 | params = { 32 | 'valueInputOption': value_input_option 33 | } 34 | 35 | body = { 36 | 'majorDimension': 'ROWS', 37 | 'values': values 38 | } 39 | 40 | return sheet.spreadsheet.values_append(sheet.title, params, body) 41 | 42 | 43 | parser = argparse.ArgumentParser() 44 | parser.add_argument('-i', '--input', type=str, required=True) 45 | parser.add_argument('-k', '--key-file', type=str, required=True) 46 | args = parser.parse_args() 47 | 48 | items_df = pd.read_csv(args.input, encoding='ISO-8859-1', header=None, names=['id', 'type', 'product', 'name']) 49 | items_df['product'] = items_df['product'].str.replace('Product_TA ProductsDB.Products.', '') 50 | 51 | sheet = get_sheet(args.key_file, 'rl-loadout', 'Items') 52 | 53 | ids = set(map(int, sheet.col_values(1)[1:])) 54 | 55 | rows_to_add = [] 56 | 57 | for index, row in items_df.iterrows(): 58 | if int(row['id']) in ids: 59 | continue 60 | 61 | rows_to_add.append([row['id'], row['type'], row['product'], row['name'], 'FALSE']) 62 | 63 | append_rows(sheet, rows_to_add, 'USER_ENTERED') 64 | -------------------------------------------------------------------------------- /backend/entity/body.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from sqlalchemy import Column, String, Boolean 3 | from sqlalchemy.orm import relationship 4 | from .base import Base 5 | from .item import BaseItem 6 | 7 | 8 | class Body(Base, BaseItem): 9 | __tablename__ = 'body' 10 | model = Column(String(255), nullable=False) 11 | blank_skin = Column(String(255), nullable=True) 12 | base_skin = Column(String(255), nullable=True) 13 | chassis_base = Column(String(255), nullable=True) 14 | chassis_n = Column(String(255), nullable=True) 15 | chassis_paintable = Column(Boolean(), nullable=False, default=False) 16 | decals = relationship('Decal') 17 | 18 | def apply_dict(self, item_dict: Dict): 19 | super().apply_dict(item_dict) 20 | self.model = item_dict.get('model', None) 21 | self.blank_skin = item_dict.get('blank_skin', None) 22 | self.base_skin = item_dict.get('base_skin', None) 23 | self.chassis_base = item_dict.get('chassis_base', None) 24 | self.chassis_n = item_dict.get('chassis_n', None) 25 | self.chassis_paintable = item_dict.get('chassis_paintable', None) 26 | 27 | def to_dict(self) -> Dict: 28 | d = super(Body, self).to_dict() 29 | 30 | d['model'] = self.model 31 | d['blank_skin'] = self.blank_skin 32 | d['base_skin'] = self.base_skin 33 | d['chassis_base'] = self.chassis_base 34 | d['chassis_n'] = self.chassis_n 35 | d['chassis_paintable'] = self.chassis_paintable 36 | 37 | return d 38 | 39 | def update(self, item_dict: Dict): 40 | super().update(item_dict) 41 | if 'model' in item_dict: 42 | self.model = item_dict['model'] 43 | if 'blank_skin' in item_dict: 44 | self.blank_skin = item_dict['blank_skin'] 45 | if 'base_skin' in item_dict: 46 | self.base_skin = item_dict['base_skin'] 47 | if 'chassis_base' in item_dict: 48 | self.chassis_base = item_dict['chassis_base'] 49 | if 'chassis_n' in item_dict: 50 | self.chassis_n = item_dict['chassis_n'] 51 | if 'model' in item_dict: 52 | self.model = item_dict['model'] 53 | if 'chassis_paintable' in item_dict: 54 | self.chassis_paintable = item_dict['chassis_paintable'] 55 | -------------------------------------------------------------------------------- /frontend/src/app/admin/components/dialog/create-topper/create-topper.component.html: -------------------------------------------------------------------------------- 1 |

Edit item

2 |
3 | 4 | 5 | {{obj}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Quality 16 | 17 | 18 | {{obj.name}} 19 | 20 | 21 | 22 | 23 | Product 24 | 25 | 26 | 28 | 29 | 30 | {{obj}} 31 | 32 | 33 | 34 | 35 | 36 | 37 | Paintable 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 |
55 | -------------------------------------------------------------------------------- /tools/extract_thumbnails.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | import subprocess 4 | from multiprocessing.pool import ThreadPool 5 | from functools import partial 6 | from PIL import Image 7 | 8 | 9 | def umodel_extract(path, file): 10 | print(f'Extracting {file}...') 11 | command = ['umodel', '-game=rocketleague', f'-path={path}', '-export', file] 12 | print(f'Running {" ".join(command)}') 13 | p = subprocess.Popen(command, stdout=subprocess.PIPE) 14 | for line in p.stdout: 15 | print(line.decode("utf-8"), end='') 16 | p.wait(timeout=600) 17 | 18 | if p.returncode != 0: 19 | print(f'umodel returned with non-zero exit code: {p.returncode}') 20 | 21 | return p.returncode == 0 22 | 23 | 24 | def process_file(file, args, existing): 25 | output_dir = file.replace('_T_SF.upk', '') 26 | 27 | try: 28 | output_dir = next(x for x in existing if x.lower() == output_dir.lower()) 29 | except StopIteration: 30 | return 31 | 32 | output_dir = os.path.join(args.output, output_dir) 33 | 34 | os.makedirs(output_dir, exist_ok=True) 35 | 36 | if not umodel_extract(args.directory, file): 37 | return 38 | 39 | texture_folder = os.path.join('UmodelExport', os.path.splitext(file)[0], 'Texture2D') 40 | if not os.path.exists(texture_folder): 41 | return 42 | 43 | for texture in os.listdir(texture_folder): 44 | if texture.endswith('.tga'): 45 | img = Image.open(os.path.join(texture_folder, texture)) 46 | rgb_im = img.convert('RGB') 47 | rgb_im.save(os.path.join(output_dir, texture.replace('.tga', '.jpg'))) 48 | 49 | 50 | if __name__ == '__main__': 51 | parser = argparse.ArgumentParser() 52 | parser.add_argument('-d', '--directory', type=str, required=True) 53 | parser.add_argument('-o', '--output', type=str, required=True) 54 | parser.add_argument('-f', '--from-item', type=str, required=False) 55 | args = parser.parse_args() 56 | 57 | files = os.listdir(args.directory) 58 | 59 | if args.from_item is not None: 60 | files = files[files.index(args.from_item):] 61 | 62 | os.makedirs(args.output, exist_ok=True) 63 | 64 | existing = os.listdir(args.output) 65 | 66 | with ThreadPool(10) as p: 67 | for _ in p.imap_unordered(partial(process_file, args=args, existing=existing), 68 | filter(lambda x: x.endswith('_T_SF.upk'), files)): 69 | pass 70 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | !/controllers/wheels/ 128 | --------------------------------------------------------------------------------