// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // 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 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 { FORGE_MANAGER; public static final BlockingQueue forge = new DelayQueue<>(); public static final AtomicInteger wordOrderCounter = new AtomicInteger(0); public static final ConcurrentHashMap> vendorWorkOrderLookup = new ConcurrentHashMap<>(); public static final ConcurrentHashMap itemWorkOrderLookup = new ConcurrentHashMap<>(); @Override public void run() { WorkOrder workOrder = null; while (true) { try { workOrder = forge.take(); } catch (Exception e) { Logger.error(e); } if (workOrder.total_produced >= workOrder.total_to_produce) { // Complete this workOrder. 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 to disk DbManager.WarehouseQueries.UPDATE_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()); } } public static void start() { Thread forgeManager; forgeManager = new Thread(FORGE_MANAGER); forgeManager.setName("Forge Manager"); forgeManager.start(); } public static int submit(WorkOrder workOrder) { // Make sure vendor can roll the formulae, warehouse can // afford this wordOrder and other related checks. int validation_result = ItemManager.validate(workOrder); // The return code is used by the submitter as a // popup error message for the player. if (validation_result != 0) return validation_result; // Concurrency is managed by same lock as warehouse City city = workOrder.vendor.building.getCity(); if (city == null) return 58; //58: The formula is beyond the means of this facility city.transactionLock.writeLock().lock(); try { // Configure this production run. workOrder.workOrderID = wordOrderCounter.incrementAndGet(); workOrder.rollingDuration = ForgeManager.calcRollingDuration(workOrder); workOrder.completionTime = System.currentTimeMillis() + workOrder.rollingDuration; workOrder.slots_used = calcAvailableSlots(workOrder); workOrder.total_produced = 0; workOrder.total_to_produce *= workOrder.slots_used; 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.put(key, value * workOrder.total_to_produce)); // Deduct gold cost from building if (!debitWorkOrderCost(workOrder)) return 58; //58: The formula is beyond the means of this facility // Create in-memory items and add to collections forgeWorkOrderBatch(workOrder); // Submit workOrder for next completion cycle vendorWorkOrderLookup.get(workOrder.vendor).add(workOrder); forge.add(workOrder); // Save workOrder to disk DbManager.WarehouseQueries.UPDATE_WORKORDER(workOrder); } catch (Exception e) { Logger.error(e); } finally { city.transactionLock.writeLock().unlock(); } 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 slots currently being used by npc workOrders 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 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 Item forgeItem(WorkOrder workOrder) { // Create new 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(); // The UpgradeDate for the item is serialized for the // vendor forge window 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); // 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); // Persist item Item completedItem = DbManager.ItemQueries.PERSIST(virutalItem); // Apply Item effects for Prefix and Suffix tokens completedItem.prefixToken = virutalItem.prefixToken; completedItem.suffixToken = virutalItem.suffixToken; 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); } // Remove the negativeID virtual item from all collections for (Item virtualItem : toRemove) { // Remove the 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); workOrder.cooking.remove(virtualItem); itemWorkOrderLookup.remove(virtualItem); DbManager.removeFromCache(virtualItem); } } public static void forgeWorkOrderBatch(WorkOrder workOrder) { // New completion time for this batch 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; } // Save updated status to disk DbManager.WarehouseQueries.UPDATE_WORKORDER(workOrder); } public static int calcRandomMod(NPC vendor, mbEnums.ItemModType itemModType, int modTable) { int modifier = 0; ModTypeTableEntry modTypeTableEntry = null; ModTableEntry modTableEntry = null; 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; } public static boolean debitWorkOrderCost(WorkOrder workOrder) { int strongbox = workOrder.vendor.building.getStrongboxValue(); if (workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD) > strongbox) { if (workOrder.vendor.building.getCity() == null) return false; Warehouse warehouse = workOrder.vendor.building.getCity().warehouse; if (warehouse == null) return false; // Deduct total cost from warehouse workOrder.production_cost_total.forEach((key, value) -> warehouse.resources.put(key, warehouse.resources.get(key) - value)); DbManager.WarehouseQueries.UPDATE_WAREHOUSE(warehouse); } return true; } }