diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index c136d24745..102308dbe0 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -1158,6 +1158,7 @@ keybind.mouse_move.name = Follow Mouse keybind.pan.name = Pan View keybind.boost.name = Boost keybind.command_mode.name = Command Mode +keybind.command_queue.name = Unit Command Queue keybind.rebuild_select.name = Rebuild Region keybind.schematic_select.name = Select Region keybind.schematic_menu.name = Schematic Menu diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index fc4b455c14..4434f2f8a9 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -13,10 +13,12 @@ import mindustry.gen.*; import mindustry.world.*; public class CommandAI extends AIController{ + protected static final int maxCommandQueueSize = 64; protected static final float localInterval = 40f; protected static final Vec2 vecOut = new Vec2(), flockVec = new Vec2(), separation = new Vec2(), cohesion = new Vec2(), massCenter = new Vec2(); protected static final boolean[] noFound = {false}; + public Seq commandQueue = new Seq<>(5); public @Nullable Vec2 targetPos; public @Nullable Teamc attackTarget; /** All encountered unreachable buildings of this AI. Why a sequence? Because contains() is very rarely called on it. */ @@ -61,6 +63,11 @@ public class CommandAI extends AIController{ @Override public void updateUnit(){ + //remove invalid targets + if(commandQueue.any()){ + commandQueue.removeAll(e -> e instanceof Healthc h && !h.isValid()); + } + //assign defaults if(command == null && unit.type.commands.length > 0){ command = unit.type.defaultCommand == null ? unit.type.commands[0] : unit.type.defaultCommand; @@ -114,6 +121,11 @@ public class CommandAI extends AIController{ targetPos = null; } + //move on to the next target + if(attackTarget == null && targetPos == null){ + finishPath(); + } + if(targetPos != null){ if(timer.get(timerTarget3, localInterval) || !flocked){ if(!flocked){ @@ -160,7 +172,7 @@ public class CommandAI extends AIController{ unreachableBuildings.addUnique(build.pos()); } attackTarget = null; - targetPos = null; + finishPath(); return; } } @@ -193,7 +205,7 @@ public class CommandAI extends AIController{ if(attackTarget == null){ if(unit.within(targetPos, Math.max(5f, unit.hitSize / 2f))){ - targetPos = null; + finishPath(); }else if(local.size > 1){ int count = 0; for(var near : local){ @@ -205,13 +217,13 @@ public class CommandAI extends AIController{ //others have arrived at destination, so this one will too if(count >= Math.max(3, local.size / 2)){ - targetPos = null; + finishPath(); } } } if(stopWhenInRange && targetPos != null && unit.within(targetPos, engageRange * 0.9f)){ - targetPos = null; + finishPath(); stopWhenInRange = false; } @@ -220,6 +232,30 @@ public class CommandAI extends AIController{ } } + void finishPath(){ + targetPos = null; + if(commandQueue.size > 0){ + var next = commandQueue.remove(0); + if(next instanceof Teamc target){ + commandTarget(target, this.stopAtTarget); + }else if(next instanceof Vec2 position){ + commandPosition(position); + } + } + } + + public void commandQueue(Position location){ + if(targetPos == null && attackTarget == null){ + if(location instanceof Teamc target){ + commandTarget(target, this.stopAtTarget); + }else if(location instanceof Vec2 position){ + commandPosition(position); + } + }else if(commandQueue.size < maxCommandQueueSize && !commandQueue.contains(location)){ + commandQueue.add(location); + } + } + @Override public void hit(Bullet bullet){ if(unit.team.isAI() && bullet.owner instanceof Teamc teamc && teamc.team() != unit.team && attackTarget == null && diff --git a/core/src/mindustry/entities/comp/StatusComp.java b/core/src/mindustry/entities/comp/StatusComp.java index 0f95637707..9c08099f5d 100644 --- a/core/src/mindustry/entities/comp/StatusComp.java +++ b/core/src/mindustry/entities/comp/StatusComp.java @@ -16,7 +16,7 @@ import static mindustry.Vars.*; @Component abstract class StatusComp implements Posc, Flyingc{ - private Seq statuses = new Seq<>(); + private Seq statuses = new Seq<>(4); private transient Bits applied = new Bits(content.getBy(ContentType.status).size); //these are considered read-only diff --git a/core/src/mindustry/input/Binding.java b/core/src/mindustry/input/Binding.java index 89d8fde1b7..fb26974f7a 100644 --- a/core/src/mindustry/input/Binding.java +++ b/core/src/mindustry/input/Binding.java @@ -13,6 +13,7 @@ public enum Binding implements KeyBind{ boost(KeyCode.shiftLeft), command_mode(KeyCode.shiftLeft), + command_queue(KeyCode.mouseMiddle), control(KeyCode.controlLeft), respawn(KeyCode.v), select(KeyCode.mouseLeft), diff --git a/core/src/mindustry/input/DesktopInput.java b/core/src/mindustry/input/DesktopInput.java index 4ff1c27b24..2ddeb2ed36 100644 --- a/core/src/mindustry/input/DesktopInput.java +++ b/core/src/mindustry/input/DesktopInput.java @@ -6,6 +6,7 @@ import arc.Graphics.Cursor.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.input.*; +import arc.input.KeyCode.*; import arc.math.*; import arc.math.geom.*; import arc.scene.*; @@ -400,6 +401,10 @@ public class DesktopInput extends InputHandler{ if(canAttack){ cursorType = ui.targetCursor; } + + if(input.keyTap(Binding.command_queue) && keybinds.get(Binding.command_mode).key.type != KeyType.mouse){ + commandTap(input.mouseX(), input.mouseY(), true); + } } if(getPlan(cursor.x, cursor.y) != null && mode == none){ @@ -721,6 +726,10 @@ public class DesktopInput extends InputHandler{ commandTap(x, y); } + if(button == keybinds.get(Binding.command_queue).key){ + commandTap(x, y, true); + } + return super.touchDown(x, y, pointer, button); } diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index ae27729b0d..29c10baaf6 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -212,7 +212,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } @Remote(called = Loc.server, targets = Loc.both, forward = true) - public static void commandUnits(Player player, int[] unitIds, @Nullable Building buildTarget, @Nullable Unit unitTarget, @Nullable Vec2 posTarget){ + public static void commandUnits(Player player, int[] unitIds, @Nullable Building buildTarget, @Nullable Unit unitTarget, @Nullable Vec2 posTarget, boolean queueCommand){ if(player == null || unitIds == null) return; //why did I ever think this was a good idea @@ -240,9 +240,19 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ !(teamTarget instanceof Unit u && !unit.canTarget(u)) && !(teamTarget instanceof Building && !unit.type.targetGround)){ anyCommandedTarget = true; - ai.commandTarget(teamTarget); + if(queueCommand){ + ai.commandQueue(teamTarget); + }else{ + ai.commandQueue.clear(); + ai.commandTarget(teamTarget); + } }else if(posTarget != null){ - ai.commandPosition(posTarget); + if(queueCommand){ + ai.commandQueue(posTarget); + }else{ + ai.commandQueue.clear(); + ai.commandPosition(posTarget); + } } unit.lastCommanded = player.coloredName(); @@ -854,6 +864,10 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } public void commandTap(float screenX, float screenY){ + commandTap(screenX, screenY, false); + } + + public void commandTap(float screenX, float screenY, boolean queue){ if(commandMode){ //right click: move to position @@ -882,10 +896,10 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ if(ids.length > maxChunkSize){ for(int i = 0; i < ids.length; i += maxChunkSize){ int[] data = Arrays.copyOfRange(ids, i, Math.min(i + maxChunkSize, ids.length)); - Call.commandUnits(player, data, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target); + Call.commandUnits(player, data, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target, queue); } }else{ - Call.commandUnits(player, ids, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target); + Call.commandUnits(player, ids, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target, queue); } } @@ -907,6 +921,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ //draw command overlay UI for(Unit unit : selectedUnits){ CommandAI ai = unit.command(); + Position lastPos = ai.attackTarget != null ? ai.attackTarget : ai.targetPos; + //draw target line if(ai.targetPos != null && ai.currentCommand().drawTarget){ Position lineDest = ai.attackTarget != null ? ai.attackTarget : ai.targetPos; @@ -922,6 +938,24 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ if(ai.attackTarget != null && ai.currentCommand().drawTarget){ Drawf.target(ai.attackTarget.getX(), ai.attackTarget.getY(), 6f, Pal.remove); } + + if(lastPos == null){ + lastPos = unit; + } + + //draw command queue + if(ai.currentCommand().drawTarget && ai.commandQueue.size > 0){ + for(var next : ai.commandQueue){ + Drawf.limitLine(lastPos, next, 3.5f, 3.5f); + lastPos = next; + + if(next instanceof Vec2 vec){ + Drawf.square(vec.x, vec.y, 3.5f); + }else{ + Drawf.target(next.getX(), next.getY(), 6f, Pal.remove); + } + } + } } for(var commandBuild : commandBuildings){ diff --git a/core/src/mindustry/io/TypeIO.java b/core/src/mindustry/io/TypeIO.java index 21ad470fd7..48ab654b15 100644 --- a/core/src/mindustry/io/TypeIO.java +++ b/core/src/mindustry/io/TypeIO.java @@ -472,7 +472,7 @@ public class TypeIO{ write.b(3); write.i(logic.controller.pos()); }else if(control instanceof CommandAI ai){ - write.b(6); + write.b(7); write.bool(ai.attackTarget != null); write.bool(ai.targetPos != null); @@ -489,6 +489,24 @@ public class TypeIO{ } } write.b(ai.command == null ? -1 : ai.command.id); + + write.b(ai.commandQueue.size); + for(var pos : ai.commandQueue){ + if(pos instanceof Building b){ + write.b(0); + write.i(b.pos()); + }else if(pos instanceof Unit u){ + write.b(1); + write.i(u.id); + }else if(pos instanceof Vec2 v){ + write.b(2); + write.f(v.x); + write.f(v.y); + }else{ + //who put garbage in the command queue?? + write.b(3); + } + } }else if(control instanceof AssemblerAI){ //hate write.b(5); }else{ @@ -520,8 +538,8 @@ public class TypeIO{ out.controller = world.build(pos); return out; } - //type 4 is the old CommandAI with no commandIndex, type 6 is the new one with the index as a single byte. - }else if(type == 4 || type == 6){ + //type 4 is the old CommandAI with no commandIndex, type 6 is the new one with the index as a single byte, type 7 is the one with the command queue + }else if(type == 4 || type == 6 || type == 7){ CommandAI ai = prev instanceof CommandAI pai ? pai : new CommandAI(); boolean hasAttack = read.bool(), hasPos = read.bool(); @@ -544,11 +562,34 @@ public class TypeIO{ ai.attackTarget = null; } - if(type == 6){ + if(type == 6 || type == 7){ byte id = read.b(); ai.command = id < 0 ? null : UnitCommand.all.get(id); } + //command queue only in type 7 + if(type == 7){ + ai.commandQueue.clear(); + int length = read.ub(); + for(int i = 0; i < length; i++){ + int commandType = read.b(); + switch(commandType){ + case 0 -> { + var build = world.build(read.i()); + if(build != null) ai.commandQueue.add(build); + } + case 1 -> { + var unit = Groups.unit.getByID(read.i()); + if(unit != null) ai.commandQueue.add(unit); + } + case 2 -> { + ai.commandQueue.add(new Vec2(read.f(), read.f())); + } + //otherwise disregard + } + } + } + return ai; }else if(type == 5){ //augh