screepsmod-market-simulator 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +504 -0
- package/marketSimulatorConfig.js +89 -0
- package/package.json +9 -0
package/index.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// Default Configuration
|
|
6
|
+
// ============================================================
|
|
7
|
+
const DEFAULTS = {
|
|
8
|
+
// Interval between order recreation cycles (5 minutes)
|
|
9
|
+
intervalMs: 5 * 60 * 1000,
|
|
10
|
+
|
|
11
|
+
// NPC user for phantom orders
|
|
12
|
+
npcUserId: '2',
|
|
13
|
+
npcUsername: 'NPC_Market',
|
|
14
|
+
|
|
15
|
+
// NPC credit backing: credits (display) and money (engine-internal integer, credits × 1000)
|
|
16
|
+
// Restocked to this value on every refresh cycle
|
|
17
|
+
npcCredits: 10000000,
|
|
18
|
+
npcMoney: 10000000 * 1000,
|
|
19
|
+
|
|
20
|
+
// Phantom room names — interior corners of 22×22 map for max coverage
|
|
21
|
+
phantomRooms: [
|
|
22
|
+
'W9N9',
|
|
23
|
+
'E9N9',
|
|
24
|
+
'W9S9',
|
|
25
|
+
'E9S9',
|
|
26
|
+
'W5S5',
|
|
27
|
+
'E5S5'
|
|
28
|
+
],
|
|
29
|
+
|
|
30
|
+
// Price spread factors for buy and sell orders
|
|
31
|
+
buySpreadFactors: [0.85, 0.92, 1.0],
|
|
32
|
+
sellSpreadFactors: [1.0, 1.08, 1.15],
|
|
33
|
+
|
|
34
|
+
// Hard cap on order amounts (units per order per resource per spread level)
|
|
35
|
+
maxOrderAmount: 50000,
|
|
36
|
+
|
|
37
|
+
// Terminal store: amount per resource, restocked on every refresh
|
|
38
|
+
terminalStoreAmount: 200000,
|
|
39
|
+
|
|
40
|
+
// Terminal capacity: must hold storeAmount × resourceCount + energy buffer
|
|
41
|
+
terminalStoreCapacity: 30000000,
|
|
42
|
+
|
|
43
|
+
// How to store prices: multiply by this and store as integer
|
|
44
|
+
priceMultiplier: 1000,
|
|
45
|
+
|
|
46
|
+
// Batch size for order insertion
|
|
47
|
+
insertionBatchSize: 500,
|
|
48
|
+
|
|
49
|
+
debug: true
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ============================================================
|
|
53
|
+
// All 89 Screeps Resource Types with Base Prices
|
|
54
|
+
// ============================================================
|
|
55
|
+
const RESOURCES = [
|
|
56
|
+
{ type: 'energy', basePrice: 0.01 },
|
|
57
|
+
// Base minerals
|
|
58
|
+
{ type: 'U', basePrice: 0.2 },
|
|
59
|
+
{ type: 'L', basePrice: 0.2 },
|
|
60
|
+
{ type: 'K', basePrice: 0.2 },
|
|
61
|
+
{ type: 'Z', basePrice: 0.2 },
|
|
62
|
+
{ type: 'O', basePrice: 0.2 },
|
|
63
|
+
{ type: 'H', basePrice: 0.2 },
|
|
64
|
+
{ type: 'X', basePrice: 0.2 },
|
|
65
|
+
{ type: 'G', basePrice: 0.5 },
|
|
66
|
+
// Base compounds
|
|
67
|
+
{ type: 'ZK', basePrice: 0.3 },
|
|
68
|
+
{ type: 'UL', basePrice: 0.3 },
|
|
69
|
+
{ type: 'OH', basePrice: 0.5 },
|
|
70
|
+
// Processed base resources
|
|
71
|
+
{ type: 'silicon', basePrice: 0.3 },
|
|
72
|
+
{ type: 'metal', basePrice: 0.3 },
|
|
73
|
+
{ type: 'mist', basePrice: 0.4 },
|
|
74
|
+
{ type: 'oxidant', basePrice: 0.3 },
|
|
75
|
+
{ type: 'reductant', basePrice: 0.3 },
|
|
76
|
+
{ type: 'purifier', basePrice: 0.3 },
|
|
77
|
+
{ type: 'biomass', basePrice: 0.3 },
|
|
78
|
+
{ type: 'zymite', basePrice: 1.0 },
|
|
79
|
+
{ type: 'catalytic_agent', basePrice: 1.0 },
|
|
80
|
+
{ type: 'hydroxide', basePrice: 0.5 },
|
|
81
|
+
// Tier 1 boosts
|
|
82
|
+
{ type: 'UH', basePrice: 0.5 },
|
|
83
|
+
{ type: 'UO', basePrice: 0.5 },
|
|
84
|
+
{ type: 'KH', basePrice: 0.5 },
|
|
85
|
+
{ type: 'KO', basePrice: 0.5 },
|
|
86
|
+
{ type: 'LH', basePrice: 0.5 },
|
|
87
|
+
{ type: 'LO', basePrice: 0.5 },
|
|
88
|
+
{ type: 'ZH', basePrice: 0.5 },
|
|
89
|
+
{ type: 'ZO', basePrice: 0.5 },
|
|
90
|
+
// Tier 1 Ghodium boosts
|
|
91
|
+
{ type: 'GH', basePrice: 1.0 },
|
|
92
|
+
{ type: 'GO', basePrice: 1.0 },
|
|
93
|
+
// Tier 2 boosts
|
|
94
|
+
{ type: 'UH2O', basePrice: 1.0 },
|
|
95
|
+
{ type: 'UHO2', basePrice: 1.0 },
|
|
96
|
+
{ type: 'KH2O', basePrice: 1.0 },
|
|
97
|
+
{ type: 'KHO2', basePrice: 1.0 },
|
|
98
|
+
{ type: 'LH2O', basePrice: 1.0 },
|
|
99
|
+
{ type: 'LHO2', basePrice: 1.0 },
|
|
100
|
+
{ type: 'ZH2O', basePrice: 1.0 },
|
|
101
|
+
{ type: 'ZHO2', basePrice: 1.0 },
|
|
102
|
+
// Tier 2 Ghodium boosts
|
|
103
|
+
{ type: 'GH2O', basePrice: 2.0 },
|
|
104
|
+
{ type: 'GHO2', basePrice: 2.0 },
|
|
105
|
+
// Tier 3 catalyzed boosts
|
|
106
|
+
{ type: 'XUH2O', basePrice: 2.0 },
|
|
107
|
+
{ type: 'XUHO2', basePrice: 2.0 },
|
|
108
|
+
{ type: 'XKH2O', basePrice: 2.0 },
|
|
109
|
+
{ type: 'XKHO2', basePrice: 2.0 },
|
|
110
|
+
{ type: 'XLH2O', basePrice: 2.0 },
|
|
111
|
+
{ type: 'XLHO2', basePrice: 2.0 },
|
|
112
|
+
{ type: 'XZH2O', basePrice: 2.0 },
|
|
113
|
+
{ type: 'XZHO2', basePrice: 2.0 },
|
|
114
|
+
// Tier 3 catalyzed Ghodium boosts
|
|
115
|
+
{ type: 'XGH2O', basePrice: 4.0 },
|
|
116
|
+
{ type: 'XGHO2', basePrice: 4.0 },
|
|
117
|
+
// Commodities tier 1
|
|
118
|
+
{ type: 'wire', basePrice: 0.5 },
|
|
119
|
+
{ type: 'alloy', basePrice: 0.5 },
|
|
120
|
+
{ type: 'cell', basePrice: 0.5 },
|
|
121
|
+
{ type: 'concentrate', basePrice: 0.5 },
|
|
122
|
+
{ type: 'board', basePrice: 0.7 },
|
|
123
|
+
// Commodities tier 2
|
|
124
|
+
{ type: 'switch', basePrice: 0.7 },
|
|
125
|
+
{ type: 'phlegm', basePrice: 0.7 },
|
|
126
|
+
{ type: 'tube', basePrice: 0.7 },
|
|
127
|
+
{ type: 'extract', basePrice: 0.7 },
|
|
128
|
+
// Commodities tier 3
|
|
129
|
+
{ type: 'transistor', basePrice: 1.0 },
|
|
130
|
+
{ type: 'tissue', basePrice: 1.0 },
|
|
131
|
+
{ type: 'fixtures', basePrice: 1.0 },
|
|
132
|
+
{ type: 'spirit', basePrice: 1.0 },
|
|
133
|
+
// Commodities tier 4
|
|
134
|
+
{ type: 'microchip', basePrice: 1.5 },
|
|
135
|
+
{ type: 'muscle', basePrice: 1.5 },
|
|
136
|
+
{ type: 'frame', basePrice: 1.5 },
|
|
137
|
+
{ type: 'emanation', basePrice: 1.5 },
|
|
138
|
+
// Commodities tier 5
|
|
139
|
+
{ type: 'circuit', basePrice: 2.0 },
|
|
140
|
+
{ type: 'organoid', basePrice: 2.0 },
|
|
141
|
+
{ type: 'hydraulics', basePrice: 2.0 },
|
|
142
|
+
{ type: 'condensate', basePrice: 2.0 },
|
|
143
|
+
// High-tier commodities
|
|
144
|
+
{ type: 'device', basePrice: 2.5 },
|
|
145
|
+
{ type: 'machine', basePrice: 3.0 },
|
|
146
|
+
{ type: 'organism', basePrice: 3.0 },
|
|
147
|
+
// Base products
|
|
148
|
+
{ type: 'battery', basePrice: 0.2 },
|
|
149
|
+
{ type: 'composite', basePrice: 0.2 },
|
|
150
|
+
{ type: 'crystal', basePrice: 0.2 },
|
|
151
|
+
{ type: 'liquid', basePrice: 0.2 },
|
|
152
|
+
// Bars
|
|
153
|
+
{ type: 'utrium_bar', basePrice: 0.5 },
|
|
154
|
+
{ type: 'lemergium_bar', basePrice: 0.5 },
|
|
155
|
+
{ type: 'zynthium_bar', basePrice: 0.5 },
|
|
156
|
+
{ type: 'keanium_bar', basePrice: 0.5 },
|
|
157
|
+
{ type: 'ghodium_melt', basePrice: 1.0 },
|
|
158
|
+
// Power resources
|
|
159
|
+
{ type: 'power', basePrice: 0.6 },
|
|
160
|
+
{ type: 'ops', basePrice: 0.5 },
|
|
161
|
+
{ type: 'essence', basePrice: 1.0 },
|
|
162
|
+
{ type: 'mitochondrion', basePrice: 5.0 }
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const RESOURCE_COUNT = RESOURCES.length;
|
|
166
|
+
|
|
167
|
+
// ============================================================
|
|
168
|
+
// Helper: Store price as integer (price * multiplier)
|
|
169
|
+
// ============================================================
|
|
170
|
+
function storePrice(price, cfg) {
|
|
171
|
+
return Math.round(price * cfg.priceMultiplier);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================
|
|
175
|
+
// Build all orders for a single phantom room
|
|
176
|
+
// ============================================================
|
|
177
|
+
function buildOrdersForRoom(roomName, cfg) {
|
|
178
|
+
const orders = [];
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
const amount = cfg.maxOrderAmount;
|
|
181
|
+
|
|
182
|
+
RESOURCES.forEach((res, resIdx) => {
|
|
183
|
+
const basePrice = res.basePrice;
|
|
184
|
+
|
|
185
|
+
cfg.buySpreadFactors.forEach((factor, buyIdx) => {
|
|
186
|
+
const rawPrice = basePrice * factor;
|
|
187
|
+
const price = storePrice(rawPrice, cfg);
|
|
188
|
+
const orderId = `buy_${res.type}_${roomName}_${resIdx}_${buyIdx}`;
|
|
189
|
+
orders.push({
|
|
190
|
+
_id: orderId,
|
|
191
|
+
user: cfg.npcUserId,
|
|
192
|
+
roomName: roomName,
|
|
193
|
+
resourceType: res.type,
|
|
194
|
+
type: 'buy',
|
|
195
|
+
price: price,
|
|
196
|
+
amount: amount,
|
|
197
|
+
remainingAmount: amount,
|
|
198
|
+
totalAmount: amount,
|
|
199
|
+
active: true,
|
|
200
|
+
created: now,
|
|
201
|
+
createdTimestamp: now
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
cfg.sellSpreadFactors.forEach((factor, sellIdx) => {
|
|
206
|
+
const rawPrice = basePrice * factor;
|
|
207
|
+
const price = storePrice(rawPrice, cfg);
|
|
208
|
+
const orderId = `sell_${res.type}_${roomName}_${resIdx}_${sellIdx}`;
|
|
209
|
+
orders.push({
|
|
210
|
+
_id: orderId,
|
|
211
|
+
user: cfg.npcUserId,
|
|
212
|
+
roomName: roomName,
|
|
213
|
+
resourceType: res.type,
|
|
214
|
+
type: 'sell',
|
|
215
|
+
price: price,
|
|
216
|
+
amount: amount,
|
|
217
|
+
remainingAmount: amount,
|
|
218
|
+
totalAmount: amount,
|
|
219
|
+
active: true,
|
|
220
|
+
created: now,
|
|
221
|
+
createdTimestamp: now
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return orders;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ============================================================
|
|
230
|
+
// Build all orders across all phantom rooms
|
|
231
|
+
// ============================================================
|
|
232
|
+
function buildAllOrders(cfg) {
|
|
233
|
+
let allOrders = [];
|
|
234
|
+
cfg.phantomRooms.forEach(roomName => {
|
|
235
|
+
const roomOrders = buildOrdersForRoom(roomName, cfg);
|
|
236
|
+
allOrders = allOrders.concat(roomOrders);
|
|
237
|
+
});
|
|
238
|
+
return allOrders;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============================================================
|
|
242
|
+
// Build terminal store object with all resources
|
|
243
|
+
// ============================================================
|
|
244
|
+
function buildStoreObject(cfg) {
|
|
245
|
+
const store = {};
|
|
246
|
+
RESOURCES.forEach(res => {
|
|
247
|
+
store[res.type] = cfg.terminalStoreAmount;
|
|
248
|
+
});
|
|
249
|
+
return store;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================================
|
|
253
|
+
// Restock NPC user: credits + money to baseline
|
|
254
|
+
// ============================================================
|
|
255
|
+
async function restockNpcUser(db, cfg) {
|
|
256
|
+
try {
|
|
257
|
+
await db['users'].update(
|
|
258
|
+
{ _id: cfg.npcUserId },
|
|
259
|
+
{
|
|
260
|
+
$setOnInsert: {
|
|
261
|
+
_id: cfg.npcUserId,
|
|
262
|
+
username: cfg.npcUsername,
|
|
263
|
+
cpu: 100,
|
|
264
|
+
active: true
|
|
265
|
+
},
|
|
266
|
+
$set: {
|
|
267
|
+
money: cfg.npcMoney,
|
|
268
|
+
credits: cfg.npcCredits,
|
|
269
|
+
lastLoginTime: Date.now()
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
{ upsert: true }
|
|
273
|
+
);
|
|
274
|
+
if (cfg.debug) console.log(`[MarketSim] NPC user restocked: ${cfg.npcCredits.toLocaleString()} credits`);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
if (cfg.debug) console.log(`[MarketSim] Note: upsert NPC user: ${e.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ============================================================
|
|
281
|
+
// Ensure phantom room documents and terminal objects exist
|
|
282
|
+
// ============================================================
|
|
283
|
+
async function ensurePhantomRooms(db, cfg) {
|
|
284
|
+
const npcUserId = cfg.npcUserId;
|
|
285
|
+
|
|
286
|
+
for (const roomName of cfg.phantomRooms) {
|
|
287
|
+
try {
|
|
288
|
+
await db['rooms'].update(
|
|
289
|
+
{ _id: roomName },
|
|
290
|
+
{
|
|
291
|
+
$setOnInsert: {
|
|
292
|
+
_id: roomName,
|
|
293
|
+
status: 'normal',
|
|
294
|
+
active: true
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
{ upsert: true }
|
|
298
|
+
);
|
|
299
|
+
} catch (e) {
|
|
300
|
+
if (cfg.debug) console.log(`[MarketSim] Note: upsert room ${roomName}: ${e.message}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const store = buildStoreObject(cfg);
|
|
304
|
+
const terminalId = `terminal_${roomName}`;
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
await db['rooms.objects'].update(
|
|
308
|
+
{ _id: terminalId },
|
|
309
|
+
{
|
|
310
|
+
$set: {
|
|
311
|
+
_id: terminalId,
|
|
312
|
+
type: 'terminal',
|
|
313
|
+
room: roomName,
|
|
314
|
+
user: npcUserId,
|
|
315
|
+
x: 25,
|
|
316
|
+
y: 25,
|
|
317
|
+
store: store,
|
|
318
|
+
storeCapacity: cfg.terminalStoreCapacity,
|
|
319
|
+
energy: 0,
|
|
320
|
+
cooldown: 0,
|
|
321
|
+
nextSendTick: 0,
|
|
322
|
+
send: {}
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
{ upsert: true }
|
|
326
|
+
);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
if (cfg.debug) console.log(`[MarketSim] Note: upsert terminal for ${roomName}: ${e.message}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (cfg.debug) console.log(`[MarketSim] Phantom rooms initialized: ${cfg.phantomRooms.join(', ')}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ============================================================
|
|
336
|
+
// Restock terminal stores to baseline for all phantom rooms
|
|
337
|
+
// ============================================================
|
|
338
|
+
async function restockTerminals(db, cfg) {
|
|
339
|
+
const store = buildStoreObject(cfg);
|
|
340
|
+
|
|
341
|
+
for (const roomName of cfg.phantomRooms) {
|
|
342
|
+
const terminalId = `terminal_${roomName}`;
|
|
343
|
+
try {
|
|
344
|
+
await db['rooms.objects'].update(
|
|
345
|
+
{ _id: terminalId },
|
|
346
|
+
{ $set: { store: store, storeCapacity: cfg.terminalStoreCapacity } }
|
|
347
|
+
);
|
|
348
|
+
} catch (e) {
|
|
349
|
+
if (cfg.debug) console.log(`[MarketSim] Note: restock terminal ${roomName}: ${e.message}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (cfg.debug) console.log(`[MarketSim] Terminal stores restocked for ${cfg.phantomRooms.length} rooms`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============================================================
|
|
357
|
+
// Remove NPC orders, then recreate them (targeted refresh)
|
|
358
|
+
// ============================================================
|
|
359
|
+
async function recreateOrders(db, cfg) {
|
|
360
|
+
try {
|
|
361
|
+
// 1. Restock NPC credits and money
|
|
362
|
+
await restockNpcUser(db, cfg);
|
|
363
|
+
|
|
364
|
+
// 2. Restock terminal stores
|
|
365
|
+
await restockTerminals(db, cfg);
|
|
366
|
+
|
|
367
|
+
// 3. Remove only NPC-created orders (not player orders)
|
|
368
|
+
try {
|
|
369
|
+
await db['market.orders'].removeWhere({ user: cfg.npcUserId });
|
|
370
|
+
} catch (e) {
|
|
371
|
+
// LokiJS uses removeWhere, MongoDB uses deleteMany
|
|
372
|
+
if (cfg.debug) console.log(`[MarketSim] Note: removeWhere not supported, trying deleteMany`);
|
|
373
|
+
try {
|
|
374
|
+
await db['market.orders'].deleteMany({ user: cfg.npcUserId });
|
|
375
|
+
} catch (e2) {
|
|
376
|
+
if (cfg.debug) console.log(`[MarketSim] Note: deleteMany also not supported, trying remove`);
|
|
377
|
+
try {
|
|
378
|
+
await db['market.orders'].remove({ user: cfg.npcUserId });
|
|
379
|
+
} catch (e3) {
|
|
380
|
+
if (cfg.debug) console.log(`[MarketSim] Warning: Could not remove old NPC orders: ${e3.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 4. Build new orders
|
|
386
|
+
const allOrders = buildAllOrders(cfg);
|
|
387
|
+
|
|
388
|
+
// 5. Insert in batches (staggered to avoid blocking the storage thread)
|
|
389
|
+
const batchSize = cfg.insertionBatchSize || 500;
|
|
390
|
+
let inserted = 0;
|
|
391
|
+
|
|
392
|
+
for (let i = 0; i < allOrders.length; i += batchSize) {
|
|
393
|
+
const batch = allOrders.slice(i, i + batchSize);
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
await db['market.orders'].insertMany(batch);
|
|
397
|
+
inserted += batch.length;
|
|
398
|
+
} catch (e) {
|
|
399
|
+
// Fall back to individual upserts
|
|
400
|
+
for (const order of batch) {
|
|
401
|
+
try {
|
|
402
|
+
await db['market.orders'].update(
|
|
403
|
+
{ _id: order._id },
|
|
404
|
+
order,
|
|
405
|
+
{ upsert: true }
|
|
406
|
+
);
|
|
407
|
+
inserted++;
|
|
408
|
+
} catch (e2) {
|
|
409
|
+
if (cfg.debug) console.log(`[MarketSim] Failed to insert order ${order._id}: ${e2.message}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const roomCount = cfg.phantomRooms.length;
|
|
416
|
+
const resCount = RESOURCE_COUNT;
|
|
417
|
+
const ordersPerResource = cfg.buySpreadFactors.length + cfg.sellSpreadFactors.length;
|
|
418
|
+
if (cfg.debug) {
|
|
419
|
+
console.log(`[MarketSim] Refreshed ${inserted} orders across ${roomCount} rooms ` +
|
|
420
|
+
`(${resCount} resources × ${ordersPerResource} orders each per room, ` +
|
|
421
|
+
`${cfg.maxOrderAmount.toLocaleString()} units each)`);
|
|
422
|
+
}
|
|
423
|
+
} catch (err) {
|
|
424
|
+
console.log(`[MarketSim] Error recreating orders: ${err.message}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ============================================================
|
|
429
|
+
// Main mod entry point
|
|
430
|
+
// ============================================================
|
|
431
|
+
module.exports = function (config, pluginContext) {
|
|
432
|
+
if (!config || !config.backend) return;
|
|
433
|
+
|
|
434
|
+
// Load optional user config from marketSimulatorConfig.js
|
|
435
|
+
let userConfig = {};
|
|
436
|
+
const configPath = path.join(__dirname, 'marketSimulatorConfig.js');
|
|
437
|
+
try {
|
|
438
|
+
if (fs.existsSync(configPath)) {
|
|
439
|
+
userConfig = require(configPath);
|
|
440
|
+
if (typeof userConfig === 'function') {
|
|
441
|
+
userConfig = userConfig(config, pluginContext);
|
|
442
|
+
}
|
|
443
|
+
console.log('[MarketSim] Loaded custom config from marketSimulatorConfig.js');
|
|
444
|
+
}
|
|
445
|
+
} catch (e) {
|
|
446
|
+
if (DEFAULTS.debug) {
|
|
447
|
+
console.log(`[MarketSim] No marketSimulatorConfig.js found, using defaults`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Merge configs (deep merge for arrays)
|
|
452
|
+
const cfg = Object.assign({}, DEFAULTS, userConfig);
|
|
453
|
+
if (userConfig.phantomRooms) cfg.phantomRooms = userConfig.phantomRooms;
|
|
454
|
+
if (userConfig.buySpreadFactors) cfg.buySpreadFactors = userConfig.buySpreadFactors;
|
|
455
|
+
if (userConfig.sellSpreadFactors) cfg.sellSpreadFactors = userConfig.sellSpreadFactors;
|
|
456
|
+
|
|
457
|
+
const db = config.common && config.common.storage && config.common.storage.db;
|
|
458
|
+
|
|
459
|
+
if (!db) {
|
|
460
|
+
console.log('[MarketSim] ERROR: No database available via config.common.storage.db');
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log('[MarketSim] Mod loaded — initializing phantom market system...');
|
|
465
|
+
|
|
466
|
+
const roomsCount = cfg.phantomRooms.length;
|
|
467
|
+
const resCount = RESOURCE_COUNT;
|
|
468
|
+
const ordersPerRes = cfg.buySpreadFactors.length + cfg.sellSpreadFactors.length;
|
|
469
|
+
const totalOrders = roomsCount * resCount * ordersPerRes;
|
|
470
|
+
console.log(`[MarketSim] Expected: ${totalOrders} orders ` +
|
|
471
|
+
`(${roomsCount} rooms × ${resCount} resources × ${ordersPerRes} price levels)`);
|
|
472
|
+
|
|
473
|
+
// Initialize phantom rooms then start refresh cycle
|
|
474
|
+
restockNpcUser(db, cfg)
|
|
475
|
+
.then(() => ensurePhantomRooms(db, cfg))
|
|
476
|
+
.then(() => {
|
|
477
|
+
console.log('[MarketSim] Phantom rooms ready');
|
|
478
|
+
recreateOrders(db, cfg);
|
|
479
|
+
|
|
480
|
+
// Prefer cronjobs if available, fall back to setInterval
|
|
481
|
+
if (config.cronjobs) {
|
|
482
|
+
config.cronjobs.marketSimRefresh = [
|
|
483
|
+
cfg.intervalMs / 1000,
|
|
484
|
+
() => recreateOrders(db, cfg)
|
|
485
|
+
];
|
|
486
|
+
console.log(`[MarketSim] Registered cronjob: marketSimRefresh every ${cfg.intervalMs / 1000}s`);
|
|
487
|
+
} else {
|
|
488
|
+
const intervalId = setInterval(() => {
|
|
489
|
+
recreateOrders(db, cfg);
|
|
490
|
+
}, cfg.intervalMs);
|
|
491
|
+
console.log(`[MarketSim] Using setInterval every ${cfg.intervalMs / 1000}s`);
|
|
492
|
+
|
|
493
|
+
if (pluginContext && pluginContext.onUnload) {
|
|
494
|
+
pluginContext.onUnload(() => {
|
|
495
|
+
clearInterval(intervalId);
|
|
496
|
+
console.log('[MarketSim] Unloaded');
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
.catch(err => {
|
|
502
|
+
console.log(`[MarketSim] Initialization failed: ${err.message}`);
|
|
503
|
+
});
|
|
504
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration overrides for screepsmod-market-simulator.
|
|
3
|
+
*
|
|
4
|
+
* This file is optional. If absent or exporting an empty object, the defaults
|
|
5
|
+
* in index.js are used. Uncomment and modify any settings below to customize.
|
|
6
|
+
*
|
|
7
|
+
* --- Market Simulation Overview ---
|
|
8
|
+
*
|
|
9
|
+
* The mod seeds the market order book with buy and sell orders for ALL 89
|
|
10
|
+
* Screeps resource types across configurable phantom rooms. Each order has a
|
|
11
|
+
* hard-capped amount (maxOrderAmount) to prevent runaway CPU/memory usage.
|
|
12
|
+
* On every refresh cycle (default 5 min), NPC credits, terminal stores, and
|
|
13
|
+
* all orders are restocked to their configured baselines.
|
|
14
|
+
*
|
|
15
|
+
* The Screeps engine handles ALL transactional aspects of dealing (resource
|
|
16
|
+
* transfer, credit exchange, energy costs, cooldowns). This mod only seeds
|
|
17
|
+
* the database.
|
|
18
|
+
*
|
|
19
|
+
* --- Key Settings ---
|
|
20
|
+
*
|
|
21
|
+
* maxOrderAmount Hard cap on every order's amount, remainingAmount,
|
|
22
|
+
* and totalAmount. This prevents the engine from
|
|
23
|
+
* computing absurd amounts from NPC money.
|
|
24
|
+
* Higher = more market depth, but larger DB writes.
|
|
25
|
+
*
|
|
26
|
+
* npcCredits / npcMoney NPC backing for buy orders. Restocked to this value
|
|
27
|
+
* every refresh. npcMoney is the internal integer field
|
|
28
|
+
* (credits × 1000). Keep these high enough to cover all
|
|
29
|
+
* active buy orders at once.
|
|
30
|
+
*
|
|
31
|
+
* terminalStoreAmount Amount of EACH resource in NPC terminal stores,
|
|
32
|
+
* restocked every refresh. Backs sell orders.
|
|
33
|
+
* Must fit within terminalStoreCapacity when summed.
|
|
34
|
+
*
|
|
35
|
+
* terminalStoreCapacity Total NPC terminal capacity. Must exceed:
|
|
36
|
+
* terminalStoreAmount × 89 (resource count) + energy.
|
|
37
|
+
*
|
|
38
|
+
* buySpreadFactors Price multipliers for buy orders relative to base
|
|
39
|
+
* price. E.g., 0.85 = 85% of base = below market.
|
|
40
|
+
*
|
|
41
|
+
* sellSpreadFactors Price multipliers for sell orders relative to base
|
|
42
|
+
* price. E.g., 1.15 = 115% of base = above market.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
|
|
47
|
+
// --- Phantom Rooms ---
|
|
48
|
+
// Room names that don't exist in the world. Each gets a terminal
|
|
49
|
+
// and posts buy/sell orders for all resources.
|
|
50
|
+
// phantomRooms: ['W777N777', 'E777N777', 'W777S777', 'E777S777'],
|
|
51
|
+
|
|
52
|
+
// --- NPC Identity ---
|
|
53
|
+
// npcUserId: '2',
|
|
54
|
+
// npcUsername: 'NPC_Market',
|
|
55
|
+
|
|
56
|
+
// --- NPC Credit Backing ---
|
|
57
|
+
// Restocked every refresh. Money is the internal integer field
|
|
58
|
+
// used by the engine (credits × 1000).
|
|
59
|
+
// npcCredits: 5000000,
|
|
60
|
+
// npcMoney: 5000000000,
|
|
61
|
+
|
|
62
|
+
// --- Order Size ---
|
|
63
|
+
// Hard cap on EACH order's amount/remainingAmount/totalAmount.
|
|
64
|
+
// Limits CPU/memory impact of engine order processing.
|
|
65
|
+
// maxOrderAmount: 25000,
|
|
66
|
+
|
|
67
|
+
// --- Price Spreads ---
|
|
68
|
+
// Multiplied against basePrice to compute order price.
|
|
69
|
+
// Must have at least 1 entry each.
|
|
70
|
+
// buySpreadFactors: [0.80, 0.90, 0.95, 1.0],
|
|
71
|
+
// sellSpreadFactors: [1.0, 1.05, 1.10, 1.20],
|
|
72
|
+
|
|
73
|
+
// --- Terminal Stores ---
|
|
74
|
+
// terminalStoreAmount must be >= maxOrderAmount so sell orders
|
|
75
|
+
// are properly backed. terminalStoreCapacity must hold all
|
|
76
|
+
// resources × storeAmount plus energy buffer.
|
|
77
|
+
// terminalStoreAmount: 100000,
|
|
78
|
+
// terminalStoreCapacity: 15000000,
|
|
79
|
+
|
|
80
|
+
// --- Refresh Interval ---
|
|
81
|
+
// Milliseconds between restock + order refresh cycles.
|
|
82
|
+
// intervalMs: 300000, // 5 minutes
|
|
83
|
+
|
|
84
|
+
// --- Prices ---
|
|
85
|
+
// priceMultiplier: 1000, // Store prices as integers
|
|
86
|
+
|
|
87
|
+
// --- Debugging ---
|
|
88
|
+
// debug: false // Set false to suppress log output
|
|
89
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "screepsmod-market-simulator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Populates the Screeps market order book with buy/sell orders for phantom rooms to test bot trading systems",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"author": "Custom",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"screeps_mod": true
|
|
9
|
+
}
|