// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2022 // www.magicbane.com package engine.gameManager; import engine.loot.ModTableEntry; import engine.loot.ModTypeTableEntry; import engine.loot.WorkOrder; import engine.mbEnums; import engine.net.DispatchMessage; import engine.net.client.msg.ItemProductionMsg; import engine.objects.City; import engine.objects.Item; import engine.objects.ItemTemplate; import engine.objects.NPC; import engine.powers.EffectsBase; import org.pmw.tinylog.Logger; import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.*; 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 with face boils. // // @TODO Reuse same set of virtual items for each workOrder cycle. FORGE_MANAGER; public static final BlockingQueue forge = new DelayQueue<>(); public static final AtomicInteger workOrderCounter = new AtomicInteger(0); public static final ConcurrentHashMap> vendorWorkOrderLookup = new ConcurrentHashMap<>(); public static final ConcurrentHashMap itemWorkOrderLookup = new ConcurrentHashMap<>(); @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(); // This workOrder has completed production. if (workOrder.total_produced >= workOrder.total_to_produce) { // Set items as completed in the window. // First CONFIRM_PRODUCE adds virtual item to the interface. // Second CONFIRM_PRODUCE sets virtual item to complete. 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); DispatchMessage.dispatchMsgToInterestArea(workOrder.vendor, outMsg, mbEnums.DispatchChannel.SECONDARY, 700, false, false); } workOrder.runCompleted = true; // Update workOrder on disk DbManager.WarehouseQueries.WRITE_WORKORDER(workOrder); } if (workOrder.runCompleted) continue; // Move current cooking batch to 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 void start() { Thread forgeManager; forgeManager = new Thread(FORGE_MANAGER); forgeManager.setName("Forge Manager"); forgeManager.start(); } 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; try { // Configure this production run. workOrder.workOrderID = workOrderCounter.incrementAndGet(); workOrder.rollingDuration = ForgeManager.calcRollingDuration(workOrder); workOrder.completionTime = System.currentTimeMillis() + workOrder.rollingDuration; workOrder.slots_used = calcAvailableSlots(workOrder); workOrder.total_produced = 0; // Single item configuration if (!workOrder.multiple_slot_request && workOrder.total_to_produce == 0) workOrder.total_to_produce = 1; // Set total cost for production run workOrder.total_to_produce *= workOrder.slots_used; workOrder.production_cost = calcProductionCost(workOrder); workOrder.production_cost_total.putAll(workOrder.production_cost); workOrder.production_cost_total.forEach((key, value) -> workOrder.production_cost_total.put(key, value * workOrder.total_to_produce)); // Withdraw gold and resource costs. 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 long calcRollingDuration(WorkOrder workOrder) { float rollingDuration; rollingDuration = workOrder.vendor.getBuilding().getRank() * -5L + 40; rollingDuration = TimeUnit.MINUTES.toMillis((long) rollingDuration); rollingDuration *= Float.parseFloat(ConfigManager.MB_PRODUCTION_RATE.getValue()); ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID); // Bane circles if (template.item_bane_rank > 0) rollingDuration = (long) template.item_bane_rank * 60 * 60 * 3 * 1000 * Float.parseFloat(ConfigManager.MB_PRODUCTION_RATE.getValue()); return (long) rollingDuration; } 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) availableSlots = 1; return availableSlots; } public static HashMap calcProductionCost(WorkOrder workOrder) { // Calculate production cost for a single run of the workOrder HashMap production_cost = new HashMap<>(); ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID); // Add gold and resource costs from template production_cost.put(mbEnums.ResourceType.GOLD, template.item_value); production_cost.putAll(template.item_resource_cost); // 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 ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID); Item forgedItem = new Item(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) forgedItem.prefixToken = calcRandomMod(workOrder.vendor, mbEnums.ItemModType.PREFIX, template.modTable); else forgedItem.prefixToken = workOrder.prefixToken; if (workOrder.suffixToken == 0) forgedItem.suffixToken = calcRandomMod(workOrder.vendor, mbEnums.ItemModType.SUFFIX, template.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 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 to these tokens. Writes to disk. 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); DispatchMessage.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); DispatchMessage.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); DispatchMessage.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; ++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); DispatchMessage.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 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); EffectsBase effectsBase = PowersManager.getEffectByIDString(modTableEntry.action); modifier = effectsBase.getToken(); } return modifier; } }