require("company.nut"); // Cargo description. class Cargo { index = null; ///< Index of the cargo in the cargo table (#BusyBeeClass.cargoes). cid = null; ///< Id of the cargo in GRF. freight = null; ///< Whether the cargo is considered to be freight. effect = null; ///< Town effect (One of #GSCargo.TownEffect) constructor(index, cid, freight, effect) { this.index = index; this.cid = cid; this.freight = freight; this.effect = effect; } } class BusyBeeClass extends GSController { cargoes = null; // Cargoes of the game (index -> 'cid' number, 'freight' boolean, 'effect' on town). num_cargoes = 0; companies = null; function Start(); } // Examine and store cargo types of the game. function BusyBeeClass::ExamineCargoes() { this.cargoes = {}; this.num_cargoes = 0; for (local cid = 0; cid < 32; cid += 1) { if (!GSCargo.IsValidCargo(cid)) continue; local cargo = Cargo(this.num_cargoes, cid, GSCargo.IsFreight(cid), GSCargo.GetTownEffect(cid)); this.cargoes[this.num_cargoes] <- cargo; this.num_cargoes += 1; } } // Find cargo sources. // @param cargo_id Cargo index (index in this.cargoes). // @return List of resources that produce the requested cargo, list of // 'ind' or 'town' number, 'prod' produced amount, 'transp' transported amount, and 'loc' location. function BusyBeeClass::FindSources(cargo_id) { local cargo = this.cargoes[cargo_id]; local num_sources = 0; local sources = {}; if (cargo.freight) { // For 'freight' cargoes, check the producing industries for sufficient available production. foreach (ind, _ in GSIndustryList_CargoProducing(cargo.cid)) { local prod_amount = GSIndustry.GetLastMonthProduction(ind, cargo.cid); if (prod_amount < 10) continue; local transp_amount = GSIndustry.GetLastMonthTransported(ind, cargo.cid); if (prod_amount - transp_amount < 10) continue; local loc = GSIndustry.GetLocation(ind); sources[num_sources] <- {ind=ind, prod=prod_amount, transp=transp_amount, loc=loc}; num_sources += 1; } } if (cargo.effect != GSCargo.TE_NONE) { // For 'town effect' cargoes, check the towns for sufficient available production. foreach (town, _ in GSTownList()) { local prod_amount = GSTown.GetLastMonthProduction(town, cargo.cid); if (prod_amount < 10) continue; local transp_amount = GSTown.GetLastMonthSupplied(town, cargo.cid); if (prod_amount - transp_amount < 10) continue; local loc = GSTown.GetLocation(town); sources[num_sources] <- {town=town, prod=prod_amount, transp=transp_amount, loc=loc}; num_sources += 1; } } // Dump sources for a cargo. // GSLog.Info("Sources for " + GSCargo.GetCargoLabel(cargo.cid)); // foreach (_, src in sources) { // if ("ind" in src) { // GSLog.Info("Industry " + GSIndustry.GetName(src.ind) + " produces " + src.prod); // } else { // GSLog.Info("Town " + GSTown.GetName(src.town) + " produces " + src.prod); // } // } // GSLog.Info(""); return sources; } // Find destinations for the cargo. // @param cargo_id Cargo index (index in this.cargoes). // @param company Company to inspect. // @return A list of destinations, tables 'ind' or 'town' id, and a 'loc' location. function BusyBeeClass::FindDestinations(cargo_id, company) { local cargo = this.cargoes[cargo_id]; local num_dests = 0; local dests = {}; if (cargo.freight) { // Assume all industries are willing to accept the cargo. foreach (ind, _ in GSIndustryList_CargoAccepting(cargo.cid)) { local loc = GSIndustry.GetLocation(ind); dests[num_dests] <- {ind=ind, loc=loc}; num_dests += 1; } } if (cargo.effect != GSCargo.TE_NONE) { // Find towns with sufficient rating. local acceptable_ratings = [ GSTown.TOWN_RATING_MEDIOCRE, GSTown.TOWN_RATING_GOOD, GSTown.TOWN_RATING_VERY_GOOD, GSTown.TOWN_RATING_EXCELLENT, GSTown.TOWN_RATING_OUTSTANDING, GSTown.TOWN_RATING_INVALID ]; foreach (town, _ in GSTownList()) { local rating = GSTown.GetRating(town, company); if (rating in acceptable_ratings) { local loc = GSTown.GetLocation(town); dests[num_dests] <- {town=town, loc=loc}; num_dests += 1; } } } return dests; } // Construct a score for the distance // @param desired Desired distance. // @param actual Actual distance. // @return Score for the distance. function BusyBeeClass::GetDistanceScore(desired, actual) { if (actual < desired) return 1000 - 3 * (desired - actual); // Too close gets punished hard. return 1000 - (actual - desired); } // Try to find a challenge for a given cargo and a desired distance. // @param cargo Cargo entry from BusyBeeClass.cargoes (#Cargo instance). // @param distance Desired distance between source and target. // @param cid Company to find a challenge for. // @return Best accepting industry to use, or 'null' if no industry-pair found. function BusyBeeClass::FindChallenge(cargo_id, distance, cid) { local prods = this.FindSources(cargo_id); if (prods.len() == 0) return null; local accepts = this.FindDestinations(cargo_id, cid); if (accepts.len() == 0) return null; local cdata = this.companies[cid]; local best_score = 0; // Best overall distance. local best_accept = null; // Best accepting to target. foreach (_, accept in accepts) { if (cdata != null && cdata.HasGoal(cargo_id, accept)) continue; // Prevent duplicates. local min_prod_distance = distance * 2; // Smallest found distance to the accepting industry. local prod_score = best_score; foreach (_, prod in prods) { local actual_dist = GSTile.GetDistanceManhattanToTile(accept.loc, prod.loc); if (actual_dist > distance * 2) continue; // Too far away, skip. if (actual_dist < min_prod_distance) { min_prod_distance = actual_dist; prod_score = this.GetDistanceScore(distance, min_prod_distance); if (min_prod_distance < distance && prod_score < best_score) break; } } if (prod_score > best_score) { // The accepting industry is better than what we have. prod_score = best_score; best_accept = accept; } } return best_accept; } // Try to add a goal for a company. function BusyBeeClass::CreateChallenge(cid) { local cdata = this.companies[cid]; for (local attempt = 0;attempt < 20; attempt += 1) { local cargo = GSBase.RandRange(this.num_cargoes); if (cdata.GetNumberOfGoalsForCargo(cargo) > 1) continue; // Already 2 goals for this cargo. local distance = GSBase.RandRange(200) + 50; // Distance 50 .. 250 tiles. local accept = FindChallenge(cargo, distance, cid); if (accept != null) { local amount = GSBase.RandRange(100) + 1; if (amount < 10) { amount = amount * 25; // 25 .. 225 } else if (amount < 10 + 35) { amount = 10 * 25 + (amount - 10) * 50; // 250..1950 } else { amount = 10 * 25 + 35 * 50 + (amount - 10 - 35) * 100; // 2000..7500 } if (cdata != null) { cdata.AddActiveGoal(cargo, accept, amount, this.cargoes); local destination_name; if ("town" in accept) { destination_name = GSTown.GetName(accept.town); } else { destination_name = GSIndustry.GetName(accept.ind); } GSLog.Info("Company " + cid + ": " + amount + " of " + GSCargo.GetCargoLabel(this.cargoes[cargo].cid) + " to " + destination_name); break; } } } } // Process events that arrived. // @return Table with 'force_goal' bool to force goal updating. function BusyBeeClass::ProcessEvents() { local force_goal = false; while (GSEventController.IsEventWaiting()) { local event = GSEventController.GetNextEvent(); local event_type = event.GetEventType(); if (event_type == GSEvent.ET_COMPANY_NEW) { local comp_id = GSEventCompanyNew.Convert(event).GetCompanyID(); if (this.companies[comp_id] == null) { this.companies[comp_id] = CompanyData(comp_id); GSLog.Info("Created company " + comp_id + " (newly started)."); force_goal = true; } } else if (event_type == GSEvent.ET_COMPANY_MERGER) { local comp_id = GSEventCompanyMerger.Convert(event).GetOldCompanyID(); if (this.companies[comp_id] != null) { this.companies[comp_id].FinalizeCompany(); this.companies[comp_id] = null; GSLog.Info("Deleted company " + comp_id + " (due to merging)."); } } else if (event_type == GSEvent.ET_COMPANY_BANKRUPT) { local comp_id = GSEventCompanyBankrupt.Convert(event).GetCompanyID(); if (this.companies[comp_id] != null) { this.companies[comp_id].FinalizeCompany(); this.companies[comp_id] = null; GSLog.Info("Deleted company " + comp_id + " (no monies anymore)."); } } else if (event_type == GSEvent.ET_INDUSTRY_CLOSE) { local ind_id = GSEventIndustryClose.Convert(event).GetIndustryID(); foreach (comp_id, cdata in companies) { if (cdata != null) cdata.IndustryClosed(ind_id); } } } return {force_goal=force_goal}; } // Check if new goals should be created. // @return Table with 'more_goals_needed' boolean, and 'force_monitor' to force updating. function BusyBeeClass::TryAddNewGoal() { local force_monitor=false; local total_missing = 0; // Total number of missing goals. local best_comp_id = null; local comp_id_missing = 0; foreach (comp_id, cdata in companies) { if (cdata == null) continue; local missing = cdata.GetMissingGoalCount(); total_missing += missing; // Find company with most missing goals. if (missing > comp_id_missing) { best_comp_id = comp_id; comp_id_missing = missing; } } if (best_comp_id != null) { this.CreateChallenge(best_comp_id); force_monitor = true; // Force updating of the monitor to get the monitoring switched on. } return {force_monitor=force_monitor, more_goals_needed=(best_comp_id != null && total_missing > 1)}; } // Update progress on existing monitored goals, add monitoring for new goals, and drop monitors for removed goals. // @param old_cmon Monitored deliveries (or 'null' if first call). // @return Table 'cmon' with the new monitored deliveries, 'finished_goals' boolean when there exist goals that are completed. function BusyBeeClass::UpdateDeliveries(old_cmon) { if (old_cmon == null) { // First run, clear any old monitoring. GSCargoMonitor.StopAllMonitoring(); old_cmon = {}; } local cmon = {}; // Collect monitors that are of interest. foreach (comp_id, cdata in companies) { if (cdata == null) continue; cdata.AddMonitorElements(cmon); } this.FillMonitors(cmon); // Query the monitors for new deliveries. // Distribute the retrieved data. local finished = false; foreach (comp_id, cdata in companies) { if (cdata == null) continue; if (cdata.UpdateDelivereds(cmon)) finished = true; } // Drop obsolete monitors. this.UpdateCompanyMonitors(old_cmon, cmon); return {cmon=cmon, finished_goals=finished}; } function BusyBeeClass::Start() { this.Sleep(1); // Wait for the game to start. this.ExamineCargoes(); // Construct empty companies. this.companies = {}; for (local cid = GSCompany.COMPANY_FIRST; cid <= GSCompany.COMPANY_LAST; cid++) { this.companies[cid] <- null; } // Main event loop. local new_goal_timeout = 0; local finished_timeout = 0; local monitor_timeout = 0; local cmonitor = null; while (true) { local result = this.ProcessEvents(); if (result.force_goal) new_goal_timeout = 0; // Check for having to create new goals. if (new_goal_timeout <= 0) { local result = this.TryAddNewGoal(); if (result.force_monitor) monitor_timeout = 0; if (result.more_goals_needed) { new_goal_timeout = 1 * 74; } else { new_goal_timeout = 30 * 74; } } // Monitoring and updating of company goals. Note that code above may force an update. if (monitor_timeout <= 0) { local result = this.UpdateDeliveries(cmonitor); cmonitor = result.cmon; if (result.finished_goals) finished_timeout = 0; monitor_timeout = 15 * 74; // By default, check monitors every 15 days (other processes may force a check earlier). } // Check for finished goals, and remove them if they exist. if (finished_timeout <= 0) { foreach (cid, cdata in companies) { if (cdata == null) continue; cdata.CheckAndFinishGoals(); } finished_timeout = 30 * 74; // By default, check for finished goals every 30 days (may be forced by other processes). } // local lake_news = GSText(GSText.STR_LAKE_NEWS); // GSNews.Create(GSNews.NT_GENERAL, lake_news, GSCompany.COMPANY_INVALID); // GSGoal.Question(1, GSCompany.COMPANY_INVALID, lake_news, GSGoal.QT_INFORMATION, GSGoal.BUTTON_GO); // Sleep until the next event. local delay_time = 5 * 74; // Check events every 2 days. if (delay_time > new_goal_timeout) delay_time = new_goal_timeout; if (delay_time > monitor_timeout) delay_time = monitor_timeout; if (delay_time > finished_timeout) delay_time = finished_timeout; if (delay_time > 0) this.Sleep(delay_time); new_goal_timeout -= delay_time; monitor_timeout -= delay_time; finished_timeout -= delay_time; // Update timeout of the goals as well. if (!GSGame.IsPaused()) { foreach (comp_id, cdata in companies) { if (cdata == null) continue; cdata.UpdateTimeout(delay_time); } } } } // Fill company monitors with monitored amounts. // @param [inout] cmon Table of 'comp_id' number to 'cargo_id' number to // 'ind' and/or 'town' to resource indices to 'null'. function BusyBeeClass::FillMonitors(cmon) { foreach (comp_id, mon in cmon) { foreach (cargo_id, rmon in mon) { if ("ind" in rmon) { foreach (ind_id, _ in rmon.ind) { local amount = GSCargoMonitor.GetIndustryDeliveryAmount(comp_id, cargo_id, ind_id, true); rmon.ind[ind_id] = amount; // Dump received amount of cargo of an industry. // local text = "Industry " + GSIndustry.GetName(ind_id) + " received " + amount + // " units for company " + comp_id + // ", cargo " + GSCargo.GetCargoLabel(cargo_id); // GSLog.Info(text); } } if ("town" in rmon) { foreach (town_id, _ in rmon.town) { local amount = GSCargoMonitor.GetTownDeliveryAmount(comp_id, cargo_id, town_id, true); rmon.town[town_id] = amount; // Dump received amount of cargo of a town. // local text = "Town " + GSTown.GetName(town_id) + " received " + amount + // " units for company " + comp_id + // ", cargo " + GSCargo.GetCargoLabel(cargo_id); // GSLog.Info(text); } } } } } function BusyBeeClass::UpdateCompanyMonitors(old_cmon, cmon) { foreach (comp_id, old_mon in old_cmon) { if (comp_id in cmon) { this.UpdateCargoMonitors(comp_id, old_mon, cmon[comp_id]); } else { this.UpdateCargoMonitors(comp_id, old_mon, {}); } } } function BusyBeeClass::UpdateCargoMonitors(comp_id, old_mon, mon) { foreach (cargo_id, old_rmon in old_mon) { if (cargo_id in mon) { this.UpdateResourceMonitors(comp_id, cargo_id, old_rmon, mon[cargo_id]); } else { this.UpdateResourceMonitors(comp_id, cargo_id, old_rmon, {}); } } } function BusyBeeClass::UpdateResourceMonitors(comp_id, cargo_id, old_rmon, rmon) { if ("town" in old_rmon) { if ("town" in rmon) { this.UpdateTownMonitors(comp_id, cargo_id, old_rmon.town, rmon.town); } else { this.UpdateTownMonitors(comp_id, cargo_id, old_rmon.town, {}); } } if ("ind" in old_rmon) { if ("ind" in rmon) { this.UpdateIndMonitors(comp_id, cargo_id, old_rmon.ind, rmon.ind); } else { this.UpdateIndMonitors(comp_id, cargo_id, old_rmon.ind, {}); } } } function BusyBeeClass::UpdateTownMonitors(comp_id, cargo_id, old_tmon, tmon) { foreach (town_id, _ in old_tmon) { if (!(town_id in tmon)) { GSCargoMonitor.GetTownDeliveryAmount(comp_id, cargo_id, town_id, false); local text = "Stop monitoring town " + GSTown.GetName(town_id) + " for company " + comp_id + ", cargo " + GSCargo.GetCargoLabel(cargo_id); GSLog.Info(text); } } } function BusyBeeClass::UpdateIndMonitors(comp_id, cargo_id, old_imon, imon) { foreach (ind_id, _ in old_imon) { if (!(ind_id in imon)) { GSCargoMonitor.GetIndustryDeliveryAmount(comp_id, cargo_id, ind_id, false); local text = "Stop monitoring industry " + GSIndustry.GetName(ind_id) + " for company " + comp_id + ", cargo " + GSCargo.GetCargoLabel(cargo_id); GSLog.Info(text); } } }