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>
283 lines
12 KiB
Java
283 lines
12 KiB
Java
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
|
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
|
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
|
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
|
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
|
|
// Magicbane Emulator Project © 2013 - 2026
|
|
// https://www.magicbane.com
|
|
|
|
package engine.loot;
|
|
|
|
import engine.gameManager.DbManager;
|
|
import engine.gameManager.ForgeManager;
|
|
import engine.mbEnums;
|
|
import engine.objects.Item;
|
|
import engine.objects.NPC;
|
|
import engine.objects.Warehouse;
|
|
import engine.parsers.objects.CObjectTemplate;
|
|
import org.json.JSONArray;
|
|
import org.json.JSONObject;
|
|
|
|
import java.time.Duration;
|
|
import java.time.Instant;
|
|
import java.time.LocalDateTime;
|
|
import java.time.ZoneId;
|
|
import java.util.ArrayList;
|
|
import java.util.EnumSet;
|
|
import java.util.HashMap;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.Delayed;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
public class WorkOrder implements Delayed {
|
|
|
|
// MB Dev notes:
|
|
// Class defines a Forge rolling request made through a vendor
|
|
// interaction; submitted to the ForgeManager singleton for completion.
|
|
//
|
|
// A workOrder once created will last until all items are either
|
|
// completed or junked. They are persisted in the table dyn_workorders.
|
|
|
|
public int workOrderID;
|
|
public NPC vendor;
|
|
public AtomicInteger slots_used = new AtomicInteger(0);
|
|
public int total_to_produce;
|
|
public int total_produced;
|
|
public boolean multiple_slot_request;
|
|
public HashMap<mbEnums.ResourceType, Integer> production_cost = new HashMap<>();
|
|
public HashMap<mbEnums.ResourceType, Integer> production_cost_total = new HashMap<>();
|
|
public int templateID;
|
|
public String item_name_override;
|
|
public int prefixToken;
|
|
public int suffixToken;
|
|
public long rollingDuration;
|
|
public long completionTime;
|
|
public AtomicBoolean runCompleted = new AtomicBoolean(false);
|
|
|
|
// This collection is serialized to the vendor rolling window in ManageNPCMsg.
|
|
|
|
public ConcurrentHashMap.KeySetView<Item, Boolean> cooking = ConcurrentHashMap.newKeySet();
|
|
|
|
public WorkOrder() {
|
|
|
|
}
|
|
|
|
public WorkOrder(JSONObject jsonWorkOrder) {
|
|
|
|
// This constructor is used to load workOrders from disk
|
|
// during bootstrap. (dyn_workorders)
|
|
|
|
this.workOrderID = jsonWorkOrder.getInt("workOrderID");
|
|
this.vendor = NPC.getNPC(jsonWorkOrder.getInt("vendor"));
|
|
this.slots_used.set(jsonWorkOrder.getInt("slots_used"));
|
|
this.total_to_produce = jsonWorkOrder.getInt("total_to_produce");
|
|
this.total_produced = jsonWorkOrder.getInt("total_produced");
|
|
this.multiple_slot_request = jsonWorkOrder.getBoolean("multiple_slot_request");
|
|
this.templateID = jsonWorkOrder.getInt("templateID");
|
|
this.item_name_override = jsonWorkOrder.getString("item_name_override");
|
|
this.prefixToken = jsonWorkOrder.getInt("prefixToken");
|
|
this.suffixToken = jsonWorkOrder.getInt("suffixToken");
|
|
this.rollingDuration = jsonWorkOrder.getLong("rollingDuration");
|
|
this.completionTime = jsonWorkOrder.getLong("completionTime");
|
|
this.runCompleted.set(jsonWorkOrder.getBoolean("runCompleted"));
|
|
|
|
// Vendor sanity check. Might have been deleted
|
|
|
|
if (this.vendor == null)
|
|
return;
|
|
|
|
JSONObject productionCostMap = jsonWorkOrder.getJSONObject("production_cost");
|
|
|
|
for (String key : productionCostMap.keySet()) {
|
|
mbEnums.ResourceType resourceType = mbEnums.ResourceType.valueOf(key);
|
|
int value = productionCostMap.getInt(key);
|
|
this.production_cost.put(resourceType, value);
|
|
}
|
|
|
|
JSONObject productionTotalCostMap = jsonWorkOrder.getJSONObject("production_cost_total");
|
|
|
|
for (String key : productionTotalCostMap.keySet()) {
|
|
mbEnums.ResourceType resourceType = mbEnums.ResourceType.valueOf(key);
|
|
int value = productionTotalCostMap.getInt(key);
|
|
this.production_cost_total.put(resourceType, value);
|
|
}
|
|
|
|
// Reconstruct cooking items
|
|
|
|
JSONArray tokenList = jsonWorkOrder.getJSONArray("cookingTokens");
|
|
|
|
for (Object o : tokenList) {
|
|
int prefix = ((JSONArray) o).getInt(0);
|
|
int suffix = ((JSONArray) o).getInt(1);
|
|
|
|
Item cookingItem = ForgeManager.forgeItem(this);
|
|
cookingItem.prefixToken = prefix;
|
|
cookingItem.suffixToken = suffix;
|
|
cookingItem.setDateToUpgrade(this.completionTime);
|
|
}
|
|
}
|
|
|
|
public static int validate(WorkOrder workOrder) {
|
|
|
|
// Validate that a workOrder can be completed by both
|
|
// the vendor and the forge.
|
|
|
|
int validation_result = 0;
|
|
|
|
CObjectTemplate template = CObjectTemplate.lookup(workOrder.templateID);
|
|
|
|
if (workOrder.vendor.getBuilding() == null)
|
|
return 58; //58: The formula is beyond the means of this facility
|
|
|
|
if (!workOrder.vendor.charItemManager.hasRoomInventory(template.getItemWt()))
|
|
return 30; //30: That person cannot carry that item
|
|
|
|
if ((workOrder.prefixToken != 0 || workOrder.suffixToken != 0) &&
|
|
!workOrder.vendor.getItemModTable().contains(ForgeManager.getVendorModTable(workOrder.templateID)))
|
|
return 59; //59: This hireling does not have this formula
|
|
|
|
if (!Warehouse.calcCostOverrun(workOrder).isEmpty())
|
|
return 10; //18: You can't really afford that
|
|
|
|
// Forge must be protected in order to access warehouse.
|
|
|
|
if (ForgeManager.calcProductionCost(workOrder).size() > 1)
|
|
if (!EnumSet.of(mbEnums.ProtectionState.PROTECTED, mbEnums.ProtectionState.CONTRACT).contains(workOrder.vendor.building.protectionState))
|
|
return 193; //193: Production denied: This building must be protected to gain access to warehouse
|
|
|
|
return validation_result;
|
|
}
|
|
|
|
public static boolean withdrawWorkOrderCost(WorkOrder workOrder) {
|
|
|
|
HashMap<mbEnums.ResourceType, Integer> modified_production_cost = new HashMap<>(workOrder.production_cost_total);
|
|
|
|
if (workOrder.vendor.building.getCity() == null)
|
|
return false;
|
|
|
|
int strongbox = workOrder.vendor.building.getStrongboxValue();
|
|
|
|
// Early exit if strongbox can cover gold only roll.
|
|
|
|
if (workOrder.production_cost_total.size() == 1 && workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD) <= strongbox) {
|
|
workOrder.vendor.building.setStrongboxValue(strongbox - workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD));
|
|
return true;
|
|
}
|
|
|
|
// Warehouse is required after this point
|
|
|
|
Warehouse warehouse = workOrder.vendor.building.getCity().warehouse;
|
|
|
|
if (warehouse == null)
|
|
return false;
|
|
|
|
// Take from strongbox first and deduct from production cost total
|
|
|
|
if (workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD) <= strongbox) {
|
|
workOrder.vendor.building.setStrongboxValue(strongbox - workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD));
|
|
modified_production_cost.put(mbEnums.ResourceType.GOLD, 0);
|
|
} else {
|
|
int overflowAmount = workOrder.production_cost_total.get(mbEnums.ResourceType.GOLD) - strongbox;
|
|
workOrder.vendor.building.setStrongboxValue(0);
|
|
modified_production_cost.put(mbEnums.ResourceType.GOLD, overflowAmount);
|
|
}
|
|
|
|
// Deduct total production cost from warehouse
|
|
|
|
modified_production_cost.forEach((key, value) -> warehouse.resources.put(key, warehouse.resources.get(key) - value));
|
|
DbManager.WarehouseQueries.UPDATE_WAREHOUSE(warehouse);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
public static JSONObject toJson(WorkOrder workOrder) {
|
|
|
|
// Workorders are persisted in JSON format.
|
|
|
|
JSONObject jsonWorkOrder = new JSONObject();
|
|
|
|
jsonWorkOrder.put("workOrderID", workOrder.workOrderID);
|
|
jsonWorkOrder.put("vendor", workOrder.vendor.getObjectUUID());
|
|
jsonWorkOrder.put("slots_used", workOrder.slots_used.get());
|
|
jsonWorkOrder.put("total_to_produce", workOrder.total_to_produce);
|
|
jsonWorkOrder.put("total_produced", workOrder.total_produced);
|
|
jsonWorkOrder.put("multiple_slot_request", workOrder.multiple_slot_request);
|
|
jsonWorkOrder.put("production_cost", workOrder.production_cost);
|
|
jsonWorkOrder.put("production_cost_total", workOrder.production_cost_total);
|
|
jsonWorkOrder.put("templateID", workOrder.templateID);
|
|
jsonWorkOrder.put("item_name_override", workOrder.item_name_override);
|
|
jsonWorkOrder.put("prefixToken", workOrder.prefixToken);
|
|
jsonWorkOrder.put("suffixToken", workOrder.suffixToken);
|
|
jsonWorkOrder.put("rollingDuration", workOrder.rollingDuration);
|
|
jsonWorkOrder.put("completionTime", workOrder.completionTime);
|
|
jsonWorkOrder.put("runCompleted", workOrder.runCompleted.get());
|
|
|
|
ArrayList<Integer[]> cookingTokens = new ArrayList<>();
|
|
|
|
for (Item item : workOrder.cooking)
|
|
cookingTokens.add(new Integer[]{item.prefixToken, item.suffixToken});
|
|
|
|
jsonWorkOrder.put("cookingTokens", cookingTokens);
|
|
|
|
return jsonWorkOrder;
|
|
}
|
|
|
|
public String toString() {
|
|
|
|
LocalDateTime localDateTime = Instant.ofEpochMilli(this.completionTime)
|
|
.atZone(ZoneId.systemDefault()).toLocalDateTime();
|
|
Duration duration = Duration.ofMillis(this.rollingDuration);
|
|
|
|
String outSTring = "\r\nwordOrderID: " + this.workOrderID + "\r\n" +
|
|
"vendor: " + this.vendor.getObjectUUID() + "\r\n" +
|
|
"slots_used: " + this.slots_used.get() + "\r\n" +
|
|
"total_to_produce: " + this.total_to_produce + "\r\n" +
|
|
"total_produced: " + this.total_produced + "\r\n" +
|
|
"templateID: " + this.templateID + "\r\n" +
|
|
"item_name_override: " + this.item_name_override + "\r\n" +
|
|
"prefixToken: " + this.prefixToken + "\r\n" +
|
|
"suffixToken: " + this.suffixToken + "\r\n" +
|
|
"rollingDuration: " + duration + "\r\n" +
|
|
"completionTime: " + localDateTime + "\r\n" +
|
|
"runCompleted: " + this.runCompleted.get() + "\r\n" +
|
|
"productionCost: " + this.production_cost.toString() + "\r\n" +
|
|
"totalProductionCost:: " + this.production_cost_total.toString();
|
|
|
|
return outSTring;
|
|
}
|
|
|
|
@Override
|
|
public long getDelay(TimeUnit unit) {
|
|
long timeRemaining = completionTime - System.currentTimeMillis();
|
|
return unit.convert(timeRemaining, TimeUnit.MILLISECONDS);
|
|
}
|
|
|
|
@Override
|
|
public int compareTo(Delayed o) {
|
|
return Long.compare(this.completionTime, ((WorkOrder) o).completionTime);
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
|
|
if (this == obj)
|
|
return true;
|
|
|
|
if (obj == null || getClass() != obj.getClass())
|
|
return false;
|
|
|
|
WorkOrder otherWorkOrder = (WorkOrder) obj;
|
|
return workOrderID == otherWorkOrder.workOrderID;
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(workOrderID);
|
|
}
|
|
|
|
}
|