WIP base prebuild AI (not functional)
This commit is contained in:
@@ -1425,7 +1425,7 @@ rules.wavespawnatcores.info = When enabled in attack mode, waves spawn near all
|
|||||||
rules.attack = Attack Mode
|
rules.attack = Attack Mode
|
||||||
rules.buildai = Base Builder AI
|
rules.buildai = Base Builder AI
|
||||||
rules.buildaitier = Builder AI Tier
|
rules.buildaitier = Builder AI Tier
|
||||||
rules.rtsai = RTS AI [red](WIP)
|
rules.rtsai = RTS AI
|
||||||
rules.rtsai.campaign = RTS Attack AI
|
rules.rtsai.campaign = RTS Attack AI
|
||||||
rules.rtsai.campaign.info = In attack maps, makes units group up and attack player bases in a more intelligent manner.
|
rules.rtsai.campaign.info = In attack maps, makes units group up and attack player bases in a more intelligent manner.
|
||||||
rules.rtsminsquadsize = Min Squad Size
|
rules.rtsminsquadsize = Min Squad Size
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import arc.math.*;
|
|||||||
import arc.math.geom.*;
|
import arc.math.geom.*;
|
||||||
import arc.struct.*;
|
import arc.struct.*;
|
||||||
import arc.util.*;
|
import arc.util.*;
|
||||||
|
import mindustry.ai.types.*;
|
||||||
import mindustry.content.*;
|
import mindustry.content.*;
|
||||||
import mindustry.entities.*;
|
import mindustry.entities.*;
|
||||||
import mindustry.entities.Units.*;
|
import mindustry.entities.Units.*;
|
||||||
@@ -115,6 +116,14 @@ public class BlockIndexer{
|
|||||||
}
|
}
|
||||||
|
|
||||||
updatePresentOres();
|
updatePresentOres();
|
||||||
|
|
||||||
|
for(Team team : Team.all){
|
||||||
|
var data = state.teams.get(team);
|
||||||
|
|
||||||
|
if(team.rules().prebuildAi && data.hasCore()){
|
||||||
|
PrebuildAI.sortPlans(data.plans);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,11 +209,17 @@ public class BuilderAI extends AIController{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AIController fallback(){
|
public AIController fallback(){
|
||||||
|
if(unit.team.isAI() && unit.team.rules().prebuildAi){
|
||||||
|
return new PrebuildAI();
|
||||||
|
}
|
||||||
return unit.type.flying ? new FlyingAI() : new GroundAI();
|
return unit.type.flying ? new FlyingAI() : new GroundAI();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean useFallback(){
|
public boolean useFallback(){
|
||||||
|
if(unit.team.isAI() && unit.team.rules().prebuildAi){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return state.rules.waves && unit.team == state.rules.waveTeam && !unit.team.rules().rtsAi;
|
return state.rules.waves && unit.team == state.rules.waveTeam && !unit.team.rules().rtsAi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public class MinerAI extends AIController{
|
|||||||
|
|
||||||
circle(core, unit.type.range / 1.8f);
|
circle(core, unit.type.range / 1.8f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!unit.type.flying){
|
if(!unit.type.flying){
|
||||||
unit.updateBoosting(unit.type.boostWhenMining || unit.floorOn().isDuct || unit.floorOn().damageTaken > 0f || unit.floorOn().isDeep());
|
unit.updateBoosting(unit.type.boostWhenMining || unit.floorOn().isDuct || unit.floorOn().damageTaken > 0f || unit.floorOn().isDeep());
|
||||||
}
|
}
|
||||||
|
|||||||
250
core/src/mindustry/ai/types/PrebuildAI.java
Normal file
250
core/src/mindustry/ai/types/PrebuildAI.java
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
package mindustry.ai.types;
|
||||||
|
|
||||||
|
import arc.math.*;
|
||||||
|
import arc.struct.*;
|
||||||
|
import arc.util.*;
|
||||||
|
import mindustry.entities.units.*;
|
||||||
|
import mindustry.game.Teams.*;
|
||||||
|
import mindustry.gen.*;
|
||||||
|
import mindustry.type.*;
|
||||||
|
import mindustry.world.*;
|
||||||
|
import mindustry.world.blocks.ConstructBlock.*;
|
||||||
|
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||||
|
|
||||||
|
import static mindustry.Vars.*;
|
||||||
|
|
||||||
|
//don't use this yet, it's not functional!
|
||||||
|
public class PrebuildAI extends AIController{
|
||||||
|
static float[] priorities = new float[Category.all.length];
|
||||||
|
|
||||||
|
static Seq<BlockPlan> tmpCopy = new Seq<>();
|
||||||
|
|
||||||
|
@Nullable BlockPlan lastPlan;
|
||||||
|
@Nullable Block collectBlock;
|
||||||
|
|
||||||
|
boolean collectingItems;
|
||||||
|
boolean mining;
|
||||||
|
@Nullable Item lastTargetItem;
|
||||||
|
@Nullable Tile ore;
|
||||||
|
|
||||||
|
static{
|
||||||
|
priorities[Category.production.ordinal()] = 11f;
|
||||||
|
priorities[Category.distribution.ordinal()] = 10f;
|
||||||
|
priorities[Category.liquid.ordinal()] = 9f;
|
||||||
|
priorities[Category.crafting.ordinal()] = 8f;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//TODO move this function?
|
||||||
|
public static void sortPlans(Queue<BlockPlan> plans){
|
||||||
|
var copy = Seq.with(plans);
|
||||||
|
|
||||||
|
copy.sort(Structs.comps(
|
||||||
|
Structs.comparingFloat(plan -> priorities[plan.block.category.ordinal()]),
|
||||||
|
Structs.comparingFloat(plan -> plan.block.buildTime)
|
||||||
|
));
|
||||||
|
|
||||||
|
plans.clear();
|
||||||
|
|
||||||
|
for(var plan : copy){
|
||||||
|
plans.addFirst(plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canBuild(CoreBuild core, Block block){
|
||||||
|
return state.rules.infiniteResources || unit.team.rules().infiniteResources || core.items.has(block.requirements, state.rules.buildCostMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable BlockPlan findNextPlan(){
|
||||||
|
var data = unit.team.data();
|
||||||
|
var core = unit.core();
|
||||||
|
if(data.buildingTree == null || core == null) return null;
|
||||||
|
var plans = data.plans;
|
||||||
|
|
||||||
|
//TODO super slow just inline the search
|
||||||
|
tmpCopy.clear();
|
||||||
|
tmpCopy.addAll(plans);
|
||||||
|
|
||||||
|
//TODO: this search is really slow
|
||||||
|
var min = tmpCopy.min(plan ->
|
||||||
|
(canBuild(core, plan.block) || !Structs.contains(plan.block.requirements, it -> !indexer.hasOre(it.item))) &&
|
||||||
|
(plan.block.category == Category.production || data.buildingTree.any(plan.x * tilesize + plan.block.offset - (plan.block.size * tilesize + 1f)/2f, plan.y * tilesize + plan.block.offset- (plan.block.size * tilesize + 1f)/2f, plan.block.size * tilesize + 1f, plan.block.size * tilesize + 1f)),
|
||||||
|
plan -> unit.dst(plan.x * tilesize, plan.y * tilesize) - priorities[plan.block.category.ordinal()] * 200f);
|
||||||
|
|
||||||
|
if(min != null){
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
return plans.first();
|
||||||
|
/*
|
||||||
|
for(int i = searchIndex; i < Math.min(maxSearches, size); i++){
|
||||||
|
searchIndex ++;
|
||||||
|
int index = (i + startIndex) % size + head;
|
||||||
|
if(index >= values.length){
|
||||||
|
index -= values.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
var plan = plans.get((i + startIndex) % plans.size);
|
||||||
|
if(plan != null){
|
||||||
|
var block = plan.block;
|
||||||
|
|
||||||
|
if(data.buildingTree != null && data.buildingTree.any(plan.x * tilesize + block.offset, plan.y * tilesize + block.offset, block.size * tilesize + 1f, block.size * tilesize + 1f)){
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
//return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMovement(){
|
||||||
|
|
||||||
|
if(target != null && shouldShoot()){
|
||||||
|
unit.lookAt(target);
|
||||||
|
}else if(!unit.type.flying){
|
||||||
|
unit.lookAt(unit.prefRotation());
|
||||||
|
}
|
||||||
|
|
||||||
|
unit.updateBuilding = !collectingItems;
|
||||||
|
|
||||||
|
boolean moving = false;
|
||||||
|
|
||||||
|
if(collectingItems){
|
||||||
|
doMining();
|
||||||
|
}else if(unit.buildPlan() != null){
|
||||||
|
//approach plan if building
|
||||||
|
BuildPlan req = unit.buildPlan();
|
||||||
|
|
||||||
|
boolean valid =
|
||||||
|
!(lastPlan != null && lastPlan.removed) &&
|
||||||
|
((req.tile() != null && req.tile().build instanceof ConstructBuild cons && cons.current == req.block) ||
|
||||||
|
(req.breaking ?
|
||||||
|
Build.validBreak(unit.team(), req.x, req.y) :
|
||||||
|
Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation)));
|
||||||
|
|
||||||
|
if(valid){
|
||||||
|
float range = Math.min(unit.type.buildRange - 20f, 100f);
|
||||||
|
//move toward the plan
|
||||||
|
moveTo(req.tile(), range - 10f, 20f);
|
||||||
|
moving = !unit.within(req.tile(), range);
|
||||||
|
}else{
|
||||||
|
//discard invalid plan
|
||||||
|
unit.plans.removeFirst();
|
||||||
|
lastPlan = null;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
|
||||||
|
//find new plan
|
||||||
|
if(!unit.team.data().plans.isEmpty() && timer.get(timerTarget3, 2f)){
|
||||||
|
//Queue<BlockPlan> blocks = unit.team.data().plans;
|
||||||
|
BlockPlan plan = findNextPlan();
|
||||||
|
|
||||||
|
//check if it's already been placed
|
||||||
|
//if(world.tile(block.x, block.y) != null && world.tile(block.x, block.y).block() == block.block){
|
||||||
|
// blocks.removeFirst();
|
||||||
|
//}else
|
||||||
|
if(plan != null && Build.validPlace(plan.block, unit.team(), plan.x, plan.y, plan.rotation)){ //it's valid
|
||||||
|
if(!canBuild(unit.core(), plan.block)){
|
||||||
|
collectingItems = true;
|
||||||
|
collectBlock = plan.block;
|
||||||
|
lastTargetItem = null;
|
||||||
|
ore = null;
|
||||||
|
timer.reset(timerTarget, 0f);
|
||||||
|
}
|
||||||
|
lastPlan = plan;
|
||||||
|
unit.addBuild(new BuildPlan(plan.x, plan.y, plan.rotation, plan.block, plan.config));
|
||||||
|
|
||||||
|
|
||||||
|
//shift build plan to tail so next unit builds something else
|
||||||
|
//blocks.addLast(blocks.removeFirst());
|
||||||
|
}//else{
|
||||||
|
//shift head of queue to tail, try something else next time
|
||||||
|
// blocks.addLast(blocks.removeFirst());
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!unit.type.flying){
|
||||||
|
unit.updateBoosting(unit.type.boostWhenBuilding || moving || unit.floorOn().isDuct || unit.floorOn().damageTaken > 0f || unit.floorOn().isDeep());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void doMining(){
|
||||||
|
var core = unit.closestCore();
|
||||||
|
|
||||||
|
if(!unit.canMine() || core == null || collectBlock == null) return;
|
||||||
|
|
||||||
|
if(!unit.validMine(unit.mineTile)){
|
||||||
|
unit.mineTile(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mining){
|
||||||
|
var targetStack = Structs.find(collectBlock.requirements, i -> !core.items.has(i.item, Mathf.ceil(state.rules.buildCostMultiplier * i.amount)));
|
||||||
|
Item targetItem = targetStack == null ? null : targetStack.item;
|
||||||
|
|
||||||
|
if(targetItem != null){
|
||||||
|
lastTargetItem = targetItem;
|
||||||
|
}else{
|
||||||
|
targetItem = lastTargetItem;
|
||||||
|
//hacky way to check if the unit just deposited something
|
||||||
|
if(!unit.hasItem() && canBuild(core, collectBlock)){
|
||||||
|
collectingItems = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//core full of the target item, do nothing
|
||||||
|
if(targetItem != null && core.acceptStack(targetItem, 1, unit) == 0){
|
||||||
|
unit.clearItem();
|
||||||
|
unit.mineTile = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//if inventory is full, drop it off.
|
||||||
|
if(targetItem == null || unit.stack.amount >= unit.type.itemCapacity || (targetItem != null && !unit.acceptsItem(targetItem))){
|
||||||
|
mining = false;
|
||||||
|
}else{
|
||||||
|
if(timer.get(timerTarget3, 60) && targetItem != null){
|
||||||
|
ore = null;
|
||||||
|
if(unit.type.mineFloor) ore = indexer.findClosestOre(unit, targetItem);
|
||||||
|
if(ore == null && unit.type.mineWalls) ore = indexer.findClosestWallOre(unit, targetItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ore != null){
|
||||||
|
moveTo(ore, unit.type.mineRange / 2f, 20f);
|
||||||
|
|
||||||
|
if(unit.within(ore, unit.type.mineRange) && unit.validMine(ore)){
|
||||||
|
unit.mineTile = ore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
unit.mineTile = null;
|
||||||
|
|
||||||
|
if(unit.stack.amount == 0){
|
||||||
|
mining = true;
|
||||||
|
|
||||||
|
if(canBuild(core, collectBlock)){
|
||||||
|
collectingItems = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(unit.within(core, unit.type.range)){
|
||||||
|
if(core.acceptStack(unit.stack.item, unit.stack.amount, unit) > 0){
|
||||||
|
Call.transferItemTo(unit, unit.stack.item, unit.stack.amount, unit.x, unit.y, core);
|
||||||
|
}
|
||||||
|
|
||||||
|
unit.clearItem();
|
||||||
|
mining = true;
|
||||||
|
|
||||||
|
if(canBuild(core, collectBlock)){
|
||||||
|
collectingItems = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
circle(core, unit.type.range / 1.8f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2368,8 +2368,7 @@ public class UnitTypes{
|
|||||||
//region core
|
//region core
|
||||||
|
|
||||||
alpha = new UnitType("alpha"){{
|
alpha = new UnitType("alpha"){{
|
||||||
aiController = () -> new BuilderAI(true, 400f);
|
controller = u -> u.team.isAI() ? new BuilderAI(true, 400f) : new CommandAI();
|
||||||
controller = u -> u.team.isAI() ? aiController.get() : new CommandAI();
|
|
||||||
isEnemy = false;
|
isEnemy = false;
|
||||||
|
|
||||||
targetBuildingsMobile = false;
|
targetBuildingsMobile = false;
|
||||||
@@ -2408,8 +2407,7 @@ public class UnitTypes{
|
|||||||
}};
|
}};
|
||||||
|
|
||||||
beta = new UnitType("beta"){{
|
beta = new UnitType("beta"){{
|
||||||
aiController = () -> new BuilderAI(true, 400f);
|
controller = u -> u.team.isAI() ? new BuilderAI(true, 400f) : new CommandAI();
|
||||||
controller = u -> u.team.isAI() ? aiController.get() : new CommandAI();
|
|
||||||
isEnemy = false;
|
isEnemy = false;
|
||||||
|
|
||||||
targetBuildingsMobile = false;
|
targetBuildingsMobile = false;
|
||||||
@@ -2451,8 +2449,7 @@ public class UnitTypes{
|
|||||||
}};
|
}};
|
||||||
|
|
||||||
gamma = new UnitType("gamma"){{
|
gamma = new UnitType("gamma"){{
|
||||||
aiController = () -> new BuilderAI(true, 400f);
|
controller = u -> u.team.isAI() ? new BuilderAI(true, 400f) : new CommandAI();
|
||||||
controller = u -> u.team.isAI() ? aiController.get() : new CommandAI();
|
|
||||||
isEnemy = false;
|
isEnemy = false;
|
||||||
|
|
||||||
targetBuildingsMobile = false;
|
targetBuildingsMobile = false;
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import arc.util.*;
|
|||||||
import mindustry.*;
|
import mindustry.*;
|
||||||
import mindustry.ai.*;
|
import mindustry.ai.*;
|
||||||
import mindustry.annotations.Annotations.*;
|
import mindustry.annotations.Annotations.*;
|
||||||
|
import mindustry.content.*;
|
||||||
import mindustry.core.GameState.*;
|
import mindustry.core.GameState.*;
|
||||||
import mindustry.ctype.*;
|
import mindustry.ctype.*;
|
||||||
|
import mindustry.entities.*;
|
||||||
import mindustry.game.EventType.*;
|
import mindustry.game.EventType.*;
|
||||||
import mindustry.game.*;
|
import mindustry.game.*;
|
||||||
import mindustry.game.Teams.*;
|
import mindustry.game.Teams.*;
|
||||||
@@ -16,6 +18,7 @@ import mindustry.maps.*;
|
|||||||
import mindustry.type.*;
|
import mindustry.type.*;
|
||||||
import mindustry.type.Weather.*;
|
import mindustry.type.Weather.*;
|
||||||
import mindustry.world.*;
|
import mindustry.world.*;
|
||||||
|
import mindustry.world.blocks.storage.*;
|
||||||
import mindustry.world.blocks.storage.CoreBlock.*;
|
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -455,7 +458,8 @@ public class Logic implements ApplicationListener{
|
|||||||
updateWeather();
|
updateWeather();
|
||||||
|
|
||||||
for(TeamData data : state.teams.getActive()){
|
for(TeamData data : state.teams.getActive()){
|
||||||
if(data.team.rules().fillItems && data.cores.size > 0){
|
var rules = data.team.rules();
|
||||||
|
if(rules.fillItems && data.cores.size > 0){
|
||||||
var core = data.cores.first();
|
var core = data.cores.first();
|
||||||
content.items().each(i -> {
|
content.items().each(i -> {
|
||||||
if(i.isOnPlanet(Vars.state.getPlanet())){
|
if(i.isOnPlanet(Vars.state.getPlanet())){
|
||||||
@@ -464,15 +468,29 @@ public class Logic implements ApplicationListener{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
//does not work on PvP so built-in attack maps can have it on by default without issues
|
//does not work on PvP so built-in attack maps can have it on by default without issues
|
||||||
if(data.team.rules().buildAi && !state.rules.pvp){
|
if(rules.buildAi && !state.rules.pvp){
|
||||||
if(data.buildAi == null) data.buildAi = new BaseBuilderAI(data);
|
if(data.buildAi == null) data.buildAi = new BaseBuilderAI(data);
|
||||||
data.buildAi.update();
|
data.buildAi.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(data.team.rules().rtsAi){
|
if(rules.rtsAi){
|
||||||
if(data.rtsAi == null) data.rtsAi = new RtsAI(data);
|
if(data.rtsAi == null) data.rtsAi = new RtsAI(data);
|
||||||
data.rtsAi.update();
|
data.rtsAi.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//spawn units for prebuild AI cores
|
||||||
|
if(rules.prebuildAi && !state.isEditor()){
|
||||||
|
for(var core : data.cores){
|
||||||
|
var units = data.getUnits(((CoreBlock)core.block).unitType);
|
||||||
|
if(units == null || !units.contains(u -> u.flag == core.pos())){
|
||||||
|
Unit unit = ((CoreBlock)core.block).unitType.spawn(core, data.team);
|
||||||
|
unit.flag = core.pos();
|
||||||
|
unit.add();
|
||||||
|
Units.notifyUnitSpawn(unit);
|
||||||
|
Fx.spawn.at(unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -305,6 +305,8 @@ public class Rules{
|
|||||||
public boolean infiniteResources;
|
public boolean infiniteResources;
|
||||||
/** If true, this team has infinite unit ammo. */
|
/** If true, this team has infinite unit ammo. */
|
||||||
public boolean infiniteAmmo;
|
public boolean infiniteAmmo;
|
||||||
|
/** EXPERIMENTAL, DO NOT USE: Pre-built base AI. Gives the illusion of intelligent design of pre-building an attack base. */
|
||||||
|
public boolean prebuildAi;
|
||||||
|
|
||||||
/** AI that builds random schematics. */
|
/** AI that builds random schematics. */
|
||||||
public boolean buildAi;
|
public boolean buildAi;
|
||||||
|
|||||||
@@ -94,7 +94,12 @@ public class Teams{
|
|||||||
|
|
||||||
/** Returns team data by type. */
|
/** Returns team data by type. */
|
||||||
public TeamData get(Team team){
|
public TeamData get(Team team){
|
||||||
return map[team.id] == null ? (map[team.id] = new TeamData(team)) : map[team.id];
|
var data = map[team.id];
|
||||||
|
if(data != null){
|
||||||
|
return data;
|
||||||
|
}else{
|
||||||
|
return map[team.id] = new TeamData(team);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable TeamData getOrNull(Team team){
|
public @Nullable TeamData getOrNull(Team team){
|
||||||
@@ -276,7 +281,7 @@ public class Teams{
|
|||||||
/** Enemies with cores or spawn points. */
|
/** Enemies with cores or spawn points. */
|
||||||
public Team[] coreEnemies = {};
|
public Team[] coreEnemies = {};
|
||||||
/** Planned blocks for drones. This is usually only blocks that have been broken. */
|
/** Planned blocks for drones. This is usually only blocks that have been broken. */
|
||||||
public Queue<BlockPlan> plans = new Queue<>();
|
public Queue<BlockPlan> plans = new Queue<>(16, BlockPlan.class);
|
||||||
|
|
||||||
/** List of live cores of this team. */
|
/** List of live cores of this team. */
|
||||||
public final Seq<CoreBuild> cores = new Seq<>();
|
public final Seq<CoreBuild> cores = new Seq<>();
|
||||||
|
|||||||
Reference in New Issue
Block a user