import empire_ai.weasel.WeaselAI; import empire_ai.weasel.Resources; import empire_ai.weasel.Budget; import empire_ai.weasel.Systems; import planets.PlanetSurface; import void relationRecordLost(AI& ai, Empire& emp, Object@ obj) from "empire_ai.weasel.Relations"; import buildings; import saving; final class BuildingRequest { int id = -1; PlanetAI@ plAI; AllocateBudget@ alloc; const BuildingType@ type; double expires = INFINITY; bool built = false; bool canceled = false; bool scatter = false; vec2i builtAt; BuildingRequest(Budget& budget, const BuildingType@ type, double priority, uint moneyType) { @this.type = type; @alloc = budget.allocate(moneyType, type.buildCostEst, type.maintainCostEst, priority=priority); } BuildingRequest() { } void save(Planets& planets, SaveFile& file) { planets.saveAI(file, plAI); planets.budget.saveAlloc(file, alloc); file.writeIdentifier(SI_Building, type.id); file << expires; file << built; file << canceled; file << builtAt; file << scatter; } void load(Planets& planets, SaveFile& file) { @plAI = planets.loadAI(file); @alloc = planets.budget.loadAlloc(file); @type = getBuildingType(file.readIdentifier(SI_Building)); file >> expires; file >> built; file >> canceled; file >> builtAt; if(file >= SV_0153) file >> scatter; } double cachedProgress = 0.0; double nextProgressCache = 0.0; double getProgress() { if(!built) return 0.0; if(gameTime < nextProgressCache) return cachedProgress; cachedProgress = plAI.obj.getBuildingProgressAt(builtAt.x, builtAt.y); if(cachedProgress > 0.95) nextProgressCache = gameTime + 1.0; else if(cachedProgress < 0.5) nextProgressCache = gameTime + 30.0; else nextProgressCache = gameTime + 10.0; return cachedProgress; } bool tick(AI& ai, Planets& planets, double time) { if(expires < gameTime) { if(planets.log) ai.print(type.name+" build request expired", plAI.obj); canceled = true; return false; } if(alloc is null || alloc.allocated) { builtAt = plAI.buildBuilding(ai, planets, type, scatter=scatter); if(builtAt == vec2i(-1,-1)) { planets.budget.remove(alloc); canceled = true; } else built = true; return false; } return true; } }; final class PlanetAI { Planet@ obj; int targetLevel = 0; int requestedLevel = 0; double prevTick = 0; array@ resources; ImportData@ claimedChain; void init(AI& ai, Planets& planets) { @resources = planets.resources.availableResource(obj); } void save(Planets& planets, SaveFile& file) { file << obj; file << targetLevel; file << requestedLevel; file << prevTick; uint cnt = 0; if(resources !is null) cnt = resources.length; file << cnt; for(uint i = 0; i < cnt; ++i) planets.resources.saveExport(file, resources[i]); planets.resources.saveImport(file, claimedChain); } void load(Planets& planets, SaveFile& file) { file >> obj; file >> targetLevel; file >> requestedLevel; file >> prevTick; uint cnt = 0; file >> cnt; @resources = array(); for(uint i = 0; i < cnt; ++i) { auto@ data = planets.resources.loadExport(file); if(data !is null) resources.insertLast(data); } @claimedChain = planets.resources.loadImport(file); } void remove(AI& ai, Planets& planets) { if(claimedChain !is null) { claimedChain.claimedFor = false; @claimedChain = null; } @resources = null; } void tick(AI& ai, Planets& planets, double time) { //Deal with losing planet ownership if(obj is null || !obj.valid || obj.owner !is ai.empire) { if(obj.owner !is ai.empire) relationRecordLost(ai, obj.owner, obj); planets.remove(this); return; } //Handle when the planet's native resources change if(obj.nativeResourceCount != resources.length || (resources.length != 0 && obj.primaryResourceId != resources[0].resourceId)) planets.updateResourceList(obj, resources); //Level up resources if we need them if(resources.length != 0 && claimedChain is null) { int resLevel = resources[0].resource.level; if(resLevel > 0 && !resources[0].resource.exportable) resLevel += 1; if(targetLevel < resLevel) { //See if we need it for anything first @claimedChain = planets.resources.findUnclaimed(resources[0]); if(claimedChain !is null) claimedChain.claimedFor = true; //Chain the levelup before what needs it planets.requestLevel(this, resLevel, before=claimedChain); } } //Request imports if the planet needs to level up if(targetLevel > requestedLevel) { int nextLevel = min(targetLevel, min(obj.resourceLevel, requestedLevel)+1); if(nextLevel != requestedLevel) { planets.resources.organizeImports(obj, nextLevel); requestedLevel = nextLevel; } } else if(targetLevel < requestedLevel) { planets.resources.organizeImports(obj, targetLevel); requestedLevel = targetLevel; } } double get_colonizeWeight() { if(obj.isColonizing) return 0.0; if(obj.level == 0) return 0.0; if(!obj.canSafelyColonize) return 0.0; double w = 1.0; double pop = obj.population; double maxPop = obj.maxPopulation; if(pop < maxPop-0.1) { if(obj.resourceLevel > 1 && pop/maxPop < 0.9) return 0.0; w *= 0.01 * (pop / maxPop); } return w; } vec2i buildBuilding(AI& ai, Planets& planets, const BuildingType@ type, bool scatter = true) { if(type is null || !type.canBuildOn(obj)) return vec2i(-1,-1); if(planets.log) ai.print("Attempt to construct "+type.name, obj); PlanetSurface@ surface = planets.surface; receive(obj.getPlanetSurface(), surface); //Find the best place to build this building int bestPenalty = INT_MAX; int possibs = 0; vec2i best; vec2i center = vec2i(type.getCenter()); for(int x = 0, w = surface.size.x; x < w; ++x) { for(int y = 0, h = surface.size.y; y < h; ++y) { vec2i pos(x, y); bool valid = true; int penalty = 0; for(int xoff = 0; xoff < int(type.size.x); ++xoff) { for(int yoff = 0; yoff < int(type.size.y); ++yoff) { vec2i rpos = pos - center + vec2i(xoff, yoff); if(rpos.x < 0 || rpos.y < 0 || rpos.x >= w || rpos.y >= h) { valid = false; break; } auto@ biome = surface.getBiome(rpos.x, rpos.y); if(biome is null || !biome.buildable) { valid = false; break; } uint flags = surface.getFlags(rpos.x, rpos.y); if(flags & SuF_Usable == 0) { bool affinity = false; if(type.buildAffinities.length != 0) { for(uint i = 0, cnt = type.buildAffinities.length; i < cnt; ++i) { if(biome is type.buildAffinities[i].biome) { affinity = true; break; } } } if(!affinity && type.tileBuildCost > 0) { penalty += 1; if(biome.buildCost > 1.0) penalty += ceil((biome.buildCost - 1.0) / 0.1); } affinity = false; if(type.maintainAffinities.length != 0) { for(uint i = 0, cnt = type.maintainAffinities.length; i < cnt; ++i) { if(biome is type.maintainAffinities[i].biome) { affinity = true; break; } } } if(!affinity && type.tileMaintainCost > 0) penalty += 2; } auto@ bld = surface.getBuilding(rpos.x, rpos.y); if(bld !is null) { if(bld.type.civilian) { penalty += 2; } else { valid = false; break; } } } if(!valid) break; } if(valid) { if(penalty < bestPenalty) { possibs = 1; bestPenalty = penalty; best = pos; } else if(penalty == bestPenalty && scatter) { possibs += 1; if(randomd() < 1.0 / double(possibs)) best = pos; } } } } if(bestPenalty != INT_MAX) { if(planets.log) ai.print("Construct "+type.name+" at "+best+" with penalty "+bestPenalty, obj); obj.buildBuilding(type.id, best); return best; } if(planets.log) ai.print("Could not find place to construct "+type.name, obj); return vec2i(-1,-1); } } final class PotentialSource { Planet@ pl; double weight = 0; }; final class AsteroidData { Asteroid@ asteroid; array@ resources; void save(Planets& planets, SaveFile& file) { file << asteroid; uint cnt = 0; if(resources !is null) cnt = resources.length; file << cnt; for(uint i = 0; i < cnt; ++i) planets.resources.saveExport(file, resources[i]); } void load(Planets& planets, SaveFile& file) { file >> asteroid; uint cnt = 0; file >> cnt; if(cnt != 0) @resources = array(); for(uint i = 0; i < cnt; ++i) { auto@ res = planets.resources.loadExport(file); if(res !is null) resources.insertLast(res); } } bool tick(AI& ai, Planets& planets) { if(asteroid is null || !asteroid.valid || asteroid.owner !is ai.empire) { planets.resources.killImportsTo(asteroid); planets.resources.killResourcesFrom(asteroid); return false; } if(resources is null) { @resources = planets.resources.availableResource(asteroid); } else { if(asteroid.nativeResourceCount != resources.length || (resources.length != 0 && asteroid.primaryResourceId != resources[0].resourceId)) planets.updateResourceList(asteroid, resources); } return true; } }; class Planets : AIComponent { Resources@ resources; Budget@ budget; Systems@ systems; PlanetSurface surface; array ownedAsteroids; array planets; array bumped; uint planetIdx = 0; array building; int nextBuildingRequestId = 0; void create() { @resources = cast(ai.resources); @budget = cast(ai.budget); @systems = cast(ai.systems); } void save(SaveFile& file) { file << nextBuildingRequestId; uint cnt = planets.length; file << cnt; for(uint i = 0; i < cnt; ++i) { auto@ plAI = planets[i]; saveAI(file, plAI); plAI.save(this, file); } cnt = building.length; file << cnt; for(uint i = 0; i < cnt; ++i) { saveBuildingRequest(file, building[i]); building[i].save(this, file); } cnt = ownedAsteroids.length; file << cnt; for(uint i = 0; i < cnt; ++i) ownedAsteroids[i].save(this, file); } void load(SaveFile& file) { file >> nextBuildingRequestId; uint cnt = 0; file >> cnt; for(uint i = 0; i < cnt; ++i) { auto@ plAI = loadAI(file); if(plAI !is null) plAI.load(this, file); else PlanetAI().load(this, file); } file >> cnt; for(uint i = 0; i < cnt; ++i) { auto@ req = loadBuildingRequest(file); if(req !is null) { req.load(this, file); building.insertLast(req); } } file >> cnt; for(uint i = 0; i < cnt; ++i) { AsteroidData data; data.load(this, file); if(data.asteroid !is null) ownedAsteroids.insertLast(data); } } PlanetAI@ loadAI(SaveFile& file) { Planet@ obj; file >> obj; if(obj is null) return null; PlanetAI@ plAI = getAI(obj); if(plAI is null) { @plAI = PlanetAI(); @plAI.obj = obj; plAI.prevTick = gameTime; planets.insertLast(plAI); } return plAI; } void saveAI(SaveFile& file, PlanetAI@ ai) { Planet@ pl; if(ai !is null) @pl = ai.obj; file << pl; } array loadIds; BuildingRequest@ loadBuildingRequest(int id) { if(id == -1) return null; for(uint i = 0, cnt = loadIds.length; i < cnt; ++i) { if(loadIds[i].id == id) return loadIds[i]; } BuildingRequest data; data.id = id; loadIds.insertLast(data); return data; } BuildingRequest@ loadBuildingRequest(SaveFile& file) { int id = -1; file >> id; if(id == -1) return null; else return loadBuildingRequest(id); } void saveBuildingRequest(SaveFile& file, BuildingRequest@ data) { int id = -1; if(data !is null) id = data.id; file << id; } void postLoad(AI& ai) { loadIds.length = 0; } void start() { checkForPlanets(); } void checkForPlanets() { auto@ data = ai.empire.getPlanets(); Object@ obj; while(receive(data, obj)) { Planet@ pl = cast(obj); if(pl !is null) register(cast(obj)); } } uint roidIdx = 0; void tick(double time) { double curTime = gameTime; if(planets.length != 0) { planetIdx = (planetIdx+1) % planets.length; auto@ plAI = planets[planetIdx]; plAI.tick(ai, this, curTime - plAI.prevTick); plAI.prevTick = curTime; } for(int i = bumped.length-1; i >= 0; --i) { auto@ plAI = bumped[i]; double tickTime = curTime - plAI.prevTick; if(tickTime != 0) { plAI.tick(ai, this, tickTime); plAI.prevTick = curTime; } } bumped.length = 0; if(ownedAsteroids.length != 0) { roidIdx = (roidIdx+1) % ownedAsteroids.length; if(!ownedAsteroids[roidIdx].tick(ai, this)) ownedAsteroids.removeAt(roidIdx); } //Construct any buildings we are waiting on for(uint i = 0, cnt = building.length; i < cnt; ++i) { if(!building[i].tick(ai, this, time)) { building.removeAt(i); --i; --cnt; break; } } } uint prevCount = 0; double checkTimer = 0; uint sysIdx = 0, ownedIdx = 0; void focusTick(double time) override { //Check for any newly obtained planets uint curCount = ai.empire.planetCount; checkTimer += time; if(curCount != prevCount || checkTimer > 60.0) { checkForPlanets(); prevCount = curCount; checkTimer = 0; } //Find any asteroids we've gained if(systems.all.length != 0) { sysIdx = (sysIdx+1) % systems.all.length; auto@ sys = systems.all[sysIdx]; for(uint i = 0, cnt = sys.asteroids.length; i < cnt; ++i) register(sys.asteroids[i]); } if(systems.owned.length != 0) { ownedIdx = (ownedIdx+1) % systems.owned.length; auto@ sys = systems.owned[ownedIdx]; for(uint i = 0, cnt = sys.asteroids.length; i < cnt; ++i) register(sys.asteroids[i]); } } void bump(Planet@ pl) { if(pl !is null) bump(getAI(pl)); } void bump(PlanetAI@ plAI) { if(plAI !is null) bumped.insertLast(plAI); } PlanetAI@ getAI(Planet& obj) { for(uint i = 0, cnt = planets.length; i < cnt; ++i) { if(planets[i].obj is obj) return planets[i]; } return null; } PlanetAI@ register(Planet& obj) { PlanetAI@ plAI = getAI(obj); if(plAI is null) { @plAI = PlanetAI(); @plAI.obj = obj; plAI.prevTick = gameTime; planets.insertLast(plAI); plAI.init(ai, this); } return plAI; } AsteroidData@ register(Asteroid@ obj) { if(obj is null || !obj.valid || obj.owner !is ai.empire) return null; for(uint i = 0, cnt = ownedAsteroids.length; i < cnt; ++i) { if(ownedAsteroids[i].asteroid is obj) return ownedAsteroids[i]; } AsteroidData data; @data.asteroid = obj; ownedAsteroids.insertLast(data); if(log) ai.print("Detected asteroid: "+obj.name, obj.region); return data; } void remove(PlanetAI@ plAI) { resources.killImportsTo(plAI.obj); resources.killResourcesFrom(plAI.obj); plAI.remove(ai, this); planets.remove(plAI); bumped.remove(plAI); } void requestLevel(PlanetAI@ plAI, int toLevel, ImportData@ before = null) { if(plAI is null) return; plAI.targetLevel = toLevel; if(before !is null) { for(int lv = max(plAI.requestedLevel, 1); lv <= toLevel; ++lv) resources.organizeImports(plAI.obj, lv, before); plAI.requestedLevel = toLevel; } else { bump(plAI); } } BuildingRequest@ requestBuilding(PlanetAI@ plAI, const BuildingType@ type, double priority = 1.0, double expire = INFINITY, bool scatter = true, uint moneyType = BT_Development) { if(plAI is null) return null; if(log) ai.print("Requested building of type "+type.name, plAI.obj); BuildingRequest req(budget, type, priority, moneyType); req.scatter = scatter; req.id = nextBuildingRequestId++; req.expires = gameTime + expire; @req.plAI = plAI; building.insertLast(req); return req; } bool isBuilding(Planet@ planet, const BuildingType@ type) { for(uint i = 0, cnt = building.length; i < cnt; ++i) { if(building[i].type is type && building[i].plAI.obj is planet) return true; } return false; } void getColonizeSources(array& sources) { sources.length = 0; for(uint i = 0, cnt = planets.length; i < cnt; ++i) { auto@ plAI = planets[i]; if(!plAI.obj.valid) continue; double w = plAI.colonizeWeight; if(w == 0) continue; if(plAI.obj.owner !is ai.empire) continue; PotentialSource src; @src.pl = planets[i].obj; src.weight = w; sources.insertLast(src); } } array newResources; array removedResources; array checkResources; void updateResourceList(Object@ obj, array& resList) { newResources.length = 0; removedResources = resList; checkResources.syncFrom(obj.getNativeResources()); uint nativeCnt = checkResources.length; for(uint i = 0; i < nativeCnt; ++i) { int id = checkResources[i].id; bool found = false; for(uint n = 0, ncnt = removedResources.length; n < ncnt; ++n) { if(removedResources[n].resourceId == id) { removedResources.removeAt(n); found = true; break; } } if(!found) { auto@ type = checkResources[i].type; auto@ res = resources.availableResource(obj, type, id); if(i == 0) resList.insertAt(0, res); else resList.insertLast(res); newResources.insertLast(res); } else if(i == 0 && resList.length > 1 && resList[0].resourceId != id) { for(uint n = 0, ncnt = resList.length; n < ncnt; ++n) { if(resList[n].resourceId == id) { auto@ res = resList[n]; resList.removeAt(n); resList.insertAt(0, res); break; } } } } //Get rid of resources we no longer have for(uint i = 0, cnt = removedResources.length; i < cnt; ++i) { resources.removeResource(removedResources[i]); resList.remove(removedResources[i]); } //Tell the resources component to try to immediately use the new resources for(uint i = 0, cnt = newResources.length; i < cnt; ++i) resources.checkReplaceCurrent(newResources[i]); } }; AIComponent@ createPlanets() { return Planets(); }