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 mongoose = require('mongoose');
|
||||||
const { calculateXPToNextLevel, getLevelFromXP } = require('../config/xp-progression');
|
|
||||||
|
|
||||||
const playerSchema = new mongoose.Schema({
|
const playerSchema = new mongoose.Schema({
|
||||||
userId: {
|
userId: {
|
||||||
@ -24,12 +23,10 @@ const playerSchema = new mongoose.Schema({
|
|||||||
select: false // Don't include password in queries by default
|
select: false // Don't include password in queries by default
|
||||||
},
|
},
|
||||||
|
|
||||||
// Player stats
|
// Player stats (simplified for API server)
|
||||||
stats: {
|
stats: {
|
||||||
level: { type: Number, default: 1 },
|
level: { type: Number, default: 1 },
|
||||||
experience: { type: Number, default: 0 },
|
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 },
|
credits: { type: Number, default: 1000 },
|
||||||
dungeonsCleared: { type: Number, default: 0 },
|
dungeonsCleared: { type: Number, default: 0 },
|
||||||
playTime: { 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({ 'stats.level': 1 });
|
||||||
playerSchema.index({ currentServer: 1 });
|
playerSchema.index({ currentServer: 1 });
|
||||||
|
|
||||||
// Methods
|
// Methods (simplified for API server)
|
||||||
playerSchema.methods.addExperience = function(amount) {
|
playerSchema.methods.addExperience = function(amount) {
|
||||||
// Add to total accumulated XP
|
this.stats.experience += amount;
|
||||||
this.stats.totalXP += amount;
|
return this.stats.experience;
|
||||||
|
|
||||||
// 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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
playerSchema.methods.addCredits = function(amount) {
|
playerSchema.methods.addCredits = function(amount) {
|
||||||
|
|||||||
@ -17,14 +17,12 @@
|
|||||||
"keywords": ["game", "server", "mmorpg", "api", "websocket"],
|
"keywords": ["game", "server", "mmorpg", "api", "websocket"],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"socket.io": "^4.7.4",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mongoose": "^8.0.3",
|
"mongoose": "^8.0.3",
|
||||||
"redis": "^4.6.11",
|
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"joi": "^17.11.0",
|
"joi": "^17.11.0",
|
||||||
"rate-limiter-flexible": "^2.4.2",
|
"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 connectDB = require('./config/database');
|
||||||
const authRoutes = require('./routes/auth');
|
const authRoutes = require('./routes/auth');
|
||||||
const serverRoutes = require('./routes/servers');
|
const serverRoutes = require('./routes/servers');
|
||||||
const modsRoutes = require('./routes/mods');
|
|
||||||
const { errorHandler, notFound } = require('./middleware/errorHandler');
|
const { errorHandler, notFound } = require('./middleware/errorHandler');
|
||||||
|
|
||||||
// Override console.error to properly log error objects
|
// 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/auth', authRoutes);
|
||||||
app.use('/api/servers', serverRoutes);
|
app.use('/api/servers', serverRoutes);
|
||||||
app.use('/api/mods', modsRoutes);
|
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
@ -124,7 +122,7 @@ async function startServer() {
|
|||||||
logger.info('Database connected successfully');
|
logger.info('Database connected successfully');
|
||||||
|
|
||||||
// Start API server
|
// Start API server
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3000;
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
logger.info(`API Server running on port ${PORT}`);
|
logger.info(`API Server running on port ${PORT}`);
|
||||||
logger.info('API Server handles: Authentication, Server Browser, User Data');
|
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
|
* Game Server - Based on Client LocalServer Infrastructure
|
||||||
* Handles actual game instances, player connections, and real-time gameplay
|
* Handles real-time multiplayer game instances and gameplay
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const socketIo = require('socket.io');
|
const socketIo = require('socket.io');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const helmet = require('helmet');
|
|
||||||
const compression = require('compression');
|
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const logger = require('./utils/logger');
|
const logger = require('./utils/logger');
|
||||||
const connectDB = require('./config/database');
|
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 app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const io = socketIo(server, {
|
const io = socketIo(server, {
|
||||||
cors: {
|
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"],
|
methods: ["GET", "POST"],
|
||||||
credentials: true
|
credentials: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Game state
|
||||||
|
const gameInstances = new Map();
|
||||||
|
const connectedClients = new Map();
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(helmet());
|
|
||||||
app.use(compression());
|
|
||||||
app.use(cors({
|
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
|
credentials: true
|
||||||
}));
|
}));
|
||||||
app.use(express.json({ limit: '10mb' }));
|
app.use(express.json({ limit: '10mb' }));
|
||||||
app.use(express.urlencoded({ extended: true }));
|
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) => {
|
app.get('/health', (req, res) => {
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'Game Server OK',
|
status: 'OK',
|
||||||
service: 'galaxystrikeonline-game',
|
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
activeServers: Object.keys(gameServers).length,
|
mode: 'game-server',
|
||||||
connectedPlayers: connectedPlayers.size
|
activeInstances: gameInstances.size,
|
||||||
|
connectedClients: connectedClients.size
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get server status
|
// API version endpoint
|
||||||
app.get('/api/game/status', (req, res) => {
|
app.get('/api/ssc/version', (req, res) => {
|
||||||
res.json({
|
res.status(200).json({
|
||||||
activeServers: Object.keys(gameServers).length,
|
version: '1.0.0',
|
||||||
connectedPlayers: connectedPlayers.size,
|
service: 'galaxystrikeonline-game-server',
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
|
mode: 'multiplayer'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mod management routes
|
// Game data endpoints (similar to LocalServer)
|
||||||
app.use('/api/mods', modRoutes);
|
app.post('/api/game/player/:id/save', (req, res) => {
|
||||||
|
const playerId = req.params.id;
|
||||||
// Error handling
|
const playerData = req.body;
|
||||||
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...`);
|
|
||||||
|
|
||||||
try {
|
// Store player data in game instance
|
||||||
// Stop accepting new connections
|
if (connectedClients.has(playerId)) {
|
||||||
server.close(async () => {
|
connectedClients.get(playerId).playerData = playerData;
|
||||||
logger.info('[GRACEFUL SHUTDOWN] HTTP server closed');
|
|
||||||
|
// Broadcast to other players in same instance
|
||||||
// Unregister from API
|
const instanceId = connectedClients.get(playerId).instanceId;
|
||||||
if (serverRegistration) {
|
if (gameInstances.has(instanceId)) {
|
||||||
await serverRegistration.stopHeartbeat();
|
io.to(instanceId).emit('playerDataUpdated', {
|
||||||
const unregistered = await serverRegistration.unregisterWithAPI();
|
playerId,
|
||||||
if (unregistered) {
|
timestamp: Date.now()
|
||||||
logger.info('[GRACEFUL SHUTDOWN] Server unregistered from API successfully');
|
});
|
||||||
} else {
|
}
|
||||||
logger.warn('[GRACEFUL SHUTDOWN] Failed to unregister from API');
|
}
|
||||||
}
|
|
||||||
|
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();
|
|
||||||
|
// Game data events (similar to LocalServer)
|
||||||
// Close database connections
|
socket.on('saveGameData', (data) => {
|
||||||
const mongoose = require('mongoose');
|
console.log('[GAME SERVER] Saving game data for:', socket.id);
|
||||||
await mongoose.connection.close();
|
|
||||||
logger.info('[GRACEFUL SHUTDOWN] Database connections closed');
|
if (connectedClients.has(socket.id)) {
|
||||||
|
connectedClients.get(socket.id).playerData = data;
|
||||||
process.exit(0);
|
}
|
||||||
|
|
||||||
|
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
|
// Notify other players
|
||||||
setTimeout(() => {
|
socket.to(instanceId).emit('playerJoinedInstance', {
|
||||||
logger.error('[GRACEFUL SHUTDOWN] Forced shutdown after timeout');
|
playerId: socket.id,
|
||||||
process.exit(1);
|
playerCount: instance.players.size
|
||||||
}, 30000);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
const clientData = connectedClients.get(socket.id);
|
||||||
logger.error('[GRACEFUL SHUTDOWN] Error during shutdown:', error);
|
if (clientData && clientData.instanceId) {
|
||||||
process.exit(1);
|
const instance = gameInstances.get(clientData.instanceId);
|
||||||
}
|
if (instance) {
|
||||||
}
|
instance.players.delete(socket.id);
|
||||||
|
|
||||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
// Clean up empty instances
|
||||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
if (instance.players.size === 0) {
|
||||||
|
gameInstances.delete(clientData.instanceId);
|
||||||
// Handle Socket.IO adapter errors
|
}
|
||||||
io.engine.on('connection_error', (err) => {
|
|
||||||
logger.error('Socket.IO connection error:', err);
|
// 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) => {
|
// Start server
|
||||||
logger.error('Socket.IO adapter error:', err);
|
const PORT = process.env.PORT || 3001;
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize database and game systems
|
async function startServer() {
|
||||||
async function startGameServer() {
|
|
||||||
try {
|
try {
|
||||||
// Connect to database
|
// Connect to database
|
||||||
await connectDB();
|
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, () => {
|
server.listen(PORT, () => {
|
||||||
logger.info(`Game Server running on port ${PORT}`);
|
console.log(`[GAME SERVER] Game server running on port ${PORT}`);
|
||||||
logger.info('Game Server handles: Real-time Multiplayer, Game Instances, Socket.IO');
|
console.log(`[GAME SERVER] Socket.IO ready for multiplayer connections`);
|
||||||
logger.info(`Game Server Name: ${serverName}`);
|
|
||||||
logger.info(`Game Server Region: ${serverRegion}`);
|
|
||||||
logger.info(`Game Server URL: ${gameServerUrl}`);
|
|
||||||
logger.info(`API Server URL: ${apiUrl}`);
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to start Game Server:', error);
|
console.error('[GAME SERVER] Failed to start server:', error);
|
||||||
process.exit(1);
|
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();
|
|
||||||
Reference in New Issue
Block a user