removed mod support for now
This commit is contained in:
parent
08edb2d80d
commit
5bb8b6aed0
@ -1,18 +0,0 @@
|
||||
# Server Configuration
|
||||
PORT=3001
|
||||
NODE_ENV=development
|
||||
|
||||
# Database Configuration
|
||||
MONGODB_URI=mongodb://localhost:27017/galaxystrikeonline
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your_jwt_secret_key_here
|
||||
|
||||
# Redis Configuration (optional)
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# Client URL
|
||||
CLIENT_URL=http://localhost:3000
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
@ -1,5 +1,4 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { calculateXPToNextLevel, getLevelFromXP } = require('../config/xp-progression');
|
||||
|
||||
const playerSchema = new mongoose.Schema({
|
||||
userId: {
|
||||
@ -24,12 +23,10 @@ const playerSchema = new mongoose.Schema({
|
||||
select: false // Don't include password in queries by default
|
||||
},
|
||||
|
||||
// Player stats
|
||||
// Player stats (simplified for API server)
|
||||
stats: {
|
||||
level: { type: Number, default: 1 },
|
||||
experience: { type: Number, default: 0 },
|
||||
totalXP: { type: Number, default: 0 }, // Total accumulated XP across all levels
|
||||
experienceToNext: { type: Number, default: function() { return calculateXPToNextLevel(1, 0); } },
|
||||
credits: { type: Number, default: 1000 },
|
||||
dungeonsCleared: { type: Number, default: 0 },
|
||||
playTime: { type: Number, default: 0 },
|
||||
@ -92,21 +89,10 @@ const playerSchema = new mongoose.Schema({
|
||||
playerSchema.index({ 'stats.level': 1 });
|
||||
playerSchema.index({ currentServer: 1 });
|
||||
|
||||
// Methods
|
||||
// Methods (simplified for API server)
|
||||
playerSchema.methods.addExperience = function(amount) {
|
||||
// Add to total accumulated XP
|
||||
this.stats.totalXP += amount;
|
||||
|
||||
// Calculate new level based on total XP
|
||||
const levelInfo = getLevelFromXP(this.stats.totalXP);
|
||||
const oldLevel = this.stats.level;
|
||||
|
||||
this.stats.level = levelInfo.level;
|
||||
this.stats.experience = levelInfo.xpIntoLevel;
|
||||
this.stats.experienceToNext = levelInfo.xpToNext;
|
||||
|
||||
// Return whether the player leveled up
|
||||
return this.stats.level > oldLevel;
|
||||
this.stats.experience += amount;
|
||||
return this.stats.experience;
|
||||
};
|
||||
|
||||
playerSchema.methods.addCredits = function(amount) {
|
||||
|
||||
@ -17,14 +17,12 @@
|
||||
"keywords": ["game", "server", "mmorpg", "api", "websocket"],
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"socket.io": "^4.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.1.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongoose": "^8.0.3",
|
||||
"redis": "^4.6.11",
|
||||
"winston": "^3.11.0",
|
||||
"joi": "^17.11.0",
|
||||
"rate-limiter-flexible": "^2.4.2",
|
||||
|
||||
@ -1,336 +0,0 @@
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Player = require('../models/Player');
|
||||
const Ship = require('../models/Ship');
|
||||
const Inventory = require('../models/Inventory');
|
||||
const { getGameSystem } = require('../systems/GameSystem');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Middleware to authenticate JWT token
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
|
||||
req.userId = decoded.userId;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
// Get player data
|
||||
router.get('/player', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const gameSystem = getGameSystem();
|
||||
const player = await gameSystem.loadPlayer(req.userId);
|
||||
|
||||
if (!player) {
|
||||
return res.status(404).json({ error: 'Player not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
userId: player.userId,
|
||||
username: player.username,
|
||||
stats: player.stats,
|
||||
attributes: player.attributes,
|
||||
info: player.info,
|
||||
settings: player.settings,
|
||||
dailyRewards: player.dailyRewards
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting player data:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get player's ships
|
||||
router.get('/ships', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const ships = await Ship.find({ userId: req.userId });
|
||||
|
||||
res.json({
|
||||
ships: ships.map(ship => ({
|
||||
id: ship.id,
|
||||
name: ship.name,
|
||||
class: ship.class,
|
||||
level: ship.level,
|
||||
stats: ship.stats,
|
||||
isEquipped: ship.isEquipped,
|
||||
isCurrent: ship.isCurrent,
|
||||
rarity: ship.rarity,
|
||||
texture: ship.texture,
|
||||
acquiredAt: ship.acquiredAt
|
||||
}))
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting player ships:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Equip a ship
|
||||
router.post('/ships/equip', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { shipId } = req.body;
|
||||
|
||||
if (!shipId) {
|
||||
return res.status(400).json({ error: 'Ship ID required' });
|
||||
}
|
||||
|
||||
const gameSystem = getGameSystem();
|
||||
const ship = await gameSystem.equipShip(req.userId, shipId);
|
||||
|
||||
res.json({
|
||||
message: 'Ship equipped successfully',
|
||||
ship: {
|
||||
id: ship.id,
|
||||
name: ship.name,
|
||||
class: ship.class,
|
||||
level: ship.level,
|
||||
stats: ship.stats,
|
||||
isEquipped: ship.isEquipped,
|
||||
isCurrent: ship.isCurrent
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error equipping ship:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get player's inventory
|
||||
router.get('/inventory', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
let inventory = await Inventory.findOne({ userId: req.userId });
|
||||
|
||||
if (!inventory) {
|
||||
// Create new inventory if it doesn't exist
|
||||
inventory = new Inventory({ userId: req.userId });
|
||||
await inventory.save();
|
||||
}
|
||||
|
||||
res.json({
|
||||
items: inventory.items,
|
||||
equippedItems: inventory.equippedItems,
|
||||
summary: inventory.getInventorySummary()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting inventory:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Add item to inventory
|
||||
router.post('/inventory/add', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { itemData } = req.body;
|
||||
|
||||
if (!itemData) {
|
||||
return res.status(400).json({ error: 'Item data required' });
|
||||
}
|
||||
|
||||
let inventory = await Inventory.findOne({ userId: req.userId });
|
||||
|
||||
if (!inventory) {
|
||||
inventory = new Inventory({ userId: req.userId });
|
||||
}
|
||||
|
||||
await inventory.addItem(itemData);
|
||||
|
||||
res.json({
|
||||
message: 'Item added to inventory',
|
||||
item: itemData,
|
||||
summary: inventory.getInventorySummary()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error adding item to inventory:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Equip item
|
||||
router.post('/inventory/equip', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { itemId, slot } = req.body;
|
||||
|
||||
if (!itemId || !slot) {
|
||||
return res.status(400).json({ error: 'Item ID and slot required' });
|
||||
}
|
||||
|
||||
const inventory = await Inventory.findOne({ userId: req.userId });
|
||||
|
||||
if (!inventory) {
|
||||
return res.status(404).json({ error: 'Inventory not found' });
|
||||
}
|
||||
|
||||
await inventory.equipItem(itemId, slot);
|
||||
|
||||
res.json({
|
||||
message: 'Item equipped successfully',
|
||||
equippedItems: inventory.equippedItems
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error equipping item:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Use consumable
|
||||
router.post('/inventory/use', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { itemId } = req.body;
|
||||
|
||||
if (!itemId) {
|
||||
return res.status(400).json({ error: 'Item ID required' });
|
||||
}
|
||||
|
||||
const inventory = await Inventory.findOne({ userId: req.userId });
|
||||
|
||||
if (!inventory) {
|
||||
return res.status(404).json({ error: 'Inventory not found' });
|
||||
}
|
||||
|
||||
const effects = await inventory.useConsumable(itemId);
|
||||
|
||||
res.json({
|
||||
message: 'Item used successfully',
|
||||
effects,
|
||||
summary: inventory.getInventorySummary()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error using consumable:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Process game action
|
||||
router.post('/action', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { actionData } = req.body;
|
||||
|
||||
if (!actionData) {
|
||||
return res.status(400).json({ error: 'Action data required' });
|
||||
}
|
||||
|
||||
const gameSystem = getGameSystem();
|
||||
const result = await gameSystem.processGameAction(req.userId, actionData);
|
||||
|
||||
res.json(result);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error processing game action:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Claim daily reward
|
||||
router.post('/daily-reward', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const player = await Player.findOne({ userId: req.userId });
|
||||
|
||||
if (!player) {
|
||||
return res.status(404).json({ error: 'Player not found' });
|
||||
}
|
||||
|
||||
const rewardResult = player.claimDailyReward();
|
||||
await player.save();
|
||||
|
||||
res.json(rewardResult);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error claiming daily reward:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Save player data
|
||||
router.post('/save', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const gameSystem = getGameSystem();
|
||||
await gameSystem.savePlayer(req.userId);
|
||||
|
||||
res.json({ message: 'Game saved successfully' });
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error saving game:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get shop items
|
||||
router.get('/shop/:category?', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const gameSystem = getGameSystem();
|
||||
const category = req.params.category;
|
||||
const items = gameSystem.economy.getShopItems(category);
|
||||
|
||||
res.json({ items });
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting shop items:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Purchase item
|
||||
router.post('/shop/purchase', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { itemId, quantity = 1 } = req.body;
|
||||
|
||||
if (!itemId) {
|
||||
return res.status(400).json({ error: 'Item ID required' });
|
||||
}
|
||||
|
||||
const gameSystem = getGameSystem();
|
||||
const purchaseInfo = gameSystem.economy.purchaseItem(req.userId, itemId, quantity);
|
||||
|
||||
const player = await Player.findOne({ userId: req.userId });
|
||||
|
||||
if (!player.canAfford(purchaseInfo.totalCost)) {
|
||||
return res.status(400).json({ error: 'Insufficient credits' });
|
||||
}
|
||||
|
||||
// Deduct credits
|
||||
player.spendCredits(purchaseInfo.totalCost);
|
||||
await player.save();
|
||||
|
||||
// Add item to inventory
|
||||
const inventory = await Inventory.findOne({ userId: req.userId });
|
||||
if (!inventory) {
|
||||
return res.status(404).json({ error: 'Inventory not found' });
|
||||
}
|
||||
|
||||
await inventory.addItem({
|
||||
...purchaseInfo.item,
|
||||
quantity
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: 'Purchase successful',
|
||||
item: purchaseInfo.item,
|
||||
quantity,
|
||||
totalCost: purchaseInfo.totalCost,
|
||||
remainingCredits: player.stats.credits
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error purchasing item:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -1,232 +0,0 @@
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const logger = require('../utils/logger');
|
||||
const ModService = require('../services/ModService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Middleware to authenticate JWT token
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret');
|
||||
req.userId = decoded.userId;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
// Get all mods
|
||||
router.get('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { enabledOnly } = req.query;
|
||||
const mods = await ModService.getMods(enabledOnly === 'true');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
mods
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting mods:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific mod
|
||||
router.get('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const mod = await ModService.getMod(parseInt(id));
|
||||
|
||||
if (!mod) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
mod
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Enable mod
|
||||
router.post('/:id/enable', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const success = await ModService.enableMod(parseInt(id));
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mod enabled successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error enabling mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Disable mod
|
||||
router.post('/:id/disable', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const success = await ModService.disableMod(parseInt(id));
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mod disabled successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error disabling mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get mod assets by type
|
||||
router.get('/assets/:type', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { type } = req.params;
|
||||
const assets = await ModService.getModAssets(type);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
assets
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting mod assets:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific mod asset
|
||||
router.get('/assets/:type/:assetId', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { type, assetId } = req.params;
|
||||
const asset = await ModService.getModAsset(type, assetId);
|
||||
|
||||
if (!asset) {
|
||||
return res.status(404).json({ error: 'Asset not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
asset
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting mod asset:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get mod load order
|
||||
router.get('/load-order', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const loadOrder = await ModService.getLoadOrder();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
loadOrder
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting load order:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Set mod load order
|
||||
router.post('/load-order', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { modIds } = req.body;
|
||||
|
||||
if (!Array.isArray(modIds)) {
|
||||
return res.status(400).json({ error: 'modIds must be an array' });
|
||||
}
|
||||
|
||||
const success = await ModService.setLoadOrder(modIds);
|
||||
|
||||
if (!success) {
|
||||
return res.status(500).json({ error: 'Failed to set load order' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Load order updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error setting load order:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get server mod preferences
|
||||
router.get('/preferences/server', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const preferences = await ModService.getServerPreferences();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
preferences
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error getting server preferences:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Set server mod preference
|
||||
router.post('/preferences/server', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { key, value } = req.body;
|
||||
|
||||
if (!key || value === undefined) {
|
||||
return res.status(400).json({ error: 'key and value are required' });
|
||||
}
|
||||
|
||||
const success = await ModService.setServerPreference(key, value);
|
||||
|
||||
if (!success) {
|
||||
return res.status(500).json({ error: 'Failed to set preference' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Preference set successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error setting server preference:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Reload mods from filesystem
|
||||
router.post('/reload', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
await ModService.reloadMods();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mods reloaded successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MODS API] Error reloading mods:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -10,7 +10,6 @@ const logger = require('./utils/logger');
|
||||
const connectDB = require('./config/database');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const serverRoutes = require('./routes/servers');
|
||||
const modsRoutes = require('./routes/mods');
|
||||
const { errorHandler, notFound } = require('./middleware/errorHandler');
|
||||
|
||||
// Override console.error to properly log error objects
|
||||
@ -67,10 +66,9 @@ app.use('/api/', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Routes - API Server Only (Auth + Server Browser + Mods)
|
||||
// Routes - API Server Only (Auth + Server Browser)
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/servers', serverRoutes);
|
||||
app.use('/api/mods', modsRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
@ -124,7 +122,7 @@ async function startServer() {
|
||||
logger.info('Database connected successfully');
|
||||
|
||||
// Start API server
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const PORT = process.env.PORT || 3000;
|
||||
server.listen(PORT, () => {
|
||||
logger.info(`API Server running on port ${PORT}`);
|
||||
logger.info('API Server handles: Authentication, Server Browser, User Data');
|
||||
|
||||
@ -1,182 +0,0 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* API Mod Service - Handles mod management through API communication with GameServer
|
||||
* This service acts as a client to the GameServer's mod functionality
|
||||
*/
|
||||
class ApiModService {
|
||||
constructor() {
|
||||
// Use service discovery - try multiple common GameServer locations
|
||||
this.gameServerUrls = [
|
||||
process.env.GAME_SERVER_URL,
|
||||
'http://localhost:3002',
|
||||
'http://127.0.0.1:3002',
|
||||
'http://game-server:3002' // Docker service name
|
||||
].filter(url => url); // Remove null/undefined values
|
||||
|
||||
this.currentUrlIndex = 0;
|
||||
}
|
||||
|
||||
async getGameServerUrl() {
|
||||
// Try each URL until we find a working one
|
||||
for (let i = 0; i < this.gameServerUrls.length; i++) {
|
||||
const url = this.gameServerUrls[i];
|
||||
try {
|
||||
const response = await fetch(`${url}/health`, {
|
||||
timeout: 5000 // 5 second timeout
|
||||
});
|
||||
if (response.ok) {
|
||||
this.currentUrlIndex = i;
|
||||
return url;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`[API MOD SERVICE] GameServer at ${url} not available:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No GameServer instances available');
|
||||
}
|
||||
|
||||
async makeRequest(endpoint, options = {}) {
|
||||
const url = await this.getGameServerUrl();
|
||||
const fullUrl = `${url}${endpoint}`;
|
||||
|
||||
const defaultOptions = {
|
||||
timeout: 10000, // 10 second timeout
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'API-ModService/1.0'
|
||||
}
|
||||
};
|
||||
|
||||
const finalOptions = { ...defaultOptions, ...options };
|
||||
|
||||
try {
|
||||
const response = await fetch(fullUrl, finalOptions);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
logger.error(`[API MOD SERVICE] Request to ${fullUrl} failed:`, error);
|
||||
|
||||
// If request failed, try next GameServer URL
|
||||
this.currentUrlIndex = (this.currentUrlIndex + 1) % this.gameServerUrls.length;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getMods(enabledOnly = false) {
|
||||
try {
|
||||
const endpoint = `/api/mods${enabledOnly ? '?enabledOnly=true' : ''}`;
|
||||
return await this.makeRequest(endpoint);
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting mods:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getMod(id) {
|
||||
try {
|
||||
return await this.makeRequest(`/api/mods/${id}`);
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting mod:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async enableMod(id) {
|
||||
try {
|
||||
return await this.makeRequest(`/api/mods/${id}/enable`, {
|
||||
method: 'POST'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error enabling mod:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async disableMod(id) {
|
||||
try {
|
||||
return await this.makeRequest(`/api/mods/${id}/disable`, {
|
||||
method: 'POST'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error disabling mod:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getModAssets(assetType) {
|
||||
try {
|
||||
return await this.makeRequest(`/api/mods/assets/${assetType}`);
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting mod assets:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getModAsset(assetType, assetId) {
|
||||
try {
|
||||
return await this.makeRequest(`/api/mods/assets/${assetType}/${assetId}`);
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting mod asset:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getLoadOrder() {
|
||||
try {
|
||||
return await this.makeRequest('/api/mods/load-order');
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting load order:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async setLoadOrder(modIds) {
|
||||
try {
|
||||
return await this.makeRequest('/api/mods/load-order', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ modIds })
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error setting load order:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getServerPreferences() {
|
||||
try {
|
||||
return await this.makeRequest('/api/mods/preferences/server');
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error getting server preferences:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async setServerPreference(key, value) {
|
||||
try {
|
||||
return await this.makeRequest('/api/mods/preferences/server', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key, value })
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error setting server preference:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async reloadMods() {
|
||||
try {
|
||||
return await this.makeRequest('/api/mods/reload', {
|
||||
method: 'POST'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[API MOD SERVICE] Error reloading mods:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ApiModService();
|
||||
17
GameServer/.env.example
Normal file
17
GameServer/.env.example
Normal file
@ -0,0 +1,17 @@
|
||||
# Game Server Configuration
|
||||
PORT=3001
|
||||
NODE_ENV=production
|
||||
|
||||
# Database Configuration
|
||||
MONGODB_URI=mongodb://localhost:27017/galaxystrikeonline
|
||||
|
||||
# Optional: API Server URL for authentication validation
|
||||
API_SERVER_URL=http://localhost:3000
|
||||
|
||||
# Optional: Server identification
|
||||
SERVER_NAME=Game Server
|
||||
SERVER_REGION=us-east
|
||||
MAX_PLAYERS=50
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
@ -1,175 +0,0 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class LocalDatabase {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.dbPath = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
// Create data directory if it doesn't exist
|
||||
const dataDir = path.join(__dirname, '../../data');
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
logger.info(`[LOCAL DB] Created data directory: ${dataDir}`);
|
||||
}
|
||||
|
||||
this.dbPath = path.join(dataDir, 'mods.db');
|
||||
|
||||
logger.info(`[LOCAL DB] Initializing database at: ${this.dbPath}`);
|
||||
|
||||
// Create database connection
|
||||
this.db = new sqlite3.Database(this.dbPath, (err) => {
|
||||
if (err) {
|
||||
logger.error('[LOCAL DB] Error opening database:', err.message);
|
||||
throw err;
|
||||
} else {
|
||||
logger.info('[LOCAL DB] Database connected successfully');
|
||||
}
|
||||
});
|
||||
|
||||
// Enable foreign keys
|
||||
await this.run('PRAGMA foreign_keys = ON');
|
||||
|
||||
// Create tables
|
||||
await this.createTables();
|
||||
|
||||
logger.info('[LOCAL DB] Database initialized successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('[LOCAL DB] Failed to initialize database:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createTables() {
|
||||
const tables = [
|
||||
// Mods table
|
||||
`CREATE TABLE IF NOT EXISTS mods (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
version TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
description TEXT,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
installed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
file_path TEXT NOT NULL,
|
||||
checksum TEXT,
|
||||
dependencies TEXT,
|
||||
config TEXT
|
||||
)`,
|
||||
|
||||
// Mod assets table
|
||||
`CREATE TABLE IF NOT EXISTS mod_assets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mod_id INTEGER NOT NULL,
|
||||
asset_type TEXT NOT NULL, -- 'ship', 'item', 'quest', etc.
|
||||
asset_id TEXT NOT NULL,
|
||||
asset_data TEXT NOT NULL, -- JSON data
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (mod_id) REFERENCES mods (id) ON DELETE CASCADE,
|
||||
UNIQUE(mod_id, asset_type, asset_id)
|
||||
)`,
|
||||
|
||||
// Server mod preferences table
|
||||
`CREATE TABLE IF NOT EXISTS server_mod_preferences (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
|
||||
// Mod load order table
|
||||
`CREATE TABLE IF NOT EXISTS mod_load_order (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mod_id INTEGER NOT NULL,
|
||||
load_order INTEGER NOT NULL,
|
||||
FOREIGN KEY (mod_id) REFERENCES mods (id) ON DELETE CASCADE,
|
||||
UNIQUE(mod_id)
|
||||
)`
|
||||
];
|
||||
|
||||
for (const table of tables) {
|
||||
await this.run(table);
|
||||
}
|
||||
|
||||
logger.info('[LOCAL DB] All tables created successfully');
|
||||
}
|
||||
|
||||
// Helper method to run SQL commands
|
||||
run(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(sql, params, function(err) {
|
||||
if (err) {
|
||||
logger.error('[LOCAL DB] SQL Error:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ id: this.lastID, changes: this.changes });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper method to get single row
|
||||
get(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get(sql, params, (err, row) => {
|
||||
if (err) {
|
||||
logger.error('[LOCAL DB] SQL Error:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper method to get multiple rows
|
||||
all(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all(sql, params, (err, rows) => {
|
||||
if (err) {
|
||||
logger.error('[LOCAL DB] SQL Error:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
close() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.db) {
|
||||
this.db.close((err) => {
|
||||
if (err) {
|
||||
logger.error('[LOCAL DB] Error closing database:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
logger.info('[LOCAL DB] Database closed');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get database instance
|
||||
getDatabase() {
|
||||
return this.db;
|
||||
}
|
||||
|
||||
// Get database path
|
||||
getDatabasePath() {
|
||||
return this.dbPath;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new LocalDatabase();
|
||||
@ -1,264 +0,0 @@
|
||||
const LocalDatabase = require('../config/LocalDatabase');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Mod Model - Handles mod data in local database
|
||||
*/
|
||||
class ModModel {
|
||||
static async create(modData) {
|
||||
const {
|
||||
name,
|
||||
version,
|
||||
author,
|
||||
description,
|
||||
filePath,
|
||||
checksum,
|
||||
dependencies,
|
||||
config
|
||||
} = modData;
|
||||
|
||||
const sql = `
|
||||
INSERT INTO mods (name, version, author, description, file_path, checksum, dependencies, config)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const result = await LocalDatabase.run(sql, [
|
||||
name,
|
||||
version,
|
||||
author,
|
||||
description || null,
|
||||
filePath,
|
||||
checksum || null,
|
||||
dependencies ? JSON.stringify(dependencies) : null,
|
||||
config ? JSON.stringify(config) : null
|
||||
]);
|
||||
|
||||
return await this.findById(result.id);
|
||||
}
|
||||
|
||||
static async findById(id) {
|
||||
const sql = 'SELECT * FROM mods WHERE id = ?';
|
||||
const mod = await LocalDatabase.get(sql, [id]);
|
||||
return this.parseMod(mod);
|
||||
}
|
||||
|
||||
static async findByName(name) {
|
||||
const sql = 'SELECT * FROM mods WHERE name = ?';
|
||||
const mod = await LocalDatabase.get(sql, [name]);
|
||||
return this.parseMod(mod);
|
||||
}
|
||||
|
||||
static async findAll(enabledOnly = false) {
|
||||
const sql = enabledOnly
|
||||
? 'SELECT * FROM mods WHERE enabled = 1 ORDER BY name'
|
||||
: 'SELECT * FROM mods ORDER BY name';
|
||||
|
||||
const mods = await LocalDatabase.all(sql);
|
||||
return mods.map(mod => this.parseMod(mod));
|
||||
}
|
||||
|
||||
static async update(id, updates) {
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
if (key === 'dependencies' || key === 'config') {
|
||||
fields.push(`${key} = ?`);
|
||||
values.push(JSON.stringify(updates[key]));
|
||||
} else {
|
||||
fields.push(`${key} = ?`);
|
||||
values.push(updates[key]);
|
||||
}
|
||||
});
|
||||
|
||||
if (fields.length === 0) return false;
|
||||
|
||||
fields.push('updated_at = CURRENT_TIMESTAMP');
|
||||
values.push(id);
|
||||
|
||||
const sql = `UPDATE mods SET ${fields.join(', ')} WHERE id = ?`;
|
||||
const result = await LocalDatabase.run(sql, values);
|
||||
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static async delete(id) {
|
||||
const sql = 'DELETE FROM mods WHERE id = ?';
|
||||
const result = await LocalDatabase.run(sql, [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static async enable(id) {
|
||||
return await this.update(id, { enabled: 1 });
|
||||
}
|
||||
|
||||
static async disable(id) {
|
||||
return await this.update(id, { enabled: 0 });
|
||||
}
|
||||
|
||||
static async getLoadOrder() {
|
||||
const sql = `
|
||||
SELECT m.*, ml.load_order
|
||||
FROM mods m
|
||||
LEFT JOIN mod_load_order ml ON m.id = ml.mod_id
|
||||
WHERE m.enabled = 1
|
||||
ORDER BY ml.load_order ASC, m.name ASC
|
||||
`;
|
||||
|
||||
const mods = await LocalDatabase.all(sql);
|
||||
return mods.map(mod => this.parseMod(mod));
|
||||
}
|
||||
|
||||
static async setLoadOrder(modIds) {
|
||||
await LocalDatabase.run('DELETE FROM mod_load_order');
|
||||
|
||||
for (let i = 0; i < modIds.length; i++) {
|
||||
await LocalDatabase.run(
|
||||
'INSERT INTO mod_load_order (mod_id, load_order) VALUES (?, ?)',
|
||||
[modIds[i], i + 1]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static parseMod(mod) {
|
||||
if (!mod) return null;
|
||||
|
||||
return {
|
||||
...mod,
|
||||
enabled: Boolean(mod.enabled),
|
||||
dependencies: mod.dependencies ? JSON.parse(mod.dependencies) : null,
|
||||
config: mod.config ? JSON.parse(mod.config) : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ModAsset Model - Handles mod assets in local database
|
||||
*/
|
||||
class ModAssetModel {
|
||||
static async create(assetData) {
|
||||
const { modId, assetType, assetId, assetData: data } = assetData;
|
||||
|
||||
const sql = `
|
||||
INSERT OR REPLACE INTO mod_assets (mod_id, asset_type, asset_id, asset_data)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const result = await LocalDatabase.run(sql, [
|
||||
modId,
|
||||
assetType,
|
||||
assetId,
|
||||
JSON.stringify(data)
|
||||
]);
|
||||
|
||||
return await this.findById(result.id);
|
||||
}
|
||||
|
||||
static async findById(id) {
|
||||
const sql = 'SELECT * FROM mod_assets WHERE id = ?';
|
||||
const asset = await LocalDatabase.get(sql, [id]);
|
||||
return this.parseAsset(asset);
|
||||
}
|
||||
|
||||
static async findByModId(modId) {
|
||||
const sql = 'SELECT * FROM mod_assets WHERE mod_id = ?';
|
||||
const assets = await LocalDatabase.all(sql, [modId]);
|
||||
return assets.map(asset => this.parseAsset(asset));
|
||||
}
|
||||
|
||||
static async findByType(assetType) {
|
||||
const sql = `
|
||||
SELECT ma.*, m.name as mod_name, m.enabled as mod_enabled
|
||||
FROM mod_assets ma
|
||||
JOIN mods m ON ma.mod_id = m.id
|
||||
WHERE ma.asset_type = ? AND m.enabled = 1
|
||||
ORDER BY m.name, ma.asset_id
|
||||
`;
|
||||
|
||||
const assets = await LocalDatabase.all(sql, [assetType]);
|
||||
return assets.map(asset => this.parseAsset(asset));
|
||||
}
|
||||
|
||||
static async findByTypeAndId(assetType, assetId) {
|
||||
const sql = `
|
||||
SELECT ma.*, m.name as mod_name, m.enabled as mod_enabled
|
||||
FROM mod_assets ma
|
||||
JOIN mods m ON ma.mod_id = m.id
|
||||
WHERE ma.asset_type = ? AND ma.asset_id = ? AND m.enabled = 1
|
||||
`;
|
||||
|
||||
const asset = await LocalDatabase.get(sql, [assetType, assetId]);
|
||||
return this.parseAsset(asset);
|
||||
}
|
||||
|
||||
static async delete(id) {
|
||||
const sql = 'DELETE FROM mod_assets WHERE id = ?';
|
||||
const result = await LocalDatabase.run(sql, [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static async deleteByModId(modId) {
|
||||
const sql = 'DELETE FROM mod_assets WHERE mod_id = ?';
|
||||
const result = await LocalDatabase.run(sql, [modId]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static parseAsset(asset) {
|
||||
if (!asset) return null;
|
||||
|
||||
return {
|
||||
...asset,
|
||||
assetData: JSON.parse(asset.asset_data),
|
||||
modEnabled: Boolean(asset.mod_enabled)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ServerModPreferences Model - Handles server mod preferences
|
||||
*/
|
||||
class ServerModPreferencesModel {
|
||||
static async set(key, value) {
|
||||
const sql = `
|
||||
INSERT OR REPLACE INTO server_mod_preferences (key, value, updated_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
`;
|
||||
|
||||
const result = await LocalDatabase.run(sql, [key, JSON.stringify(value)]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static async get(key) {
|
||||
const sql = 'SELECT * FROM server_mod_preferences WHERE key = ?';
|
||||
const pref = await LocalDatabase.get(sql, [key]);
|
||||
|
||||
if (!pref) return null;
|
||||
return JSON.parse(pref.value);
|
||||
}
|
||||
|
||||
static async getAll() {
|
||||
const sql = 'SELECT * FROM server_mod_preferences';
|
||||
const prefs = await LocalDatabase.all(sql);
|
||||
|
||||
const result = {};
|
||||
prefs.forEach(pref => {
|
||||
result[pref.key] = JSON.parse(pref.value);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static async delete(key) {
|
||||
const sql = 'DELETE FROM server_mod_preferences WHERE key = ?';
|
||||
const result = await LocalDatabase.run(sql, [key]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ModModel,
|
||||
ModAssetModel,
|
||||
ServerModPreferencesModel
|
||||
};
|
||||
@ -1,214 +0,0 @@
|
||||
const express = require('express');
|
||||
const logger = require('../utils/logger');
|
||||
const ModService = require('../services/ModService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get all mods
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { enabledOnly } = req.query;
|
||||
const mods = await ModService.getMods(enabledOnly === 'true');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
mods
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting mods:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific mod
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const mod = await ModService.getMod(parseInt(id));
|
||||
|
||||
if (!mod) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
mod
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Enable mod
|
||||
router.post('/:id/enable', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const success = await ModService.enableMod(parseInt(id));
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mod enabled successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error enabling mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Disable mod
|
||||
router.post('/:id/disable', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const success = await ModService.disableMod(parseInt(id));
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: 'Mod not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mod disabled successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error disabling mod:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get mod assets by type
|
||||
router.get('/assets/:type', async (req, res) => {
|
||||
try {
|
||||
const { type } = req.params;
|
||||
const assets = await ModService.getModAssets(type);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
assets
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting mod assets:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific mod asset
|
||||
router.get('/assets/:type/:assetId', async (req, res) => {
|
||||
try {
|
||||
const { type, assetId } = req.params;
|
||||
const asset = await ModService.getModAsset(type, assetId);
|
||||
|
||||
if (!asset) {
|
||||
return res.status(404).json({ error: 'Asset not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
asset
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting mod asset:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get mod load order
|
||||
router.get('/load-order', async (req, res) => {
|
||||
try {
|
||||
const loadOrder = await ModService.getLoadOrder();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
loadOrder
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting load order:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Set mod load order
|
||||
router.post('/load-order', async (req, res) => {
|
||||
try {
|
||||
const { modIds } = req.body;
|
||||
|
||||
if (!Array.isArray(modIds)) {
|
||||
return res.status(400).json({ error: 'modIds must be an array' });
|
||||
}
|
||||
|
||||
const success = await ModService.setLoadOrder(modIds);
|
||||
|
||||
if (!success) {
|
||||
return res.status(500).json({ error: 'Failed to set load order' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Load order updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error setting load order:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get server mod preferences
|
||||
router.get('/preferences/server', async (req, res) => {
|
||||
try {
|
||||
const preferences = await ModService.getServerPreferences();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
preferences
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error getting server preferences:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Set server mod preference
|
||||
router.post('/preferences/server', async (req, res) => {
|
||||
try {
|
||||
const { key, value } = req.body;
|
||||
|
||||
if (!key || value === undefined) {
|
||||
return res.status(400).json({ error: 'key and value are required' });
|
||||
}
|
||||
|
||||
const success = await ModService.setServerPreference(key, value);
|
||||
|
||||
if (!success) {
|
||||
return res.status(500).json({ error: 'Failed to set preference' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Preference set successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error setting server preference:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Reload mods from filesystem
|
||||
router.post('/reload', async (req, res) => {
|
||||
try {
|
||||
await ModService.reloadMods();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mods reloaded successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GAME SERVER MODS API] Error reloading mods:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -1,202 +1,273 @@
|
||||
/**
|
||||
* Game Server - Real-time Multiplayer
|
||||
* Handles actual game instances, player connections, and real-time gameplay
|
||||
* Game Server - Based on Client LocalServer Infrastructure
|
||||
* Handles real-time multiplayer game instances and gameplay
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const dotenv = require('dotenv');
|
||||
require('dotenv').config();
|
||||
|
||||
const logger = require('./utils/logger');
|
||||
const connectDB = require('./config/database');
|
||||
const { initializeGameSystems } = require('./systems/GameSystem');
|
||||
const SocketHandlers = require('./socket/socketHandlers');
|
||||
const ServerRegistrationService = require('./services/ServerRegistrationService');
|
||||
const ModService = require('./services/ModService');
|
||||
const modRoutes = require('./routes/mods');
|
||||
const { errorHandler, notFound } = require('./middleware/errorHandler');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = socketIo(server, {
|
||||
cors: {
|
||||
origin: ["https://galaxystrike.online", "https://api.korvarix.com", "http://localhost:3000"],
|
||||
origin: ["http://localhost:3000", "http://127.0.0.1:3000", "file://"],
|
||||
methods: ["GET", "POST"],
|
||||
credentials: true
|
||||
}
|
||||
});
|
||||
|
||||
// Game state
|
||||
const gameInstances = new Map();
|
||||
const connectedClients = new Map();
|
||||
|
||||
// Middleware
|
||||
app.use(helmet());
|
||||
app.use(compression());
|
||||
app.use(cors({
|
||||
origin: ["https://galaxystrike.online", "https://api.korvarix.com", "http://localhost:3000"],
|
||||
origin: ["http://localhost:3000", "http://127.0.0.1:3000", "file://"],
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Game Server Routes (minimal - mostly for health checks and server management)
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
status: 'Game Server OK',
|
||||
service: 'galaxystrikeonline-game',
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
activeServers: Object.keys(gameServers).length,
|
||||
connectedPlayers: connectedPlayers.size
|
||||
mode: 'game-server',
|
||||
activeInstances: gameInstances.size,
|
||||
connectedClients: connectedClients.size
|
||||
});
|
||||
});
|
||||
|
||||
// Get server status
|
||||
app.get('/api/game/status', (req, res) => {
|
||||
res.json({
|
||||
activeServers: Object.keys(gameServers).length,
|
||||
connectedPlayers: connectedPlayers.size,
|
||||
timestamp: new Date().toISOString()
|
||||
// API version endpoint
|
||||
app.get('/api/ssc/version', (req, res) => {
|
||||
res.status(200).json({
|
||||
version: '1.0.0',
|
||||
service: 'galaxystrikeonline-game-server',
|
||||
timestamp: new Date().toISOString(),
|
||||
mode: 'multiplayer'
|
||||
});
|
||||
});
|
||||
|
||||
// Mod management routes
|
||||
app.use('/api/mods', modRoutes);
|
||||
|
||||
// Error handling
|
||||
app.use(notFound);
|
||||
app.use(errorHandler);
|
||||
|
||||
// Global game server instances
|
||||
const gameServers = {};
|
||||
let serverRegistration; // Global reference to registration service
|
||||
|
||||
// Player tracking
|
||||
const connectedPlayers = new Set(); // Track actual player connections
|
||||
let socketHandlers;
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
logger.info(`Game Server: Player connected - ${socket.id}`);
|
||||
socketHandlers.handleConnection(socket);
|
||||
});
|
||||
|
||||
// Handle uncaught errors
|
||||
process.on('uncaughtException', (error) => {
|
||||
logger.error('Uncaught Exception:', error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
// Graceful shutdown handlers
|
||||
async function gracefulShutdown(signal) {
|
||||
logger.info(`[GRACEFUL SHUTDOWN] Received ${signal}, shutting down gracefully...`);
|
||||
// Game data endpoints (similar to LocalServer)
|
||||
app.post('/api/game/player/:id/save', (req, res) => {
|
||||
const playerId = req.params.id;
|
||||
const playerData = req.body;
|
||||
|
||||
try {
|
||||
// Stop accepting new connections
|
||||
server.close(async () => {
|
||||
logger.info('[GRACEFUL SHUTDOWN] HTTP server closed');
|
||||
|
||||
// Unregister from API
|
||||
if (serverRegistration) {
|
||||
await serverRegistration.stopHeartbeat();
|
||||
const unregistered = await serverRegistration.unregisterWithAPI();
|
||||
if (unregistered) {
|
||||
logger.info('[GRACEFUL SHUTDOWN] Server unregistered from API successfully');
|
||||
} else {
|
||||
logger.warn('[GRACEFUL SHUTDOWN] Failed to unregister from API');
|
||||
}
|
||||
// Store player data in game instance
|
||||
if (connectedClients.has(playerId)) {
|
||||
connectedClients.get(playerId).playerData = playerData;
|
||||
|
||||
// Broadcast to other players in same instance
|
||||
const instanceId = connectedClients.get(playerId).instanceId;
|
||||
if (gameInstances.has(instanceId)) {
|
||||
io.to(instanceId).emit('playerDataUpdated', {
|
||||
playerId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Player data saved to game server'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/game/player/:id', (req, res) => {
|
||||
const playerId = req.params.id;
|
||||
|
||||
if (connectedClients.has(playerId)) {
|
||||
const playerData = connectedClients.get(playerId).playerData;
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
player: playerData
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Player not connected to game server'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Socket.IO handlers (based on LocalServer)
|
||||
io.on('connection', (socket) => {
|
||||
console.log('[GAME SERVER] Client connected:', socket.id);
|
||||
connectedClients.set(socket.id, {
|
||||
connectedAt: Date.now(),
|
||||
playerData: null,
|
||||
instanceId: null
|
||||
});
|
||||
|
||||
// Authentication (similar to LocalServer)
|
||||
socket.on('authenticate', (data) => {
|
||||
console.log('[GAME SERVER] Authenticating client:', socket.id, data);
|
||||
|
||||
// In production, validate with API server
|
||||
socket.emit('authenticated', {
|
||||
success: true,
|
||||
user: {
|
||||
id: data.userId || socket.id,
|
||||
username: data.username || 'Game Player',
|
||||
token: 'game-token-' + Date.now()
|
||||
}
|
||||
|
||||
// Shutdown mod service
|
||||
await ModService.shutdown();
|
||||
|
||||
// Close database connections
|
||||
const mongoose = require('mongoose');
|
||||
await mongoose.connection.close();
|
||||
logger.info('[GRACEFUL SHUTDOWN] Database connections closed');
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Game data events (similar to LocalServer)
|
||||
socket.on('saveGameData', (data) => {
|
||||
console.log('[GAME SERVER] Saving game data for:', socket.id);
|
||||
|
||||
if (connectedClients.has(socket.id)) {
|
||||
connectedClients.get(socket.id).playerData = data;
|
||||
}
|
||||
|
||||
socket.emit('gameDataSaved', {
|
||||
success: true,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('loadGameData', (data) => {
|
||||
console.log('[GAME SERVER] Loading game data for:', socket.id);
|
||||
|
||||
const playerData = connectedClients.get(socket.id)?.playerData || {};
|
||||
|
||||
socket.emit('gameDataLoaded', {
|
||||
success: true,
|
||||
data: playerData
|
||||
});
|
||||
});
|
||||
|
||||
// Game-specific events
|
||||
socket.on('joinGameInstance', (data) => {
|
||||
const { instanceId } = data;
|
||||
|
||||
if (!gameInstances.has(instanceId)) {
|
||||
gameInstances.set(instanceId, {
|
||||
id: instanceId,
|
||||
players: new Set(),
|
||||
createdAt: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
const instance = gameInstances.get(instanceId);
|
||||
instance.players.add(socket.id);
|
||||
|
||||
connectedClients.get(socket.id).instanceId = instanceId;
|
||||
socket.join(instanceId);
|
||||
|
||||
socket.emit('joinedGameInstance', {
|
||||
instanceId,
|
||||
playerCount: instance.players.size
|
||||
});
|
||||
|
||||
// Force shutdown after 30 seconds
|
||||
setTimeout(() => {
|
||||
logger.error('[GRACEFUL SHUTDOWN] Forced shutdown after timeout');
|
||||
process.exit(1);
|
||||
}, 30000);
|
||||
// Notify other players
|
||||
socket.to(instanceId).emit('playerJoinedInstance', {
|
||||
playerId: socket.id,
|
||||
playerCount: instance.players.size
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('leaveGameInstance', (data) => {
|
||||
const clientData = connectedClients.get(socket.id);
|
||||
if (clientData && clientData.instanceId) {
|
||||
const instance = gameInstances.get(clientData.instanceId);
|
||||
if (instance) {
|
||||
instance.players.delete(socket.id);
|
||||
socket.leave(clientData.instanceId);
|
||||
|
||||
// Clean up empty instances
|
||||
if (instance.players.size === 0) {
|
||||
gameInstances.delete(clientData.instanceId);
|
||||
}
|
||||
|
||||
// Notify other players
|
||||
socket.to(clientData.instanceId).emit('playerLeftInstance', {
|
||||
playerId: socket.id,
|
||||
playerCount: instance.players.size
|
||||
});
|
||||
}
|
||||
|
||||
clientData.instanceId = null;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('gameAction', (data) => {
|
||||
const clientData = connectedClients.get(socket.id);
|
||||
if (clientData && clientData.instanceId) {
|
||||
// Broadcast game action to other players in same instance
|
||||
socket.to(clientData.instanceId).emit('gameAction', {
|
||||
playerId: socket.id,
|
||||
action: data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('[GAME SERVER] Client disconnected:', socket.id);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GRACEFUL SHUTDOWN] Error during shutdown:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||
|
||||
// Handle Socket.IO adapter errors
|
||||
io.engine.on('connection_error', (err) => {
|
||||
logger.error('Socket.IO connection error:', err);
|
||||
const clientData = connectedClients.get(socket.id);
|
||||
if (clientData && clientData.instanceId) {
|
||||
const instance = gameInstances.get(clientData.instanceId);
|
||||
if (instance) {
|
||||
instance.players.delete(socket.id);
|
||||
|
||||
// Clean up empty instances
|
||||
if (instance.players.size === 0) {
|
||||
gameInstances.delete(clientData.instanceId);
|
||||
}
|
||||
|
||||
// Notify other players
|
||||
socket.to(clientData.instanceId).emit('playerLeftInstance', {
|
||||
playerId: socket.id,
|
||||
playerCount: instance.players.size
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
connectedClients.delete(socket.id);
|
||||
});
|
||||
|
||||
// Welcome message (similar to LocalServer)
|
||||
socket.emit('welcome', {
|
||||
message: 'Connected to game server',
|
||||
serverInfo: {
|
||||
mode: 'multiplayer',
|
||||
activeInstances: gameInstances.size,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
io.of('/').adapter.on('error', (err) => {
|
||||
logger.error('Socket.IO adapter error:', err);
|
||||
});
|
||||
// Start server
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// Initialize database and game systems
|
||||
async function startGameServer() {
|
||||
async function startServer() {
|
||||
try {
|
||||
// Connect to database
|
||||
await connectDB();
|
||||
logger.info('Game Server: Database connected successfully');
|
||||
|
||||
// Initialize mod service
|
||||
await ModService.initialize();
|
||||
logger.info('Game Server: Mod service initialized');
|
||||
|
||||
// Initialize game systems
|
||||
await initializeGameSystems();
|
||||
logger.info('Game Server: Game systems initialized');
|
||||
|
||||
// Initialize server registration service
|
||||
const gameServerUrl = `https://api.korvarix.com:${process.env.GAME_PORT || 3002}`;
|
||||
const apiUrl = process.env.API_SERVER_URL || 'https://api.korvarix.com';
|
||||
const serverName = process.env.SERVER_NAME || 'Game Server';
|
||||
const serverRegion = process.env.SERVER_REGION || 'us-east';
|
||||
const maxPlayers = parseInt(process.env.MAX_PLAYERS) || 10;
|
||||
|
||||
serverRegistration = new ServerRegistrationService(gameServerUrl, apiUrl, serverName, serverRegion, maxPlayers);
|
||||
|
||||
// Set up player count callback
|
||||
serverRegistration.setPlayerCountCallback(() => connectedPlayers.size);
|
||||
|
||||
serverRegistration.startHeartbeat();
|
||||
|
||||
// Initialize socket handlers
|
||||
socketHandlers = new SocketHandlers(io, gameServers, connectedPlayers);
|
||||
|
||||
// Make registration service available to socket handlers
|
||||
socketHandlers.serverRegistration = serverRegistration;
|
||||
|
||||
// Start server
|
||||
const PORT = process.env.GAME_PORT || 3002; // Game Server on port 3002
|
||||
server.listen(PORT, () => {
|
||||
logger.info(`Game Server running on port ${PORT}`);
|
||||
logger.info('Game Server handles: Real-time Multiplayer, Game Instances, Socket.IO');
|
||||
logger.info(`Game Server Name: ${serverName}`);
|
||||
logger.info(`Game Server Region: ${serverRegion}`);
|
||||
logger.info(`Game Server URL: ${gameServerUrl}`);
|
||||
logger.info(`API Server URL: ${apiUrl}`);
|
||||
console.log(`[GAME SERVER] Game server running on port ${PORT}`);
|
||||
console.log(`[GAME SERVER] Socket.IO ready for multiplayer connections`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to start Game Server:', error);
|
||||
console.error('[GAME SERVER] Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startGameServer();
|
||||
startServer();
|
||||
|
||||
module.exports = { app, server, io, gameServers };
|
||||
module.exports = { app, server, io, gameInstances, connectedClients };
|
||||
|
||||
@ -1,264 +0,0 @@
|
||||
const { ModModel, ModAssetModel, ServerModPreferencesModel } = require('../models/ModModels');
|
||||
const LocalDatabase = require('../config/LocalDatabase');
|
||||
const logger = require('../utils/logger');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
class ModService {
|
||||
constructor() {
|
||||
this.modsDirectory = path.join(__dirname, '../../mods');
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
// Initialize local database
|
||||
await LocalDatabase.initialize();
|
||||
|
||||
// Create mods directory if it doesn't exist
|
||||
if (!fs.existsSync(this.modsDirectory)) {
|
||||
fs.mkdirSync(this.modsDirectory, { recursive: true });
|
||||
logger.info(`[MOD SERVICE] Created mods directory: ${this.modsDirectory}`);
|
||||
}
|
||||
|
||||
// Load existing mods from filesystem
|
||||
await this.loadModsFromFilesystem();
|
||||
|
||||
this.initialized = true;
|
||||
logger.info('[MOD SERVICE] Mod service initialized successfully');
|
||||
} catch (error) {
|
||||
logger.error('[MOD SERVICE] Failed to initialize mod service:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadModsFromFilesystem() {
|
||||
try {
|
||||
if (!fs.existsSync(this.modsDirectory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modFolders = fs.readdirSync(this.modsDirectory, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
|
||||
for (const modFolder of modFolders) {
|
||||
await this.loadModFromFolder(modFolder);
|
||||
}
|
||||
|
||||
logger.info(`[MOD SERVICE] Loaded ${modFolders.length} mods from filesystem`);
|
||||
} catch (error) {
|
||||
logger.error('[MOD SERVICE] Error loading mods from filesystem:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadModFromFolder(modFolder) {
|
||||
try {
|
||||
const modPath = path.join(this.modsDirectory, modFolder);
|
||||
const manifestPath = path.join(modPath, 'mod.json');
|
||||
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
logger.warn(`[MOD SERVICE] Mod ${modFolder} missing mod.json manifest`);
|
||||
return;
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
|
||||
// Validate required fields
|
||||
if (!manifest.name || !manifest.version || !manifest.author) {
|
||||
logger.warn(`[MOD SERVICE] Mod ${modFolder} has invalid manifest`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if mod already exists in database
|
||||
const existingMod = await ModModel.findByName(manifest.name);
|
||||
|
||||
// Calculate checksum of mod files
|
||||
const checksum = await this.calculateModChecksum(modPath);
|
||||
|
||||
const modData = {
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
author: manifest.author,
|
||||
description: manifest.description || '',
|
||||
filePath: modPath,
|
||||
checksum,
|
||||
dependencies: manifest.dependencies || [],
|
||||
config: manifest.config || {}
|
||||
};
|
||||
|
||||
if (existingMod) {
|
||||
// Update existing mod if checksum changed
|
||||
if (existingMod.checksum !== checksum) {
|
||||
await ModModel.update(existingMod.id, modData);
|
||||
logger.info(`[MOD SERVICE] Updated mod: ${manifest.name}`);
|
||||
}
|
||||
} else {
|
||||
// Create new mod
|
||||
await ModModel.create(modData);
|
||||
logger.info(`[MOD SERVICE] Installed new mod: ${manifest.name}`);
|
||||
}
|
||||
|
||||
// Load mod assets
|
||||
await this.loadModAssets(manifest.name, modPath);
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[MOD SERVICE] Error loading mod ${modFolder}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadModAssets(modName, modPath) {
|
||||
try {
|
||||
const mod = await ModModel.findByName(modName);
|
||||
if (!mod) return;
|
||||
|
||||
// Clear existing assets for this mod
|
||||
await ModAssetModel.deleteByModId(mod.id);
|
||||
|
||||
// Load assets from assets folder
|
||||
const assetsPath = path.join(modPath, 'assets');
|
||||
if (fs.existsSync(assetsPath)) {
|
||||
await this.loadAssetsFromDirectory(mod.id, assetsPath);
|
||||
}
|
||||
|
||||
logger.info(`[MOD SERVICE] Loaded assets for mod: ${modName}`);
|
||||
} catch (error) {
|
||||
logger.error(`[MOD SERVICE] Error loading assets for mod ${modName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadAssetsFromDirectory(modId, assetsPath) {
|
||||
const assetTypes = ['ships', 'items', 'quests', 'systems'];
|
||||
|
||||
for (const assetType of assetTypes) {
|
||||
const assetTypePath = path.join(assetsPath, assetType);
|
||||
|
||||
if (fs.existsSync(assetTypePath)) {
|
||||
const assetFiles = fs.readdirSync(assetTypePath);
|
||||
|
||||
for (const assetFile of assetFiles) {
|
||||
if (assetFile.endsWith('.json')) {
|
||||
try {
|
||||
const assetData = JSON.parse(
|
||||
fs.readFileSync(path.join(assetTypePath, assetFile), 'utf8')
|
||||
);
|
||||
|
||||
const assetId = path.basename(assetFile, '.json');
|
||||
|
||||
await ModAssetModel.create({
|
||||
modId,
|
||||
assetType: assetType.slice(0, -1), // Remove 's' for singular
|
||||
assetId,
|
||||
assetData
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[MOD SERVICE] Error loading asset ${assetFile}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async calculateModChecksum(modPath) {
|
||||
try {
|
||||
const hash = crypto.createHash('sha256');
|
||||
|
||||
// Hash all files in mod directory
|
||||
const hashDirectory = (dir) => {
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
hashDirectory(filePath);
|
||||
} else {
|
||||
const data = fs.readFileSync(filePath);
|
||||
hash.update(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
hashDirectory(modPath);
|
||||
return hash.digest('hex');
|
||||
} catch (error) {
|
||||
logger.error(`[MOD SERVICE] Error calculating checksum for ${modPath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Public API methods
|
||||
async getMods(enabledOnly = false) {
|
||||
return await ModModel.findAll(enabledOnly);
|
||||
}
|
||||
|
||||
async getMod(id) {
|
||||
return await ModModel.findById(id);
|
||||
}
|
||||
|
||||
async getModByName(name) {
|
||||
return await ModModel.findByName(name);
|
||||
}
|
||||
|
||||
async enableMod(id) {
|
||||
const result = await ModModel.enable(id);
|
||||
if (result) {
|
||||
logger.info(`[MOD SERVICE] Enabled mod: ${id}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async disableMod(id) {
|
||||
const result = await ModModel.disable(id);
|
||||
if (result) {
|
||||
logger.info(`[MOD SERVICE] Disabled mod: ${id}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async getModAssets(assetType) {
|
||||
return await ModAssetModel.findByType(assetType);
|
||||
}
|
||||
|
||||
async getModAsset(assetType, assetId) {
|
||||
return await ModAssetModel.findByTypeAndId(assetType, assetId);
|
||||
}
|
||||
|
||||
async getLoadOrder() {
|
||||
return await ModModel.getLoadOrder();
|
||||
}
|
||||
|
||||
async setLoadOrder(modIds) {
|
||||
const result = await ModModel.setLoadOrder(modIds);
|
||||
if (result) {
|
||||
logger.info(`[MOD SERVICE] Updated mod load order`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async getServerPreferences() {
|
||||
return await ServerModPreferencesModel.getAll();
|
||||
}
|
||||
|
||||
async setServerPreference(key, value) {
|
||||
return await ServerModPreferencesModel.set(key, value);
|
||||
}
|
||||
|
||||
async reloadMods() {
|
||||
logger.info('[MOD SERVICE] Reloading mods from filesystem...');
|
||||
await this.loadModsFromFilesystem();
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
if (this.initialized) {
|
||||
await LocalDatabase.close();
|
||||
this.initialized = false;
|
||||
logger.info('[MOD SERVICE] Mod service shut down');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ModService();
|
||||
Loading…
Reference in New Issue
Block a user