// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // 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.*; import engine.powers.EffectsBase; import engine.powers.poweractions.AbstractPowerAction; import org.pmw.tinylog.Logger; import java.util.ArrayList; import java.util.HashMap; import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.DelayQueue; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public enum ForgeManager implements Runnable { FORGE_MANAGER; private static final BlockingQueue forge = new DelayQueue<>(); public static final AtomicInteger wordOrderCounter = new AtomicInteger(0); public static final HashMap inMemoryItemLookup = new HashMap<>(); public static final HashMap> vendorItemLookup = new HashMap<>(); public static final HashMap> vendorWorkOrderLookup = new HashMap<>(); @Override public void run() { WorkOrder workOrder = null; while (true) { try { workOrder = forge.take(); } catch (Exception e) { Logger.error(e); } // Completed or canceled work orders are not re-enqueued if (Objects.requireNonNull(workOrder).runCanceled || workOrder.runCompleted) continue; // Persist current items that are currently cooking for this // workOrder ArrayList toRemove = new ArrayList<>(); Item completed_item = null; for (Item cooked_item : workOrder.cooking) { toRemove.add(cooked_item); cooked_item.containerType = mbEnums.ItemContainerType.INVENTORY; completed_item = DbManager.ItemQueries.PERSIST(cooked_item); workOrder.vendor.charItemManager.addItemToInventory(completed_item); } // Remove the fake negativeID item from all collections. for (Item memoryItem : toRemove) { workOrder.cooking.remove(memoryItem); inMemoryItemLookup.remove(memoryItem.objectUUID); vendorItemLookup.get(workOrder.vendor).remove(memoryItem); } // Update vendor window ItemProductionMsg outMsg = new ItemProductionMsg(workOrder.vendor.building, workOrder.vendor, completed_item, 8, true); DispatchMessage.dispatchMsgToInterestArea(workOrder.vendor, outMsg, mbEnums.DispatchChannel.SECONDARY, 700, false, false); Logger.info("item forged:" + workOrder.workOrderID + " (" + workOrder.total_produced + "/" + workOrder.total_to_produce + ")"); if (workOrder.total_produced >= workOrder.total_to_produce) { Logger.info("workOrder has completed: " + workOrder.workOrderID); workOrder.runCompleted = true; continue; } // Create new set in memory items forgeItems(workOrder); // enQueue this workOrder again; back into the oven // until all items for this workOrder are completed. workOrder.completionTime = System.currentTimeMillis() + workOrder.rollingDuration; forge.add(workOrder); Logger.info(workOrder.toString()); } } public static void start() { Thread forgeManager; forgeManager = new Thread(FORGE_MANAGER); forgeManager.setName("Forge Manager"); forgeManager.start(); } public static int submit(WorkOrder workOrder) { int validation_result = ForgeManager.validate(workOrder); if (validation_result != 0) return validation_result; workOrder.workOrderID = wordOrderCounter.incrementAndGet(); workOrder.rollingDuration = ForgeManager.calcRollingDuration(workOrder); workOrder.completionTime = System.currentTimeMillis() + workOrder.rollingDuration; workOrder.slots_used = calcAvailableSlots(workOrder); // Cost to execute this workOrder workOrder.production_cost = calcProductionCost(workOrder); // Set total cost for this production run workOrder.production_cost_total.putAll(workOrder.production_cost); workOrder.production_cost_total.forEach((key, value) -> workOrder.production_cost_total.compute(key, (k, v) -> v * workOrder.total_to_produce)); workOrder.total_to_produce *= workOrder.slots_used; // Create in-memory items and add to collections forgeItems(workOrder); Logger.info(workOrder.toString()); forge.add(workOrder); return validation_result; } public static int validate(WorkOrder workOrder) { int validation_result = 0; ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID); if (!workOrder.vendor.charItemManager.hasRoomInventory(template.item_wt)) return 30; //30: That person cannot carry that item if (!workOrder.vendor.getItemModTable().contains((template.modTable))) return 59; //59: This hireling does not have this formula if (!calcCostOverrun(workOrder).isEmpty()) return 10; //18: You can't really afford that // Forge must be protected in order to access warehouse. if (calcProductionCost(workOrder).size() > 1) if (!workOrder.vendor.building.protectionState.equals(mbEnums.ProtectionState.PROTECTED)) return 193; //193: Production denied: This building must be protected to gain access to warehouse 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(); // Slots currently used up by the npc workOrders for (WorkOrder npcWorkOrder : ForgeManager.vendorWorkOrderLookup.get(workOrder.vendor)) availableSlots = availableSlots - npcWorkOrder.slots_used; // Single item rolls are msg_size of 0; if (availableSlots > 0 && workOrder.multiple_slot_request == 0) availableSlots = 1; return availableSlots; } public static HashMap calcProductionCost(WorkOrder workOrder) { // Calculate the production cost for a single run of this 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); EffectsBase prefixValue = PowersManager.getEffectByIDString(prefix.getIDString() + 'A'); production_cost.putAll(prefixValue.getResourcesForEffect()); } if (workOrder.suffixToken != 0) { EffectsBase suffix = PowersManager.getEffectByToken(workOrder.suffixToken); EffectsBase suffixValue = PowersManager.getEffectByIDString(suffix.getIDString() + 'A'); production_cost.putAll(suffixValue.getResourcesForEffect()); } return production_cost; } public static HashMap calcCostOverrun(WorkOrder workOrder) { HashMap costMap = new HashMap<>(); Warehouse warehouse; // See if we can meet gold only requirements and early exit if (workOrder.production_cost_total.size() == 1) { if (workOrder.vendor.building.getStrongboxValue() > workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD)) return costMap; } // Gold deficit exists so a warehouse is required warehouse = workOrder.vendor.building.getCity() == null ? null : workOrder.vendor.building.getCity().warehouse; if (warehouse == null) return workOrder.production_cost_total; // Method returns a map of resourceType that a transaction overdrafts. HashMap overflowMap = new HashMap<>(); for (mbEnums.ResourceType resourceType : workOrder.production_cost_total.keySet()) { int debit = warehouse.resources.get(resourceType) - workOrder.production_cost_total.get(resourceType); // Locked resources are always unavailable if (debit < 0 || warehouse.locked.contains(resourceType)) overflowMap.put(resourceType, debit); } return overflowMap; } public static Item forgeItem(WorkOrder workOrder) { ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID); Item forgedItem = new Item(workOrder.templateID); forgedItem.objectUUID = MobLoot.lastNegativeID.getAndDecrement(); forgedItem.containerType = mbEnums.ItemContainerType.FORGE; forgedItem.ownerID = workOrder.vendor.getObjectUUID(); forgedItem.setDateToUpgrade(workOrder.completionTime); // Give 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; // Forged random rolled items are unidentified until completed if (workOrder.prefixToken == 0 && workOrder.suffixToken == 0) forgedItem.flags.remove(mbEnums.ItemFlags.Identified); return forgedItem; } public static void forgeItems(WorkOrder workOrder) { for (int i = 0; i < workOrder.slots_used; ++i) { Item forged_item = forgeItem(workOrder); // Add item to in-memory caches workOrder.cooking.add(forged_item); inMemoryItemLookup.put(forged_item.objectUUID, forged_item); vendorItemLookup.computeIfAbsent(workOrder.vendor, k -> new ArrayList<>()); vendorItemLookup.get(workOrder.vendor).add(forged_item); vendorItemLookup.get(workOrder.vendor).add(forged_item); vendorWorkOrderLookup.get(workOrder.vendor).add(workOrder); // Update NPC window ItemProductionMsg outMsg = new ItemProductionMsg(workOrder.vendor.building, workOrder.vendor, forged_item, 8, true); DispatchMessage.dispatchMsgToInterestArea(workOrder.vendor, outMsg, mbEnums.DispatchChannel.SECONDARY, 700, false, false); Logger.info("Forging item: " + forged_item.objectUUID + " (" + forged_item.templateID + ") " + forged_item.template.item_base_name); workOrder.total_produced = workOrder.total_produced + 1; } } public static int calcRandomMod(NPC vendor, mbEnums.ItemModType itemModType, int modTable) { int modifier = 0; ModTypeTableEntry modTypeTableEntry = null; ModTableEntry modRollEntry = null; int modifierRoll; switch (itemModType) { case PREFIX: modifier = vendor.getModTypeTable().get(vendor.getItemModTable().indexOf(modTable)); modTypeTableEntry = ModTypeTableEntry.rollTable(modifier, ThreadLocalRandom.current().nextInt(1, 100 + 1)); break; case SUFFIX: modifier = vendor.getModSuffixTable().get(vendor.getItemModTable().indexOf(modTable)); modTypeTableEntry = ModTypeTableEntry.rollTable(modifier, ThreadLocalRandom.current().nextInt(1, 100 + 1)); break; } if (modTypeTableEntry == null) return 0; modifierRoll = ThreadLocalRandom.current().nextInt(1, 100 + 1); if (modifierRoll < 80) { modifierRoll = LootManager.TableRoll(vendor.getLevel(), false); modRollEntry = ModTableEntry.rollTable(modTypeTableEntry.modTableID, modifierRoll); } if (modRollEntry != null) { AbstractPowerAction abstractPowerAction = PowersManager.getPowerActionByIDString(modRollEntry.action); modifier = abstractPowerAction.getEffectsBase().getToken(); } return modifier; } }