import resources; import constructions; import artifacts; import int getOrbitalModuleID(const string&) from "orbitals"; //TODO: This doesn't represent the import request well // Relies on how a given planet will tend to request imports int64 importResHash(Planet@ pl, array@ res) { int64 Hash = (int64(ACT_Trade) << ACT_BIT_OFFSET) | int64(pl.id); Hash |= int64(res[0]) << 32; Hash |= int64(res.length) << 48; return Hash; } class ImportResource : Action { Planet@ dest; Object@ source; uint nextType = 0; int foundRes = -1; int[]@ resources; bool attemptExpand = false; int64 Hash; const ResourceType@ water = getResource("Water"); double attemptStart = gameTime; ImportResource(Planet@ to, int[]@ Resources) { @dest = to; @resources = Resources; Hash = importResHash(to, Resources); } ImportResource(BasicAI@ ai, SaveFile& msg) { msg >> dest >> source; msg >> foundRes >> attemptExpand >> nextType; uint cnt = 0; msg >> cnt; @resources = array(cnt); for(uint i = 0; i < cnt; ++i) resources[i] = msg.readIdentifier(SI_Resource); Hash = importResHash(dest, resources); } ~ImportResource() { if(source !is null) error("ImportResource ended without proper resolution"); } double get_age() const { return gameTime - attemptStart; } void postLoad(BasicAI@ ai) { } void save(BasicAI@ ai, SaveFile& msg) { msg << dest << source; msg << foundRes << attemptExpand << nextType; uint cnt = resources.length; msg << cnt; for(uint i = 0; i < cnt; ++i) msg.writeIdentifier(SI_Resource, resources[i]); } int64 get_hash() const { return Hash; } ActionType get_actionType() const { return ACT_Trade; } string get_state() const { if(source is null) return format("Looking for $1 (+$2 others) for $3", getResource(resources[0]).name, resources.length-1, dest.name); else return format("Importing $1 from $2 to $3", getResource(foundRes).name, source.name, dest.name); } void clear(BasicAI@ ai) { if(source !is null) { ai.markIdleGeneric(source); @source = null; } ai.removeIdle(this); } bool hasWater(BasicAI@ ai) { if(ai.planets[water.id].idle.length == 0 && ai.skillEconomy >= DIFF_Medium) { auto@ hydro = getBuildingType("Hydrogenator"); if((ai.usesMotherships ||ai.empire.RemainingBudget < hydro.baseBuildCost) || gameTime < 3.0 * 60.0) { const auto@ comet = getArtifactType("Comet"); if(ai.empire.EnergyStored < comet.abilities[0].energyCost || ai.getArtifact(comet, dest.region) is null) { return false; } } } return true; } bool hasFood(BasicAI@ ai) { const ResourceClass@ cls = getResourceClass("Food"); for(uint i = 0, cnt = cls.types.length; i < cnt; ++i) if(ai.planets[cls.types[i].id].idle.length != 0) return true; if(!ai.usesMotherships && gameTime > 3.0 * 60.0) { auto@ farm = getBuildingType("Farm"); if(ai.empire.RemainingBudget >= farm.baseBuildCost) return true; } return false; } bool canLevelPlanet(BasicAI@ ai) const { if(ai.isMachineRace) return true; return hasWater(ai) && hasFood(ai); } Planet@ findViablePlanet(BasicAI@ ai) { uint tryCount = 1; if(ai.skillEconomy >= DIFF_Hard) tryCount = resources.length; else if(ai.skillEconomy >= DIFF_Medium) tryCount = min(2, resources.length); uint checkType = randomi(0, resources.length-1); Empire@ owner = ai.empire; auto@ reg = dest.region; auto@ destTerr = reg !is null ? dest.region.getTerritory(owner) : null; if(destTerr is null) return null; //Look through a few resources for idle planets of the resources we need for(uint tries = 0; tries < tryCount; ++tries) { uint resID = resources[checkType % resources.length]; PlanetList@ planets = ai.planets[resID]; const ResourceType@ type = getResource(resID); uint cnt = planets.idle.length; for(uint i = 0; i < cnt; ++i) { Planet@ pl = planets.idle[i]; if(pl.owner !is owner || !pl.valid) continue; auto@ r = pl.region; if(r is null || r.getTerritory(owner) !is destTerr) continue; uint plLevel = pl.level; if(pl.level < type.level) continue; return pl; } checkType += 1; if(cnt != 0) break; } return null; } bool perform(BasicAI@ ai) { if(source !is null) { uint resLevel = getResource(foundRes).level; if(source.owner !is ai.empire) { foundRes = -1; @source = null; ai.removeIdle(this); } else if(!source.isPlanet || source.resourceLevel >= resLevel) { //Attempt to export if(source is dest) { //When the homeworld moves, it can end up using its own resource - this looks a bit weird but works for this purpose source.exportResource(0, null); ai.markUsedGeneric(source, source); @source = null; return true; } else { source.exportResource(0, dest); source.wait(); if(!source.isPrimaryDestination(dest)) { ai.markIdleGeneric(source); @source = null; foundRes = -1; ai.removeIdle(this); } else { ai.markUsedGeneric(source, dest); @source = null; return true; } } } else { //See if any other planets are available instead Planet@ other = findViablePlanet(ai); if(other !is null) { ai.markIdleGeneric(source); @source = other; foundRes = other.primaryResourceType; ai.markUsedGeneric(source, source); } else if(source.isPlanet && source.population >= 1.0) { //Request the planet be upgraded ai.requestPlanetImprovement(cast(source), resLevel); } } } else { //Already found the resource and ordered the export if(foundRes != -1) return true; if(nextType >= resources.length) attemptExpand = true; Empire@ owner = ai.empire; Territory@ destTerr = null; { auto@ reg = dest.region; if(reg !is null) @destTerr = reg.getTerritory(owner); if(destTerr is null) return true; } Object@ farMatch; Object@ best = null; double bestScore = 0; uint resID; bool tryBuildWater = false, tryBuildFood = false; int aiHasBaseNeeds = -1; uint tryCount = 1; if(ai.skillEconomy >= DIFF_Hard) tryCount = resources.length; else if(ai.skillEconomy >= DIFF_Medium) tryCount = min(2, resources.length); ai.focus = dest.position; //Look through a few resources for idle planets of the resources we need for(uint tries = 0; tries < tryCount; ++tries) { resID = resources[nextType % resources.length]; if(resID == water.id) { const auto@ comet = getArtifactType("Comet"); if(owner.EnergyStored >= comet.abilities[0].energyCost) { auto@ artifact = ai.getArtifact(comet, dest.region); if(artifact !is null) { artifact.activateAbilityFor(owner, 0, dest); dest.wait(); return true; } } if(!ai.usesMotherships && ai.skillEconomy >= DIFF_Medium && age > 180.0 && ai.empire.RemainingBudget >= 300) tryBuildWater = true; } PlanetList@ planets = ai.planets[resID]; const ResourceType@ type = getResource(resID); uint cnt = planets.idle.length; for(uint i = 0; i < cnt; ++i) { Planet@ pl = planets.idle[i]; if(pl.owner !is owner || !pl.valid) continue; auto@ reg = pl.region; if(reg is null) continue; if(reg.getTerritory(owner) !is destTerr) { @farMatch = pl; continue; } uint plLevel = pl.level; if(type.level > 0 && plLevel == 0 && aiHasBaseNeeds == 0) continue; double score = 1.0; if(plLevel >= type.level) score = 2.0; else if(pl.resourceLevel >= type.level) score = 1.5; if(score > bestScore) { @best = pl; bestScore = score; } } //TODO: This could probably be sorted into resources as well if(best is null) { for(uint i = 0, cnt = ai.idleAsteroids.length; i < cnt; ++i) { Asteroid@ roid = ai.idleAsteroids[i]; if(roid.primaryResourceType != resID) continue; auto@ reg = roid.region; if(reg is null) continue; if(reg.getTerritory(owner) !is destTerr) { @farMatch = roid; continue; } @best = roid; bestScore = 3.0; break; } } //Check that the AI has enough spare planets to bother aquiring the planet // Only check for Medium or harder AI early in the game // TODO: Skip check based on colonization capacity, rather than a time heuristic if(type.level >= 1 && gameTime < 30.0 * 60.0 && ai.skillEconomy >= DIFF_Medium) { if(aiHasBaseNeeds == -1) aiHasBaseNeeds = canLevelPlanet(ai) ? 1 : 0; } else if(type.level == 0 && ai.skillEconomy >= DIFF_Medium && (best is null || cnt <= 2) && age > 180.0 && type.cls is getResourceClass("Food")) { if(!ai.usesMotherships && ai.empire.RemainingBudget >= 300) tryBuildFood = true; } else if(resID == water.id && best !is null && cnt > 2) { tryBuildWater = false; } nextType += 1; if(cnt != 0) break; } if(best !is null) { foundRes = resID; @source = best; ai.markUsedGeneric(source, source); ai.addIdle(this); //We found a useful planet. //Hard AIs will go on to try to use their free money to build up food and water if(ai.skillEconomy < DIFF_Hard) return false; } else if(farMatch !is null) { //TODO: Request the connection of two territories } if(tryBuildFood) { ai.requestBuilding(dest, getBuildingType("Farm")); if(source is null) return true; } else if(tryBuildWater) { ai.requestBuilding(dest, getBuildingType("Hydrogenator")); if(source is null) return true; } else if(attemptExpand && source is null && aiHasBaseNeeds != 0) { //Request colonization of planet with a resource we need ai.colonizeByResource(resources); } else if(aiHasBaseNeeds == 0 && !ai.isMachineRace) { if(!hasWater(ai)) ai.addIdle(ai.colonizeByResource(ai.getResourceList(RT_Water, onlyExportable=true), execute=false)); if(!hasFood(ai)) ai.addIdle(ai.colonizeByResource(ai.getResourceList(RT_Food, onlyExportable=true), execute=false)); } } return false; } } int64 buildBuildingHash(Planet@ pl, const BuildingType@ bldg) { return (int64(ACT_Building) << ACT_BIT_OFFSET) | (int64(bldg.id) << 32) | int64(pl.id); } class BuildBuilding : Action { Planet@ planet; const BuildingType@ type; int64 Hash; uint tries = 0; vec2i inProgress = vec2i(-1,-1); bool force = false; BuildBuilding(Planet@ pl, const BuildingType@ bldg, bool force = false) { @planet = pl; @type = bldg; this.force = force; Hash = buildBuildingHash(pl, bldg); } int64 get_hash() const { return Hash; } ActionType get_actionType() const { return ACT_Building; } string get_state() const { return "Building " + type.name + " on " + planet.name; } BuildBuilding(BasicAI@ ai, SaveFile& msg) { uint typeID = 0; msg >> planet >> typeID >> inProgress; @type = getBuildingType(typeID); Hash = buildBuildingHash(planet, type); if(msg >= SV_0130) msg >> force; } void postLoad(BasicAI@ ai) { } void save(BasicAI@ ai, SaveFile& msg) { msg << planet << type.id << inProgress; msg << force; } bool perform(BasicAI@ ai) { if(planet is null || !planet.valid || planet.owner !is ai.empire) return true; if(ai.usesMotherships) return true; ai.focus = planet.position; if(inProgress.x >= 0) { int at = planet.getBuildingAt(inProgress.x, inProgress.y); if(at != int(type.id)) { //error(type.name + " on " + planet.name + " expected at " + inProgress + " got " + at); inProgress = vec2i(-1,-1); } else if(planet.getBuildingProgressAt(inProgress.x, inProgress.y) >= 0.999f) { @planet = null; return true; } else { //Await completion return false; } } if(force) { if(!ai.empire.canPay(type.baseBuildCost)) { if(tries++ > 20) return true; return false; } } else { if(ai.empire.RemainingBudget < type.baseBuildCost) return true; } //Try to plop it on a random tile if(tries++ < 20) { vec2i off = vec2i(type.getCenter()); bool validPos = true; vec2i pos = vec2i(randomi(0,10), randomi(0,8)); for(int x = pos.x, endx = pos.x + type.size.width; x < endx; ++x) { for(int y = pos.y, endy = pos.y + type.size.height; y < endy; ++y) { if(x < 0 || y < 0) { validPos = false; break; } int bType = planet.getBuildingAt(x - off.x,y - off.y); if(bType >= 0) { auto@ b = getBuildingType(bType); if(b !is null && !b.civilian) { validPos = false; break; } } } if(!validPos) break; } if(validPos) { planet.buildBuilding(type.id, pos); planet.wait(); inProgress = pos; } return false; } else { return true; } } } int64 improvePlanetHash(Planet@ pl, uint toLevel) { return (int64(ACT_Improve) << ACT_BIT_OFFSET) | (int64(toLevel) << 48) | int64(pl.id); } class ImprovePlanet : Action { Planet@ planet; uint level = 1; bool requested = false; int64 Hash; Action@[] requests; int64[]@ reqHashes; uint lastResState = 0xffffffff; ImprovePlanet(Planet@ pl, uint toLevel) { @planet = pl; level = toLevel; Hash = improvePlanetHash(pl, toLevel); } ImprovePlanet(BasicAI@ ai, SaveFile& msg) { msg >> planet >> level; msg >> requested >> Hash; uint cnt = 0; msg >> cnt; @reqHashes = int64[](cnt); for(uint i = 0; i < cnt; ++i) msg >> reqHashes[i]; } void postLoad(BasicAI@ ai) { for(uint i = 0, cnt = reqHashes.length; i < cnt; ++i) { Action@ act = ai.locateAction(reqHashes[i]); if(act !is null) requests.insertLast(act); } @reqHashes = null; } void save(BasicAI@ ai, SaveFile& msg) { msg << planet << level; msg << requested << Hash; uint cnt = requests.length; msg << cnt; for(uint i = 0; i < cnt; ++i) msg << requests[i].hash; } int64 get_hash() const { return Hash; } ActionType get_actionType() const { return ACT_Improve; } string get_state() const { return "Improving " + planet.name + " to level " + level; } bool hasFoods(array& avail, uint count) { if(count == 0) return true; const ResourceClass@ cls = getResourceClass("Food"); uint levelCount = count; for(uint i = 0, cnt = avail.length; i < cnt; ++i) { auto@ type = avail[i].type; if(type.cls is cls) if(--count == 0) return true; } return false; } bool hasRequirement(array& avail, uint level, uint count) { if(count == 0) return true; uint levelCount = count; for(uint i = 0, cnt = avail.length; i < cnt; ++i) { auto@ type = avail[i].type; if(type.level == level) if(--count == 0) return true; } return false; } bool perform(BasicAI@ ai) { //TODO: Doesn't this leak into the action map? // Looks like it should be killing the actions in requests // if it exits prematurely from here. if(!planet.valid || planet.owner !is ai.empire) return true; if(ai.usesMotherships && planet.Population < double(planet.maxPopulation)) { ai.requestPopulate(planet); return false; } uint plLevel = planet.resourceLevel; if(plLevel >= level) return true; ai.focus = planet.position; if(requests.length > 0 && lastResState == planet.resourceModID) { int index = randomi(0, int(requests.length)-1); if(ai.performAction(requests[index])) requests.length = 0; } else { lastResState = planet.resourceModID; //We only check against the needs for the next level, not our ultimate goal uint goingTo = plLevel + 1; requests.length = 0; auto@ reg = planet.region; auto@ destTerr = reg is null ? null : reg.getTerritory(ai.empire); array avail; avail.syncFrom(planet.getAllResources()); //Remove all resources that aren't usable or won't be soon for(int i = int(avail.length) - 1; i > 0; --i) { auto@ res = avail[i]; if(!res.usable) { auto@ src = res.origin; auto@ srcReg = src.region; if(src !is planet && (src.owner !is ai.empire || srcReg is null || srcReg.getTerritory(ai.empire) !is destTerr || (src.isPlanet && src.resourceLevel < res.type.level))) { Planet@ source = cast(src); if(source !is null) { source.exportResource(ai.empire, 0, null); ai.markPlanetIdle(source); } avail.removeAt(i); } } } //Look for all the resources we still need, and queue up a series of requests that can be completed simultaneously bool needWater = true; if(!ai.isMachineRace) { array@ waterNeed = ai.getResourceList(RT_Water); for(uint i = 0, cnt = avail.length; i < cnt; ++i) { if(waterNeed.find(avail[i].type.id) > 0) { needWater = false; break; } } } if(goingTo > 2) { uint need = 0; switch(goingTo) { case 3: need = 1; break; case 4: need = 2; break; case 5: need = 4; break; } if(!hasRequirement(avail, 2, need)) { requests.insertLast(ai.requestImport(planet, ai.getResourceList(RT_LevelTwo, true), execute=false)); return false; } } bool requestLowerResources = gameTime > 15.0 * 60.0; if(goingTo > 1) { uint need = 0; switch(goingTo) { case 2: need = 1; break; case 3: need = ai.isFrugal ? 1 : 2; break; case 4: need = ai.isFrugal ? 2 : 4; break; case 5: need = ai.isFrugal ? 4 : 6; break; } if(!hasRequirement(avail, 1, need)) { requests.insertLast(ai.requestImport(planet, ai.getResourceList(RT_LevelOne, true), execute=false)); if(!requestLowerResources) return false; } } if(!ai.isMachineRace) { if(needWater) requests.insertLast(ai.requestImport(planet, ai.getResourceList(RT_Water, true), execute=false)); if(!hasFoods(avail, goingTo)) requests.insertLast(ai.requestImport(planet, ai.getResourceList(RT_Food, true), execute=false)); } } return false; } } int64 budgetHash() { return int64(ACT_Budget) << ACT_BIT_OFFSET; } class GatherBudget : Action { Planet@ hw; GatherBudget() { } GatherBudget(BasicAI@ ai, SaveFile& msg) { if(msg < SV_0024) { int64 dummy = 0; msg >> dummy; } if(msg >= SV_0114) { msg >> hw; } } void postLoad(BasicAI@ ai) { } void save(BasicAI@ ai, SaveFile& msg) { msg << hw; } int64 get_hash() const { return budgetHash(); } ActionType get_actionType() const { return ACT_Budget; } string get_state() const { return "Increasing available budget"; } Planet@ getHomeworld(BasicAI@ ai) { Planet@ hw = ai.homeworld; if(hw !is null && hw.owner is ai.empire && hw.valid) return hw; for(int t = 3; t >= 0; --t) { AIResourceType type; switch(t) { case 3: type = RT_LevelThree; break; case 2: type = RT_LevelTwo; break; case 1: type = RT_LevelOne; break; case 0: type = RT_Food; break; } auto@ resources = ai.getResourceList(type, onlyExportable = false); uint base = randomi(0, 250); for(uint r = 0, rcnt = resources.length; r < rcnt; ++r) { auto@ planets = ai.planets[resources[(r+base) % rcnt]].idle; for(uint i = 0, cnt = planets.length; i < cnt; ++i) { auto@ pl = planets[i]; if(pl.valid && pl.owner is ai.empire) return pl; } } } return null; } bool perform(BasicAI@ ai) { if(hw is null) @hw = getHomeworld(ai); if(hw !is null) { uint plLevel = hw.level; if(plLevel < 5) { ai.requestPlanetImprovement(hw, plLevel+1); } else { ai.requestImport(hw, ai.getResourceList(RT_LevelThree, true)); } } else { ai.colonizeByResource(ai.getResourceList(RT_LevelOne, onlyExportable = false)); } return false; } } enum OrbitalType { OT_Shipyard = 0, OT_TradeOutpost = 1, OT_FlingBeacon = 2, OT_Mainframe = 3, OT_Gate, }; int64 buildOrbitalHash(Region@ at, OrbitalType type) { return (int64(ACT_BuildOrbital) << ACT_BIT_OFFSET) | (int64(type) << 32) | at.id; } class BuildOrbital : Action { uint type; Region@ sys; Object@ builder; int64 Hash; BuildOrbital(Region& at, OrbitalType Type) { type = Type; @sys = at; Hash = buildOrbitalHash(sys, Type); } BuildOrbital(BasicAI@ ai, SaveFile& msg) { msg >> type; msg >> sys; msg >> builder; Hash = buildOrbitalHash(sys, OrbitalType(type)); } void postLoad(BasicAI@ ai) { } void save(BasicAI@ ai, SaveFile& msg) { msg << type; msg << sys; msg << builder; } int64 get_hash() const { return Hash; } ActionType get_actionType() const { return ACT_BuildOrbital; } string get_state() const { if(builder is null) return "Planning orbital construction in " + sys.name; else return "Building Orbital in " + sys.name; } bool isTradeable(Region@ region, Empire@ emp, const Territory@ territory) { if(territory is region.getTerritory(emp)) return true; const SystemDesc@ system = getSystem(region); for(uint i = 0, cnt = system.adjacent.length; i < cnt; ++i) { Region@ other = getSystem(system.adjacent[i]).object; if(territory is other.getTerritory(emp)) return true; } return false; } bool perform(BasicAI@ ai) { if(builder !is null) { //Look for the orbital auto@ reg = ai.findSystem(sys); if(reg is null) return true; if(type == OT_Gate) { auto@ gate = ai.empire.getStargate(sys.position); if(gate !is null && gate.region is sys) return true; } else { reg.scout(ai); for(uint i = 0, cnt = reg.orbitals.length; i < cnt; ++i) { auto@ orb = reg.orbitals[i]; if(orb.owner is ai.empire && ai.orbitals.find(orb) < 0) { int orbID = -1; switch(type) { case OT_Shipyard: orbID = getOrbitalModuleID("Shipyard"); break; case OT_TradeOutpost: orbID = getOrbitalModuleID("TradeOutpost"); break; case OT_FlingBeacon: orbID = getOrbitalModuleID("FlingCore"); break; case OT_Mainframe: orbID = getOrbitalModuleID("Mainframe"); break; } if(orbID < -1) return true; if(orb.hasModule(orbID)) { ai.orbitals.insertLast(reg.orbitals[i]); return true; } } } } if(!builder.valid || builder.owner !is ai.empire || ai.isBuildIdle(builder)) @builder = null; } else { for(uint i = 0, cnt = ai.factories.length; i < cnt; ++i) { Object@ buildAt = ai.factories[i]; if(buildAt.owner is ai.empire && buildAt.hasConstruction && buildAt.canBuildOrbitals && ai.isBuildIdle(buildAt) && buildAt.laborIncome > 0.05) { auto@ reg = buildAt.region; if(reg is null || !isTradeable(sys, ai.empire, reg.getTerritory(ai.empire))) continue; int orbID = -1; const Design@ station; switch(type) { case OT_Shipyard: orbID = getOrbitalModuleID("Shipyard"); break; case OT_TradeOutpost: orbID = getOrbitalModuleID("TradeOutpost"); break; case OT_FlingBeacon: orbID = getOrbitalModuleID("FlingCore"); break; case OT_Mainframe: orbID = getOrbitalModuleID("Mainframe"); break; case OT_Gate: @station = ai.empire.getDesign("Gate"); break; } if(orbID < 0 && station is null) return true; vec2d off = random2d(200.0, sys.radius - 200.0); if(orbID >= 0) buildAt.buildOrbital(orbID, sys.position + vec3d(off.x, 0.0, off.y)); else buildAt.buildStation(station, sys.position + vec3d(off.x, 0.0, off.y)); @builder = buildAt; break; } } } return false; } } int64 manageFactoriesHash() { return int64(ACT_ManageFactories) << ACT_BIT_OFFSET; } class ManageFactories : Action { ManageFactories(BasicAI@ ai, SaveFile& file) { } ManageFactories() { } int64 get_hash() const { return manageFactoriesHash(); } ActionType get_actionType() const { return ACT_ManageFactories; } string get_state() const { return "Managing factories"; } void save(BasicAI@, SaveFile& msg) {} void postLoad(BasicAI@) {} set_int checked; AsteroidCache@ findAsteroid(BasicAI@ ai, PlanRegion@ region, bool recurse = true, bool original = true, int maxHops = -1) { if(original) checked.clear(); if(checked.contains(region.region.id)) return null; checked.insert(region.region.id); if(region.resourceAsteroids.length != 0) return region.resourceAsteroids[randomi(0, region.resourceAsteroids.length-1)]; if(recurse && region.region.TradeMask & ai.empire.TradeMask.value != 0 && maxHops != 0) { uint cnt = region.system.adjacent.length; uint off = randomi(0, cnt-1); for(uint i = 0; i < cnt; ++i) { auto@ other = getSystem(region.system.adjacent[(i+off)%cnt]); if(other !is null) { PlanRegion@ otherRegion = ai.findSystem(other.object); if(otherRegion !is null) { AsteroidCache@ cache = findAsteroid(ai, otherRegion, recurse=false, original=false, maxHops=maxHops-1); if(cache !is null) return cache; } } } for(uint i = 0; i < cnt; ++i) { auto@ other = getSystem(region.system.adjacent[(i+off)%cnt]); if(other !is null) { PlanRegion@ otherRegion = ai.findSystem(other.object); if(otherRegion !is null) { AsteroidCache@ cache = findAsteroid(ai, otherRegion, recurse=true, original=false, maxHops=maxHops-1); if(cache !is null) return cache; } } } } return null; } bool perform(BasicAI@ ai) { //Check for idle factories, and make them build nearby asteroids if(ai.factories.length == 0) return false; Object@ fact = ai.factories[randomi(0, ai.factories.length-1)]; uint consCount = fact.constructionCount; uint consType = fact.constructionType; //Deal with mechanoid population construction if(ai.isMachineRace && fact.isPlanet) { bool buildPop = false; if(fact.population < int(fact.maxPopulation + 1 + fact.owner.autoColonizeCount)) { buildPop = true; } if(consCount != 0) { if(consType == CT_Construction) buildPop = false; if(consType == CT_Flagship && ai.factories.length > 1 && int(fact.population) >= int(fact.maxPopulation)) buildPop = false; } if(buildPop) { auto@ constr = getConstructionType("MechanoidPopulation"); if(constr !is null) { fact.buildConstruction(constr.id); if(consCount != 0) fact.moveConstruction(fact.constructionID[0], -1); consCount += 1; if(constr.getLaborCost(fact) > fact.laborIncome * 60.0) { ai.addIdle( ai.requestImport(cast(fact), ai.getResourceList(RT_LaborZero, onlyExportable=true), execute=false) ); } return false; } } } //Move asteroid constructibles to the end if(consCount != 0) { if(consCount > 1 && consType == CT_Asteroid) fact.moveConstruction(fact.constructionID[0], -1); return false; } //Find a nearby asteroid to build if(fact.canBuildAsteroids) { Region@ regObj = fact.region; if(regObj is null) return false; int hops = randomi(0,ai.skillEconomy * 2); auto@ region = ai.findSystem(regObj); AsteroidCache@ roid = findAsteroid(ai, region, maxHops=hops); if(roid is null || roid.asteroid.owner !is defaultEmpire || !roid.asteroid.valid) return false; roid.cache(); if(roid.resources.length == 0) return false; int resourceId = roid.resources[randomi(0, roid.resources.length-1)]; fact.buildAsteroid(roid.asteroid, resourceId); return false; } return false; } }; int64 managePressureResourcesHash() { return int64(ACT_ManagePressureResources) << ACT_BIT_OFFSET; } class ManagePressureResources : Action { Object@ destination; int destFree = 0; float destPct = 1.f; Object@ secondary; int secondaryFree = 0; const ResourceType@ asteroidLabor = getResource("AsteroidLabor"); const ResourceType@ morphics = getResource("AsteroidAffinity"); const ResourceType@ growth = getResource("AsteroidGrowth"); ManagePressureResources(BasicAI@ ai, SaveFile& file) { file >> destination; file >> destFree; file >> destPct; file >> secondary; file >> secondaryFree; } ManagePressureResources() { } int64 get_hash() const { return managePressureResourcesHash(); } ActionType get_actionType() const { return ACT_ManagePressureResources; } string get_state() const { return "Managing pressure resources."; } void save(BasicAI@, SaveFile& file) { file << destination; file << destFree; file << destPct; file << secondary; file << secondaryFree; } void postLoad(BasicAI@) {} void checkDestination(Object@ dest) { int pTotal = dest.pressureCap; int pUsed = dest.totalPressure; int free = max(pTotal - pUsed, 0); float pct = float(pUsed) / float(pTotal); if(free > destFree) { @destination = dest; destFree = free; destPct = pct; } if(free > secondaryFree) { auto@ res = getResource(dest.primaryResourceType); if(res.totalPressure > 0 && res.tilePressure[TR_Labor] == 0) { secondaryFree = free; @secondary = dest; } } } bool checkResource(BasicAI@ ai, Object@ obj, Object@ curDest = null) { const ResourceType@ res = getResource(obj.primaryResourceType); if(res !is null) { uint resCls = ai.classifyResource(res); if(resCls == RT_PressureResource) { if(destination !is null && destPct < 1.5f) { if(!obj.primaryResourceExported) { obj.exportResource(0, destination); ai.markUsedGeneric(obj, destination); return true; } else if(ai.skillEconomy >= DIFF_Medium) { if(curDest is null) @curDest = obj.nativeResourceDestination[0]; if(curDest !is null && curDest.isPlanet) { if(destFree >= res.totalPressure - 0.001f && uint(curDest.totalPressure) > curDest.pressureCap) { obj.exportResource(0, destination); ai.markUsedGeneric(obj, destination); return true; } } } } } else if(res is asteroidLabor) { //Send this to one of our factories if(ai.factories.length != 0 && !obj.primaryResourceExported) { Object@ fact = ai.factories[randomi(0, ai.factories.length-1)]; if(fact.isPlanet) { obj.exportResource(0, fact); ai.markUsedGeneric(obj, fact); return true; } } } else if(res is morphics) { //Send this to a planet that can use pressure and has a native resource with non-labor pressure if(secondary !is null) { if(!obj.primaryResourceExported) { obj.exportResource(0, secondary); ai.markUsedGeneric(obj, secondary); return true; } else if(secondaryFree >= 2) { if(curDest is null) @curDest = obj.nativeResourceDestination[0]; if(curDest !is null && curDest.isPlanet) { if(uint(curDest.totalPressure) > curDest.pressureCap) { obj.exportResource(0, secondary); ai.markUsedGeneric(obj, secondary); return true; } } } } } else if(res is growth) { //Send this whereever if(destination !is null && !obj.primaryResourceExported) { obj.exportResource(0, destination); ai.markUsedGeneric(obj, destination); return true; } } else if(ai.isMachineRace && (resCls == RT_Food || resCls == RT_Water) && obj.isPlanet && !obj.isContested) { //Abandon food and water planets we captured or no longer need to have to keep the system. Region@ reg = obj.region; if(reg !is null && reg.getPlanetCount(ai.empire) >= 2) { obj.abandon(); return true; } } } if(ai.needsStalks && obj.isPlanet && obj.resourceLevel >= 2 && obj.pressureCap < 1 && obj.totalPressure > 0) ai.addIdle(ai.requestBuilding(cast(obj), getBuildingType("Stalk"))); return false; } uint nextCheckRes = 0; bool perform(BasicAI@ ai) { //check our cached destinations if(destination is null) @destination = ai.homeworld; if(destination !is null && (!destination.valid || destination.owner !is ai.empire)) @destination = null; if(secondary !is null && (!secondary.valid || secondary.owner !is ai.empire)) @secondary = null; if(destination !is null) { int pTotal = destination.pressureCap; int pUsed = destination.totalPressure; destFree = max(pTotal - pUsed, 0); destPct = float(pUsed) / float(pTotal); } if(secondary !is null) { secondaryFree = secondary.pressureCap - secondary.totalPressure; if(secondaryFree <= 0) { @secondary = null; secondaryFree = 0; } } //Find a planet we have that has significant pressure cap free PlanetList@ list; for(uint n = 0; n < 30; ++n) { @list = ai.planetsByResource[nextCheckRes++ % ai.planetsByResource.length]; if(list.idle.length == 0 && list.used.length == 0) continue; for(uint j = 0; j < 2; ++j) { auto@ planets = j == 0 ? list.idle : list.used; uint plCnt = planets.length; if(plCnt == 0) continue; uint check = min(ai.skillEconomy * 2, plCnt); for(uint i = 0, cnt = check, base = randomi(0,plCnt-1); i < cnt; ++i) { Planet@ pl = planets[(i+base) % plCnt]; if(pl.valid && pl.owner is ai.empire) checkDestination(pl); } } break; } if(destination is null || destPct > 1.5f) return false; //Handle pressure-providing resources from asteroids if(ai.idleAsteroids.length > 0) { uint index = randomi(0, ai.idleAsteroids.length-1); Asteroid@ roid = ai.idleAsteroids[index]; if(checkResource(ai, roid)) return false; } if(ai.usedAsteroids.length > 0) { uint index = randomi(0, ai.usedAsteroids.length-1); Asteroid@ roid = ai.usedAsteroids[index]; if(checkResource(ai, roid)) return false; } //Handle pressure-providing resources on planets for(uint j = 0; j < 2; ++j) { auto@ planets = j == 0 ? list.idle : list.used; for(uint i = 0, cnt = planets.length; i < cnt; ++i) { Planet@ pl = planets[i]; Object@ curDest; if(j == 1) @curDest = list.purpose[i]; if(checkResource(ai, pl, curDest)) return false; } } return false; } };