import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { LootProfile } from '../../entities/loot-profile.entity'; import { NatsService } from '../../services/nats.service'; import { CreateLootProfileDto } from './dto/create-loot-profile.dto'; import { UpdateLootProfileDto } from './dto/update-loot-profile.dto'; import { ImportLootProfileDto } from './dto/import-loot-profile.dto'; import { RUST_CONTAINERS } from './data/rust-containers'; @Injectable() export class LootService { private readonly logger = new Logger(LootService.name); constructor( @InjectRepository(LootProfile) private readonly lootRepo: Repository, private readonly natsService: NatsService, ) {} /** List profiles for a license (summaries — no JSONB) */ async getProfiles(licenseId: string) { const profiles = await this.lootRepo.find({ where: { license_id: licenseId }, select: ['id', 'profile_name', 'description', 'is_active', 'created_at', 'updated_at'], order: { created_at: 'DESC' }, }); return { profiles }; } /** Get full profile with JSONB data */ async getProfile(licenseId: string, profileId: string) { const profile = await this.lootRepo.findOne({ where: { id: profileId, license_id: licenseId }, }); if (!profile) throw new NotFoundException('Loot profile not found'); return { profile }; } /** Create a new profile */ async createProfile(licenseId: string, dto: CreateLootProfileDto) { const profile = this.lootRepo.create({ license_id: licenseId, profile_name: dto.profile_name, description: dto.description || null, loot_table: dto.loot_table || {}, loot_groups: dto.loot_groups || {}, }); const saved = await this.lootRepo.save(profile); return { profile: saved }; } /** Update an existing profile */ async updateProfile(licenseId: string, profileId: string, dto: UpdateLootProfileDto) { const profile = await this.lootRepo.findOne({ where: { id: profileId, license_id: licenseId }, }); if (!profile) throw new NotFoundException('Loot profile not found'); if (dto.profile_name !== undefined) profile.profile_name = dto.profile_name; if (dto.description !== undefined) profile.description = dto.description; if (dto.loot_table !== undefined) profile.loot_table = dto.loot_table; if (dto.loot_groups !== undefined) profile.loot_groups = dto.loot_groups; if (dto.is_active !== undefined) profile.is_active = dto.is_active; profile.updated_at = new Date(); const saved = await this.lootRepo.save(profile); return { profile: saved }; } /** Delete a profile */ async deleteProfile(licenseId: string, profileId: string) { const result = await this.lootRepo.delete({ id: profileId, license_id: licenseId }); if (result.affected === 0) throw new NotFoundException('Loot profile not found'); return { deleted: true }; } /** Duplicate a profile */ async duplicateProfile(licenseId: string, profileId: string) { const source = await this.lootRepo.findOne({ where: { id: profileId, license_id: licenseId }, }); if (!source) throw new NotFoundException('Loot profile not found'); const copy = this.lootRepo.create({ license_id: licenseId, profile_name: `${source.profile_name} (Copy)`, description: source.description, loot_table: JSON.parse(JSON.stringify(source.loot_table)), loot_groups: JSON.parse(JSON.stringify(source.loot_groups)), is_active: false, }); const saved = await this.lootRepo.save(copy); return { profile: saved }; } /** Apply profile to server with multiplier */ async applyToServer(licenseId: string, profileId: string, multiplier: number) { const profile = await this.lootRepo.findOne({ where: { id: profileId, license_id: licenseId }, }); if (!profile) throw new NotFoundException('Loot profile not found'); // Deep clone and apply multiplier const scaledTable = JSON.parse(JSON.stringify(profile.loot_table)); const scaledGroups = JSON.parse(JSON.stringify(profile.loot_groups)); if (multiplier !== 1) { this.applyMultiplierToTable(scaledTable, multiplier); this.applyMultiplierToGroups(scaledGroups, multiplier); } const lootTablesJson = JSON.stringify(scaledTable, null, 2); const lootGroupsJson = JSON.stringify(scaledGroups, null, 2); try { // Write LootTables.json via file manager NATS await this.natsService.request( `corrosion.${licenseId}.files.cmd`, { func: 'fm_save', path: 'server://oxide/data/BetterLoot/LootTables.json', content: lootTablesJson, }, 30000, ); // Write LootGroups.json via file manager NATS await this.natsService.request( `corrosion.${licenseId}.files.cmd`, { func: 'fm_save', path: 'server://oxide/data/BetterLoot/LootGroups.json', content: lootGroupsJson, }, 30000, ); // Reload BetterLoot plugin via RCON await this.natsService.publish( `corrosion.${licenseId}.cmd.server`, { action: 'command', command: 'oxide.reload BetterLoot', timestamp: new Date().toISOString(), }, ); // Mark this profile as active, deactivate others await this.lootRepo.update({ license_id: licenseId }, { is_active: false }); await this.lootRepo.update( { id: profileId, license_id: licenseId }, { is_active: true, updated_at: new Date() }, ); return { success: true, message: `Profile "${profile.profile_name}" applied with ${multiplier}x multiplier`, profile_name: profile.profile_name, multiplier, }; } catch (error) { this.logger.error(`Failed to apply loot profile: ${(error as Error).message}`); throw new HttpException( 'Failed to apply loot profile — agent may be offline', HttpStatus.SERVICE_UNAVAILABLE, ); } } /** Import BetterLoot/Looty JSON as a new profile */ async importProfile(licenseId: string, dto: ImportLootProfileDto) { const profile = this.lootRepo.create({ license_id: licenseId, profile_name: dto.profile_name, description: dto.description || 'Imported profile', loot_table: dto.loot_table, loot_groups: dto.loot_groups || {}, }); const saved = await this.lootRepo.save(profile); return { profile: saved }; } /** Export profile as BetterLoot-compatible JSON with optional multiplier */ async exportProfile(licenseId: string, profileId: string, multiplier: number) { const profile = await this.lootRepo.findOne({ where: { id: profileId, license_id: licenseId }, }); if (!profile) throw new NotFoundException('Loot profile not found'); const exportTable = JSON.parse(JSON.stringify(profile.loot_table)); const exportGroups = JSON.parse(JSON.stringify(profile.loot_groups)); if (multiplier && multiplier !== 1) { this.applyMultiplierToTable(exportTable, multiplier); this.applyMultiplierToGroups(exportGroups, multiplier); } return { profile_name: profile.profile_name, multiplier: multiplier || 1, loot_table: exportTable, loot_groups: exportGroups, }; } /** Get static list of Rust container prefabs */ getContainers() { return { containers: RUST_CONTAINERS }; } // --- Multiplier helpers --- private applyMultiplierToTable(table: Record, multiplier: number) { for (const prefab of Object.values(table)) { if (prefab?.ItemSettings) { this.scaleField(prefab.ItemSettings, 'ItemsMin', multiplier); this.scaleField(prefab.ItemSettings, 'ItemsMax', multiplier); this.scaleField(prefab.ItemSettings, 'MinScrap', multiplier); this.scaleField(prefab.ItemSettings, 'MaxScrap', multiplier); } if (prefab?.GuaranteedItems) { this.scaleItems(prefab.GuaranteedItems, multiplier); } if (prefab?.UngroupedItems) { this.scaleItems(prefab.UngroupedItems, multiplier); } } } private applyMultiplierToGroups(groups: Record, multiplier: number) { for (const group of Object.values(groups)) { if (group?.GuaranteedItems) { this.scaleItems(group.GuaranteedItems, multiplier); } if (group?.ItemList) { this.scaleItems(group.ItemList, multiplier); } } } private scaleItems(items: Record, multiplier: number) { for (const item of Object.values(items)) { this.scaleField(item, 'Min', multiplier); this.scaleField(item, 'Max', multiplier); // Recursively scale bonus items if (item?.BonusItems) { this.scaleItems(item.BonusItems, multiplier); } } } private scaleField(obj: Record, field: string, multiplier: number) { if (typeof obj[field] === 'number') { obj[field] = Math.round(obj[field] * multiplier); } } }