├── 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 | No 4 | 5 | Yes 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 | Upload items.csv 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 | Login 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 | 6 | 7 | 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 | Cancel 13 | Save 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 | Cancel 14 | Save 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 | 11 | edit 12 | 13 | 14 | delete 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | add 24 | 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 | 12 | {{key.active ? 'Active' : 'Inactive'}} 13 | 14 | 15 | file_copy 16 | 17 | 18 | delete 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | add 27 | 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 | 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 | Items arrow_drop_down 5 | API Keys 6 | Product Upload 7 | 8 | 9 | {{ username }} arrow_drop_down 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Logout 18 | 19 | 20 | Antennas 21 | Antenna sticks 22 | Bodies 23 | Decals 24 | Toppers 25 | Wheels 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 LoadoutAlpha 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | info 13 | 14 | 15 | bug_report 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Admin page 24 | Texture viewer 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 | Cancel 53 | Save 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 | --------------------------------------------------------------------------------
Create and share car designs for Rocket League. Built by klay.
Rocket Loadout is not affiliated with Psyonix, Inc. All models and textures belong to Psyonix, Inc.
View source on GitHub.
Hitbox data provided by Trelgne.