diff --git a/app/helpers/dependencies.ts b/app/helpers/dependencies.ts new file mode 100644 index 0000000..358c4d0 --- /dev/null +++ b/app/helpers/dependencies.ts @@ -0,0 +1,67 @@ +import * as fs from 'fs-extra'; +import * as recursiveReadDir from 'recursive-readdir'; +import { SendToUI } from '../types'; +import { baseUrl } from './constants'; + +const depDir = `${baseUrl}/resources/dependencies`; + +export async function addDependency(sendToUI: SendToUI, data: any) { + sendToUI('notify', { + type: 'info', + text: `Attempting to download mod dependency ${data}`, + }); + + try { + const res = await fetch(data); + const resJson = await res.json(); + + if ( + !resJson.meta || + !resJson.meta.name || + !resJson.meta.author || + !resJson.meta.savedAt || + !resJson.meta.version + ) { + sendToUI('notify', { + type: 'error', + text: 'Malformed mod!', + }); + return; + } + + resJson.meta._url = data; + + fs.ensureDirSync(depDir); + fs.writeJSONSync(`${depDir}/${resJson.meta.name}.rairmod`, resJson); + + getAndSendDependencies(sendToUI); + + sendToUI('notify', { + type: 'success', + text: `Got dependency mod "${resJson.meta.name}"!`, + }); + + sendToUI('adddependency', { name: resJson.meta.name, url: data }); + } catch { + sendToUI('notify', { + type: 'error', + text: 'Malformed mod URL!', + }); + } +} + +export async function getAndSendDependencies(sendToUI: SendToUI) { + const deps = await getDependencies(); + sendToUI('dependencies', deps); +} + +export async function getDependencies() { + fs.ensureDirSync(depDir); + + const allDeps = await recursiveReadDir(depDir); + const allDepData = allDeps + .filter((f) => f.includes('rairmod')) + .map((f) => fs.readJSONSync(f)); + + return allDepData; +} diff --git a/app/helpers/index.ts b/app/helpers/index.ts index 399e5ad..0fd12df 100644 --- a/app/helpers/index.ts +++ b/app/helpers/index.ts @@ -1,2 +1,3 @@ export * from './constants'; +export * from './dependencies'; export * from './modtest'; diff --git a/app/ipc.ts b/app/ipc.ts index 25b6ef0..a6172d7 100644 --- a/app/ipc.ts +++ b/app/ipc.ts @@ -315,4 +315,12 @@ export function setupIPC(sendToUI: SendToUI) { sendToUI('notify', { type: 'info', text: 'Killing LotR/MongoDB...' }); helpers.killMod(sendToUI); }); + + ipcMain.on('GET_DEPENDENCIES', async () => { + helpers.getAndSendDependencies(sendToUI); + }); + + ipcMain.on('ADD_DEPENDENCY', async (e: any, data: any) => { + helpers.addDependency(sendToUI, data); + }); } diff --git a/app/main.ts b/app/main.ts index 28ce524..65ad46a 100644 --- a/app/main.ts +++ b/app/main.ts @@ -12,8 +12,6 @@ const isDevelopment = !app.isPackaged; log.transports.file.level = 'info'; -console.log(`Starting in ${isDevelopment ? 'dev' : 'prod'} mode...`); - log.transports.file.resolvePath = () => path.join(app.getAppPath(), 'logs/main.log'); @@ -123,11 +121,17 @@ async function createWindow(): Promise { }, }); - win.setMenu(null); + if (!isDevelopment) { + win.setMenu(null); + } win.once('ready-to-show', () => { win?.show(); handleSetup(); + + if (isDevelopment) { + win?.webContents.openDevTools(); + } }); // load intercepter for image loading @@ -160,14 +164,12 @@ async function createWindow(): Promise { await win.loadURL(url.href); } - if (isDevelopment) { - win.webContents.openDevTools(); - } - return win; } try { + console.log(`Starting in ${isDevelopment ? 'dev' : 'prod'} mode...`); + protocol.registerSchemesAsPrivileged([ { scheme: 'lotr', privileges: { bypassCSP: true, supportFetchAPI: true } }, ]); diff --git a/src/app/dependencies/dependencies.component.html b/src/app/dependencies/dependencies.component.html new file mode 100644 index 0000000..d0f5e94 --- /dev/null +++ b/src/app/dependencies/dependencies.component.html @@ -0,0 +1,65 @@ +
+ +
+ + +
+ +
+ +
+
+ + @if(modService.activeDependencies().length === 0) { +
+

You don't currently have any dependencies.

+
+ } + + @for(dep of modService.activeDependencies(); track $index) { +
+
+
+

{{ dep.meta.name }}

+

{{ dep.meta.author }} · Last edited {{ dep.meta.savedAt | date }}

+ +

{{ dep.meta._url }}

+
+
+ +
+
+
    +
  • {{ dep.achievements.length | number }} Achievements
  • +
  • {{ dep.items.length | number }} Items
  • +
  • {{ dep.npcs.length | number }} NPCs
  • +
  • {{ dep.dialogs.length | number }} NPC Scripts
  • +
  • {{ dep.quests.length | number }} Quests
  • +
  • {{ dep.recipes.length | number }} Recipes
  • +
  • {{ dep.spawners.length | number }} Spawners
  • +
  • {{ dep.stems.length | number }} STEMs
  • +
+
+
+ +
+
+ + +
+
+
+ } + +
+
+ + + \ No newline at end of file diff --git a/src/app/dependencies/dependencies.component.scss b/src/app/dependencies/dependencies.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dependencies/dependencies.component.ts b/src/app/dependencies/dependencies.component.ts new file mode 100644 index 0000000..2b90739 --- /dev/null +++ b/src/app/dependencies/dependencies.component.ts @@ -0,0 +1,24 @@ +import { Component, inject, output } from '@angular/core'; +import { ElectronService } from '../services/electron.service'; +import { ModService } from '../services/mod.service'; + +@Component({ + selector: 'app-dependencies', + templateUrl: './dependencies.component.html', + styleUrl: './dependencies.component.scss', +}) +export class DependenciesComponent { + public exit = output(); + + public modService = inject(ModService); + private electronService = inject(ElectronService); + + addNewDependency(dependency: string) { + if (!dependency?.trim()) return; + this.electronService.send('ADD_DEPENDENCY', dependency); + } + + removeDependency(dependency: string) { + this.modService.removeModDependency(dependency); + } +} diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index a4bbb27..d24d316 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -27,6 +27,8 @@ [class.btn-disabled]="!electronService.isInElectron()">Change Mod Name
  • Change Mod Author
  • +
  • Manage Dependencies
  • +} @else if(isManagingDependencies()) { +
    +
    +
    +
    + +
    +
    +
    +
    + } @else if(pinpointService.isPinpointing()) {
    diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 6e24ed5..ed99a21 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -43,6 +43,7 @@ export class HomeComponent implements OnInit { public activeTab = signal(0); public isValidating = signal(false); + public isManagingDependencies = signal(false); public hasErrors = computed(() => { const mod = this.modService.mod(); @@ -130,6 +131,10 @@ export class HomeComponent implements OnInit { case 'query': { return this.toggleQuerying(); } + case 'dependencies': { + this.isManagingDependencies.set(true); + return; + } } } @@ -185,8 +190,25 @@ export class HomeComponent implements OnInit { saveMod(); } + toggleDependencies() { + this.isManagingDependencies.set(true); + this.pinpointService.togglePinpointing(false); + this.isValidating.set(false); + this.analysisService.toggleAnalyzing(false); + this.queryService.toggleQuerying(false); + + void this.router.navigate([], { + relativeTo: this.route, + queryParamsHandling: 'merge', + queryParams: { + sub: 'dependencies', + }, + }); + } + toggleModValidation() { this.isValidating.set(!this.isValidating()); + this.isManagingDependencies.set(false); this.pinpointService.togglePinpointing(false); this.analysisService.toggleAnalyzing(false); this.queryService.toggleQuerying(false); @@ -202,6 +224,7 @@ export class HomeComponent implements OnInit { togglePinpointing() { this.pinpointService.togglePinpointing(true); + this.isManagingDependencies.set(false); this.isValidating.set(false); this.analysisService.toggleAnalyzing(false); this.queryService.toggleQuerying(false); @@ -217,6 +240,7 @@ export class HomeComponent implements OnInit { toggleAnalyzing() { this.analysisService.toggleAnalyzing(true); + this.isManagingDependencies.set(false); this.isValidating.set(false); this.pinpointService.togglePinpointing(false); this.queryService.toggleQuerying(false); @@ -232,6 +256,7 @@ export class HomeComponent implements OnInit { toggleQuerying() { this.queryService.toggleQuerying(true); + this.isManagingDependencies.set(false); this.isValidating.set(false); this.pinpointService.togglePinpointing(false); this.analysisService.toggleAnalyzing(false); diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts index fe4b490..bde62cc 100644 --- a/src/app/home/home.module.ts +++ b/src/app/home/home.module.ts @@ -11,6 +11,7 @@ import { ColorPickerModule } from 'ngx-color-picker'; import { NgxFloatUiModule } from 'ngx-float-ui'; import { AnalysisComponent } from '../analysis/analysis.component'; +import { DependenciesComponent } from '../dependencies/dependencies.component'; import { PinpointComponent } from '../pinpoint/pinpoint.component'; import { QueryComponent } from '../query/query.component'; import { SharedModule } from '../shared/shared.module'; @@ -70,6 +71,7 @@ import { HomeComponent } from './home.component'; PinpointComponent, AnalysisComponent, QueryComponent, + DependenciesComponent, ], imports: [ CommonModule, diff --git a/src/app/query/query.component.ts b/src/app/query/query.component.ts index a9da046..4403e6a 100644 --- a/src/app/query/query.component.ts +++ b/src/app/query/query.component.ts @@ -82,7 +82,7 @@ export class QueryComponent { try { // eslint-disable-next-line @typescript-eslint/no-implied-eval - const func = new Function(`return ${this.jsModel.value};`); + const func = new Function(`return ${this.jsModel.value.trim()};`); const modifiableMod = this.queryService.modForJSModifiable(); const result = func()(this.queryService.modForJS(), modifiableMod); this.queryService.updateMod(modifiableMod); diff --git a/src/app/services/electron.service.ts b/src/app/services/electron.service.ts index 01b6826..da180e6 100644 --- a/src/app/services/electron.service.ts +++ b/src/app/services/electron.service.ts @@ -1,6 +1,11 @@ import { computed, effect, inject, Injectable, signal } from '@angular/core'; -import { IEditorMap, IModKit, ModJSONKey } from '../../interfaces'; +import { + IEditorMap, + IModKit, + IModKitDependency, + ModJSONKey, +} from '../../interfaces'; import { importMod } from '../helpers/importer'; import { ModService } from './mod.service'; import { NotifyService } from './notify.service'; @@ -106,6 +111,7 @@ export class ElectronService { this.send('GET_VERSION'); this.send('GET_BASEURL'); + this.send('GET_DEPENDENCIES'); this.requestAllJSON(); tryEnsureMaps(); @@ -180,11 +186,18 @@ export class ElectronService { this.baseUrl.set(baseurl as string) ); + window.api.receive('dependencies', (deps) => { + this.modService.setDependencies(deps as IModKit[]); + }); + + window.api.receive('adddependency', (dep) => { + this.modService.addModDependency(dep as IModKitDependency); + }); + const quicksaveFilepath = this.quicksaveFilepath(); if (quicksaveFilepath) { this.needsLoadForReadyCheck.set(true); this.send('LOAD_MOD_QUIETLY', { path: quicksaveFilepath }); - return; } this.send('READY_CHECK'); diff --git a/src/app/services/mod.service.ts b/src/app/services/mod.service.ts index 2e97063..48cf128 100644 --- a/src/app/services/mod.service.ts +++ b/src/app/services/mod.service.ts @@ -7,6 +7,7 @@ import { IEditorMap, IItemDefinition, IModKit, + IModKitDependency, ItemSlotType, ModJSON, ModJSONKey, @@ -32,6 +33,7 @@ export function defaultModKit(): IModKit { name: 'Unnamed Mod', savedAt: 0, version: 1, + dependencies: [], _backup: undefined, }, dialogs: [], @@ -57,14 +59,33 @@ export class ModService { private localStorage = inject(LocalStorageService); private settingsService = inject(SettingsService); + public allDependencies = signal([]); + public mod = signal(defaultModKit()); public modName = computed(() => this.mod().meta.name); public modAuthor = computed(() => this.mod().meta.author); - public availableNPCs = computed(() => this.mod().npcs); - public availableItems = computed(() => this.mod().items); + public availableNPCs = computed(() => [ + ...this.mod().npcs, + ...this.activeDependencies() + .map((d) => d.npcs) + .flat(), + ]); + public availableItems = computed(() => [ + ...this.mod().items, + ...this.activeDependencies() + .map((d) => d.items) + .flat(), + ]); public availableMaps = computed(() => this.mod().maps); + public activeDependencies = computed(() => { + const myDeps = this.mod().meta.dependencies ?? []; + return this.allDependencies().filter((f) => + myDeps.map((d) => d.name).includes(f.meta.name) + ); + }); + public availableClasses = computed(() => { const settingsJson = this.mod().cores.find((f) => f.name === 'settings')?.json ?? {}; @@ -100,6 +121,8 @@ export class ModService { // mod functions public migrateMod(mod: IModKit) { const check = defaultModKit(); + mod.meta.dependencies ??= []; + Object.keys(check).forEach((checkKeyString) => { const checkKey = checkKeyString as keyof IModKit; @@ -127,6 +150,26 @@ export class ModService { this.updateMod(mod); } + public setDependencies(deps: IModKit[]): void { + this.allDependencies.set(deps); + } + + public addModDependency(dep: IModKitDependency): void { + const mod = this.mod(); + mod.meta.dependencies.push(dep); + + this.updateMod(mod); + } + + public removeModDependency(depName: string): void { + const mod = this.mod(); + mod.meta.dependencies = mod.meta.dependencies.filter( + (f) => f.name !== depName + ); + + this.updateMod(mod); + } + private presave(mod: IModKit) { mod.meta.savedAt = Date.now(); diff --git a/src/app/shared/components/input-item/input-item.component.ts b/src/app/shared/components/input-item/input-item.component.ts index 3996218..4ff4409 100644 --- a/src/app/shared/components/input-item/input-item.component.ts +++ b/src/app/shared/components/input-item/input-item.component.ts @@ -29,19 +29,31 @@ export class InputItemComponent implements OnInit { public values = computed(() => { const mod = this.modService.mod(); + const activeDependencies = this.modService.activeDependencies(); + + const myModItems = mod.items.map((i) => ({ + category: `${mod.meta.name} (Current)`, + data: i, + value: i.name, + index: 0, + })); + + const depItems = activeDependencies + .map((dep, idx) => + dep.items.map((i) => ({ + category: dep.meta.name, + data: i, + value: i.name, + index: idx + 1, + })) + ) + .flat(); return [ this.allowNone() ? { category: 'Default', data: { name: 'none' }, value: 'none' } : undefined, - ...sortBy( - mod.items.map((i) => ({ - category: 'My Mod Items', - data: i, - value: i.name, - })), - 'value' - ), + ...sortBy([...myModItems, ...depItems], ['index', 'value']), ] .flat() .filter(Boolean) as ItemModel[]; diff --git a/src/app/shared/components/input-npc/input-npc.component.ts b/src/app/shared/components/input-npc/input-npc.component.ts index 7d09869..bf835b1 100644 --- a/src/app/shared/components/input-npc/input-npc.component.ts +++ b/src/app/shared/components/input-npc/input-npc.component.ts @@ -28,17 +28,27 @@ export class InputNpcComponent implements OnInit { public values = computed(() => { const mod = this.modService.mod(); + const activeDependencies = this.modService.activeDependencies(); - return [ - ...sortBy( - mod.npcs.map((i) => ({ - category: 'My Mod NPCs', + const myModNPCs = mod.npcs.map((i) => ({ + category: `${mod.meta.name} (Current)`, + data: i, + value: i.npcId, + index: 0, + })); + + const depNPCs = activeDependencies + .map((dep, idx) => + dep.npcs.map((i) => ({ + category: dep.meta.name, data: i, value: i.npcId, - })), - 'value' - ), - ]; + index: idx + 1, + })) + ) + .flat(); + + return [...sortBy([...myModNPCs, ...depNPCs], ['index', 'value'])]; }); ngOnInit() { diff --git a/src/app/shared/components/input-quest/input-quest.component.html b/src/app/shared/components/input-quest/input-quest.component.html index 552ae43..1ccb1c0 100644 --- a/src/app/shared/components/input-quest/input-quest.component.html +++ b/src/app/shared/components/input-quest/input-quest.component.html @@ -1,6 +1,6 @@
    - +
    diff --git a/src/app/shared/components/input-quest/input-quest.component.ts b/src/app/shared/components/input-quest/input-quest.component.ts index 11d6499..fd672c1 100644 --- a/src/app/shared/components/input-quest/input-quest.component.ts +++ b/src/app/shared/components/input-quest/input-quest.component.ts @@ -7,6 +7,7 @@ import { OnInit, output, } from '@angular/core'; +import { sortBy } from 'lodash'; import { IQuest } from '../../../../interfaces'; import { ModService } from '../../../services/mod.service'; @@ -25,13 +26,29 @@ export class InputQuestComponent implements OnInit { public values = computed(() => { const mod = this.modService.mod(); + const activeDependencies = this.modService.activeDependencies(); - return [ - ...mod.quests.map((q) => ({ - value: q.name, - desc: `[${q.giver}] ${q.desc}`, - })), - ]; + const myModQuests = mod.quests.map((i) => ({ + category: `${mod.meta.name} (Current)`, + data: i, + value: i.name, + desc: `[${i.giver}] ${i.desc}`, + index: 0, + })); + + const depQuests = activeDependencies + .map((dep, idx) => + dep.quests.map((i) => ({ + category: dep.meta.name, + data: i, + value: i.name, + desc: `[${i.giver}] ${i.desc}`, + index: idx + 1, + })) + ) + .flat(); + + return [...sortBy([...myModQuests, ...depQuests], ['index', 'value'])]; }); ngOnInit() { diff --git a/src/app/shared/components/input-recipe/input-recipe.component.ts b/src/app/shared/components/input-recipe/input-recipe.component.ts index 765d769..873eb34 100644 --- a/src/app/shared/components/input-recipe/input-recipe.component.ts +++ b/src/app/shared/components/input-recipe/input-recipe.component.ts @@ -7,6 +7,7 @@ import { OnInit, output, } from '@angular/core'; +import { sortBy } from 'lodash'; import { IRecipe } from '../../../../interfaces'; import { ModService } from '../../../services/mod.service'; @@ -28,16 +29,35 @@ export class InputRecipeComponent implements OnInit { public values = computed(() => { const mod = this.modService.mod(); - const learnable = this.onlyLearnable(); + const activeDependencies = this.modService.activeDependencies(); - return [ - ...mod.recipes - .filter((i) => (learnable ? i.requireLearn : true)) - .map((i) => ({ - category: 'My Mod Recipes', + const myModRecipes = mod.recipes.map((i) => ({ + category: `${mod.meta.name} (Current)`, + data: i, + value: i.name, + index: 0, + })); + + const depRecipes = activeDependencies + .map((dep, idx) => + dep.recipes.map((i) => ({ + category: dep.meta.name, data: i, value: i.name, - })), + index: idx + 1, + })) + ) + .flat(); + + const learnable = this.onlyLearnable(); + + return [ + ...sortBy( + [...myModRecipes, ...depRecipes].filter((i) => + learnable ? i.data.requireLearn : true + ), + ['index', 'value'] + ), ]; }); diff --git a/src/app/shared/components/input-spawner/input-spawner.component.ts b/src/app/shared/components/input-spawner/input-spawner.component.ts index 617e253..d09e142 100644 --- a/src/app/shared/components/input-spawner/input-spawner.component.ts +++ b/src/app/shared/components/input-spawner/input-spawner.component.ts @@ -28,19 +28,27 @@ export class InputSpawnerComponent implements OnInit { public values = computed(() => { const mod = this.modService.mod(); + const activeDependencies = this.modService.activeDependencies(); - return [ - ...sortBy( - mod.spawners.map((i) => ({ - category: 'My Mod Spawners', + const myModSpawners = mod.spawners.map((i) => ({ + category: `${mod.meta.name} (Current)`, + data: i, + value: i.tag, + index: 0, + })); + + const depSpawners = activeDependencies + .map((dep, idx) => + dep.spawners.map((i) => ({ + category: dep.meta.name, data: i, value: i.tag, - })), - 'value' - ), - ] - .flat() - .filter(Boolean) as ItemModel[]; + index: idx + 1, + })) + ) + .flat(); + + return [...sortBy([...myModSpawners, ...depSpawners], ['index', 'value'])]; }); ngOnInit() { diff --git a/src/interfaces/modkit.ts b/src/interfaces/modkit.ts index 7d4dd7e..533a42f 100644 --- a/src/interfaces/modkit.ts +++ b/src/interfaces/modkit.ts @@ -14,6 +14,11 @@ import { ITraitTree } from './trait-tree'; export type ModJSONKey = 'bgm' | 'sfx' | 'macicons'; export type ModJSON = Record; +export interface IModKitDependency { + url: string; + name: string; +} + export interface IModKit { meta: { id: string; @@ -21,6 +26,8 @@ export interface IModKit { author: string; version: number; savedAt: number; + dependencies: IModKitDependency[]; + _url?: string; _backup: any; };