Files
Server/src/engine/gameManager/ForgeManager.java
T
MagicBotandClaude Opus 5.5 566870de79 Move the remaining legacy item template lookups to CObjectTemplate
Dev commands, BuildingManager, ChatManager, ForgeManager, NPCManager,
NPC, Warehouse, WorkOrder and ManageNPCMsg now call
CObjectTemplate.lookup instead of ItemTemplate.templates.get. Forge
production cost reads getItemResourceCosts(). Only the DB loader and
linkCacheTemplates still use the legacy class (removed in Task 1.8).

Removes the static_item_templates ParityCheck comparator. Final parity
(offline p17.py, 7586 rows): every id has a cache template, 0 item_type
differences, 0 item_resource_costs differences (84 ITEMs have costs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:56:08 -04:00

416 lines
16 KiB
Java

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.gameManager;
import engine.loot.ModTableEntry;
import engine.loot.ModTypeTableEntry;
import engine.loot.WorkOrder;
import engine.mbEnums;
import engine.net.client.msg.ItemProductionMsg;
import engine.objects.City;
import engine.objects.Item;
import engine.objects.NPC;
import engine.parsers.objects.CObjectTemplate;
import engine.powers.EffectsBase;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
public enum ForgeManager implements Runnable {
// MB Dev notes:
// Class implements forge rolling mechanics for Magicbane.
//
// .submit(workOrder) may be called from any thread: (ItemProductionMsgHandler).
// Concurrency is managed by same lock used for warehouse (city.cityTransactionLock).
// WorkOrders are persisted then reconstituted at bootstrap using table dyn.workorders.
// Forge window (ManageNPCMsg) uses item.upgradeDate to serialize completion time.
//
// Replaces garbage code that looked as if written by a mental patient.
//
// @TODO Reuse same set of virtual items for the life of a workOrder.
FORGE_MANAGER;
public static final BlockingQueue<WorkOrder> forge = new DelayQueue<>();
public static final AtomicInteger workOrderCounter = new AtomicInteger(0);
public static final ConcurrentHashMap<NPC, ConcurrentHashMap.KeySetView<WorkOrder, Boolean>> vendorWorkOrderLookup = new ConcurrentHashMap<>();
public static final ConcurrentHashMap<Item, WorkOrder> itemWorkOrderLookup = new ConcurrentHashMap<>();
// Server-only data from static_vendor_items; the client templates have no mod table.
public static HashMap<Integer, Integer> vendorModTables = new HashMap<>();
public static void start() {
Thread forgeManager;
forgeManager = new Thread(FORGE_MANAGER);
forgeManager.setName("Forge Manager");
forgeManager.start();
}
@Override
public void run() {
WorkOrder workOrder;
while (true) {
// The forge is a blocking priority queue using an epoc sort.
// workOrders are popped once the workOrder.completionTime has
// passed.
try {
workOrder = forge.take();
// Early exit for completed workOrders loaded from disk
// or vendors who were re-deeded with items still cooking.
if (workOrder.vendor == null || workOrder.runCompleted.get())
continue;
// This workOrder has completed production.
if (workOrder.total_produced >= workOrder.total_to_produce) {
// CONFIRM_PRODUCE to refresh the interface after we add
// the Identified flag to the item.
for (Item workOrderItem : workOrder.cooking) {
workOrderItem.flags.add(mbEnums.ItemFlags.Identified);
ItemProductionMsg outMsg = new ItemProductionMsg(workOrder.vendor.building, workOrder.vendor, workOrderItem, mbEnums.ProductionActionType.CONFIRM_PRODUCE, true);
DispatchManager.dispatchMsgToInterestArea(workOrder.vendor, outMsg, mbEnums.DispatchChannel.SECONDARY, 700, false, false);
}
// Set this workOrder to completed and update on disk
workOrder.runCompleted.set(true);
DbManager.WarehouseQueries.WRITE_WORKORDER(workOrder);
continue;
}
// PERSIST our current cooking batch of virtual
// items then add to the vendor inventory
completeWorkOrderBatch(workOrder);
// Create new set of in-memory only virtual items
forgeWorkOrderBatch(workOrder);
// enQueue this workOrder again; back into the oven
// until all items for this workOrder are completed.
forge.add(workOrder);
// Debugging: Logger.info(workOrder.toString());
} catch (Exception e) {
Logger.error(e);
}
}
}
public static int submit(WorkOrder workOrder) {
// Must have a city to roll anything
City city = workOrder.vendor.building.getCity();
if (city == null)
return 58; //58: The formula is beyond the means of this facility
// Concurrency is rightly managed by same lock as warehouse
city.transactionLock.writeLock().lock();
// Make sure vendor can roll the formulae, warehouse can
// afford this wordOrder and other related checks.
int validation_result = WorkOrder.validate(workOrder);
// The return code is used by the caller (ItemProductionMsgHandler)
// for display of a popup error message to the player.
if (validation_result != 0)
return validation_result;
// Configure this production run.
try {
workOrder.workOrderID = workOrderCounter.incrementAndGet();
workOrder.rollingDuration = NPCManager.calcRollingDuration(workOrder.vendor, workOrder.templateID);
workOrder.completionTime = System.currentTimeMillis() + workOrder.rollingDuration;
workOrder.slots_used.set(calcAvailableSlots(workOrder));
if (workOrder.slots_used.get() == 0)
return 58;
workOrder.total_produced = 0;
// Single item configuration from protocol message (ItemProductionMsg)
if (workOrder.multiple_slot_request == false && workOrder.total_to_produce == 0)
workOrder.total_to_produce = 1;
// Calculate total cost for this production run
workOrder.total_to_produce *= workOrder.slots_used.get();
workOrder.production_cost = calcProductionCost(workOrder);
workOrder.production_cost.forEach((key, value) -> workOrder.production_cost_total.put(key, value * workOrder.total_to_produce));
// Withdraw gold and resources. Availability has previously been validated.
if (!WorkOrder.withdrawWorkOrderCost(workOrder))
return 58; //58: The formula is beyond the means of this facility
// Create new batch of virtual items
forgeWorkOrderBatch(workOrder);
// Assign the new workOrder to the vendor
vendorWorkOrderLookup.get(workOrder.vendor).add(workOrder);
// Enqueue the new workOrder
forge.add(workOrder);
// PERSIST workOrder (dyn_workorders)
DbManager.WarehouseQueries.WRITE_WORKORDER(workOrder);
} catch (Exception e) {
Logger.error(e);
} finally {
city.transactionLock.writeLock().unlock();
}
Logger.info(workOrder.toString());
return validation_result;
}
public static int calcAvailableSlots(WorkOrder workOrder) {
// Slots available in a forge are based on the npc rank
int availableSlots = workOrder.vendor.getRank();
// Subtract the slots currently assigned to other workOrders for this vendor
for (WorkOrder npcWorkOrder : ForgeManager.vendorWorkOrderLookup.get(workOrder.vendor))
availableSlots = availableSlots - npcWorkOrder.cooking.size();
// Single item rolls are always a single slot
if (availableSlots > 0 && workOrder.multiple_slot_request == false)
availableSlots = 1;
return availableSlots;
}
public static HashMap<mbEnums.ResourceType, Integer> calcProductionCost(WorkOrder workOrder) {
// Calculate production cost for a single run of the workOrder
HashMap<mbEnums.ResourceType, Integer> production_cost = new HashMap<>();
CObjectTemplate template = CObjectTemplate.lookup(workOrder.templateID);
// Add gold and resource costs from template
production_cost.put(mbEnums.ResourceType.GOLD, template.getItemValue());
production_cost.putAll(template.getItemResourceCosts());
// Calculate cost of prefix and suffix
if (workOrder.prefixToken != 0) {
EffectsBase prefix = PowersManager.getEffectByToken(workOrder.prefixToken);
production_cost.putAll(PowersManager._effect_costMaps.get(prefix.getIDString()));
}
if (workOrder.suffixToken != 0) {
EffectsBase suffix = PowersManager.getEffectByToken(workOrder.suffixToken);
production_cost.putAll(PowersManager._effect_costMaps.get(suffix.getIDString()));
}
return production_cost;
}
public static Item forgeItem(WorkOrder workOrder) {
// Create new virtual item from specified template
Item forgedItem = new Item(workOrder.templateID);
int modTable = getVendorModTable(workOrder.templateID);
// forgedItem gets a negative id; a virtual item which is not persisted
forgedItem.objectUUID = ItemManager.lastNegativeID.getAndDecrement();
forgedItem.containerType = mbEnums.ItemContainerType.FORGE;
forgedItem.ownerID = workOrder.vendor.getObjectUUID();
// item.upgradeDate is serialized (ItemProductionMsg)
// for vendor forge window completion time.
forgedItem.setDateToUpgrade(workOrder.completionTime);
// Assign a prefix and suffix to this item if random rolled
if (workOrder.prefixToken == 0 && workOrder.vendor.getItemModTable().contains(modTable))
forgedItem.prefixToken = calcRandomMod(workOrder.vendor, mbEnums.ItemModType.PREFIX, modTable);
else
forgedItem.prefixToken = workOrder.prefixToken;
if (workOrder.suffixToken == 0 && workOrder.vendor.getItemModTable().contains(modTable))
forgedItem.suffixToken = calcRandomMod(workOrder.vendor, mbEnums.ItemModType.SUFFIX, modTable);
else
forgedItem.suffixToken = workOrder.suffixToken;
// Random rolled items are unidentified until completed
if (workOrder.prefixToken == 0 && workOrder.suffixToken == 0)
forgedItem.flags.remove(mbEnums.ItemFlags.Identified);
else
forgedItem.flags.add(mbEnums.ItemFlags.Identified);
// Add virtual item to in-memory caches
workOrder.cooking.add(forgedItem);
DbManager.addToCache(forgedItem);
itemWorkOrderLookup.put(forgedItem, workOrder);
return forgedItem;
}
public static void completeWorkOrderBatch(WorkOrder workOrder) {
ArrayList<Item> toRemove = new ArrayList<>();
for (Item virutalItem : workOrder.cooking) {
// Identify completed items
virutalItem.flags.add(mbEnums.ItemFlags.Identified);
virutalItem.containerType = mbEnums.ItemContainerType.INVENTORY;
// Persist item
Item completedItem = DbManager.ItemQueries.PERSIST(virutalItem);
// Copy Prefix and Suffix tokens from virtual item.
completedItem.prefixToken = virutalItem.prefixToken;
completedItem.suffixToken = virutalItem.suffixToken;
// Add effects for the tokens. Writes to disk using
// table dyn_item_enchantment
ItemManager.applyItemEffects(completedItem);
// Add to the vendor inventory
workOrder.vendor.charItemManager.addItemToInventory(completedItem);
ItemProductionMsg outMsg1 = new ItemProductionMsg(workOrder.vendor.building, workOrder.vendor, completedItem, mbEnums.ProductionActionType.DEPOSIT, true);
DispatchManager.dispatchMsgToInterestArea(workOrder.vendor, outMsg1, mbEnums.DispatchChannel.SECONDARY, 700, false, false);
ItemProductionMsg outMsg2 = new ItemProductionMsg(workOrder.vendor.building, workOrder.vendor, completedItem, mbEnums.ProductionActionType.CONFIRM_DEPOSIT, true);
DispatchManager.dispatchMsgToInterestArea(workOrder.vendor, outMsg2, mbEnums.DispatchChannel.SECONDARY, 700, false, false);
toRemove.add(virutalItem);
}
for (Item virtualItem : toRemove) {
// Remove virtual items from the forge window
ItemProductionMsg outMsg = new ItemProductionMsg(workOrder.vendor.building, workOrder.vendor, virtualItem, mbEnums.ProductionActionType.CONFIRM_SETPRICE, true);
DispatchManager.dispatchMsgToInterestArea(workOrder.vendor, outMsg, mbEnums.DispatchChannel.SECONDARY, 700, false, false);
// Remove virtual item from all collections
workOrder.cooking.remove(virtualItem);
itemWorkOrderLookup.remove(virtualItem);
DbManager.removeFromCache(virtualItem);
}
}
public static void forgeWorkOrderBatch(WorkOrder workOrder) {
// Completion time for this batch is in the future
workOrder.completionTime = System.currentTimeMillis() + workOrder.rollingDuration;
for (int i = 0; i < workOrder.slots_used.get(); ++i) {
Item forged_item = forgeItem(workOrder);
// Update NPC window
ItemProductionMsg outMsg = new ItemProductionMsg(workOrder.vendor.building, workOrder.vendor, forged_item, mbEnums.ProductionActionType.CONFIRM_PRODUCE, true);
DispatchManager.dispatchMsgToInterestArea(workOrder.vendor, outMsg, mbEnums.DispatchChannel.SECONDARY, 700, false, false);
workOrder.total_produced = workOrder.total_produced + 1;
}
// Write updated workOrder to disk
DbManager.WarehouseQueries.WRITE_WORKORDER(workOrder);
}
public static int getVendorModTable(int templateID) {
return vendorModTables.getOrDefault(templateID, 0);
}
public static int calcRandomMod(NPC vendor, mbEnums.ItemModType itemModType, int modTable) {
// Random prefix or suffix token based on item.template.modtable
int modifier = 0;
ModTypeTableEntry modTypeTableEntry = null;
ModTableEntry modTableEntry;
int rollForModifier;
switch (itemModType) {
case PREFIX:
int randomPrefix = vendor.getModTypeTable().get(vendor.getItemModTable().indexOf(modTable));
modTypeTableEntry = ModTypeTableEntry.rollTable(randomPrefix, ThreadLocalRandom.current().nextInt(1, 100 + 1));
break;
case SUFFIX:
int randomSuffix = vendor.getModSuffixTable().get(vendor.getItemModTable().indexOf(modTable));
modTypeTableEntry = ModTypeTableEntry.rollTable(randomSuffix, ThreadLocalRandom.current().nextInt(1, 100 + 1));
break;
}
if (modTypeTableEntry == null)
return 0;
rollForModifier = ThreadLocalRandom.current().nextInt(1, 100 + 1);
if (rollForModifier < 80) {
int randomModifier = LootManager.TableRoll(vendor.getLevel(), false);
modTableEntry = ModTableEntry.rollTable(modTypeTableEntry.modTableID, randomModifier);
// @TODO : Figure out how a null can be returned from a defined set.
if (modTableEntry == null)
return 0;
EffectsBase effectsBase = PowersManager.getEffectByIDString(modTableEntry.action);
modifier = effectsBase.getToken();
}
return modifier;
}
}