Merge branch 'master' of https://github.com/Anuken/Mindustry into crux-floor

# Conflicts:
#	gradle.properties
This commit is contained in:
Anuken
2025-07-10 13:52:31 -04:00
96 changed files with 1256 additions and 501 deletions

View File

@@ -202,6 +202,10 @@ public class Vars implements Loadable{
/** Whether to draw shadows of blocks at map edges and static blocks.
* Do not change unless you know exactly what you are doing.*/
public static boolean enableDarkness = true;
/** Whether to draw debug lines for collisions. */
public static boolean drawDebugHitboxes = false;
/** Whether to draw avoidance fields. */
public static boolean debugDrawAvoidance = false;
/** application data directory, equivalent to {@link Settings#getDataDirectory()} */
public static Fi dataDirectory;
/** data subdirectory used for screenshots */
@@ -255,6 +259,7 @@ public class Vars implements Loadable{
public static BaseRegistry bases;
public static GlobalVars logicVars;
public static MapEditor editor;
public static AvoidanceProcess avoidance;
public static GameService service = new GameService();
public static Universe universe;

View File

@@ -8,17 +8,26 @@ import mindustry.type.*;
public class ItemUnitStance extends UnitStance{
private static ObjectMap<Item, ItemUnitStance> itemToStance = new ObjectMap<>();
private static Seq<ItemUnitStance> all = new Seq<>();
public final Item item;
public ItemUnitStance(Item item){
super("item-" + item.name, "item-" + item.name, null);
this.item = item;
incompatibleStances.add(UnitStance.mineAuto).addAll(UnitStance.mineAuto.incompatibleStances);
itemToStance.put(item, this);
all.add(this);
}
public static @Nullable ItemUnitStance getByItem(Item item){
return itemToStance.get(item);
return item == null ? null : itemToStance.get(item);
}
public static Seq<ItemUnitStance> all(){
return all;
}
@Override

View File

@@ -121,7 +121,6 @@ public class Pathfinder implements Runnable{
mainList = new Seq<>();
clearCache();
for(int i = 0; i < tiles.length; i++){
Tile tile = world.tiles.geti(i);
tiles[i] = packTile(tile);
@@ -253,7 +252,7 @@ public class Pathfinder implements Runnable{
nearSolid,
nearLegSolid,
tile.floor().isDeep(),
tile.floor().damageTaken > 0.00001f,
tile.floor().damages(),
allDeep,
nearDeep,
tile.block().teamPassable
@@ -359,6 +358,11 @@ public class Pathfinder implements Runnable{
/** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Flowfield path, boolean diagonals){
return getTargetTile(tile, path, diagonals, 0);
}
/** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Flowfield path, boolean diagonals, int avoidanceId){
if(tile == null) return null;
//uninitialized flowfields are not applicable
@@ -390,6 +394,7 @@ public class Pathfinder implements Runnable{
int value = values[apos];
var points = diagonals ? Geometry.d8 : Geometry.d4;
int[] avoid = avoidanceId <= 0 ? null : avoidance.getAvoidance();
Tile current = null;
int tl = 0;
@@ -400,11 +405,13 @@ public class Pathfinder implements Runnable{
if(other == null) continue;
int packed = dx/res + dy/res * ww;
int avoidance = avoid == null ? 0 : avoid[packed] > Integer.MAX_VALUE - avoidanceId ? 1 : 0;
int cost = values[packed] + avoidance;
if(values[packed] < value && (current == null || values[packed] < tl) && path.passable(packed) &&
if(cost < value && avoidance == 0 && (current == null || cost < tl) && path.passable(packed) &&
!(point.x != 0 && point.y != 0 && (!path.passable(((tile.x + point.x)/res + tile.y/res*ww)) || !path.passable((tile.x/res + (tile.y + point.y)/res*ww))))){ //diagonal corner trap
current = other;
tl = values[packed];
tl = cost;
}
}
@@ -655,6 +662,11 @@ public class Pathfinder implements Runnable{
return pathfinder.getTargetTile(from, this);
}
/** @return the next tile to travel to for this flowfield. Main thread only. */
public @Nullable Tile getNextTile(Tile from, int unitAvoidanceId){
return pathfinder.getTargetTile(from, this, true, unitAvoidanceId);
}
public boolean hasCompleteWeights(){
return hasComplete && completeWeights != null;
}

View File

@@ -3,6 +3,7 @@ package mindustry.ai;
import arc.*;
import arc.input.*;
import arc.scene.style.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.ctype.*;
@@ -11,17 +12,39 @@ import mindustry.input.*;
import mindustry.type.*;
public class UnitStance extends MappableContent{
public static UnitStance stop, shoot, holdFire, pursueTarget, patrol, ram, mineAuto;
public static UnitStance stop, holdFire, pursueTarget, patrol, ram, mineAuto;
/** Name of UI icon (from Icon class). */
public String icon;
/** Key to press for this stance. */
public @Nullable KeyBind keybind;
/** Stances that are mutually exclusive to this stance. This is used for convenience, for writing only! */
public Seq<UnitStance> incompatibleStances = new Seq<>();
/** Incompatible stances as a bitset for easier operations. This is where incompatibility is actually stored. */
public Bits incompatibleBits = new Bits(32);
/** If true, this stance can be toggled on or off. */
public boolean toggle = true;
public UnitStance(String name, String icon, KeyBind keybind){
public UnitStance(String name, String icon, KeyBind keybind, boolean toggle){
super(name);
this.icon = icon;
this.keybind = keybind;
this.toggle = toggle;
}
public UnitStance(String name, String icon, KeyBind keybind){
this(name, icon, keybind, true);
}
@Override
public void init(){
super.init();
for(var stance : incompatibleStances){
if(stance == this) continue;
incompatibleBits.set(stance.id);
stance.incompatibleBits.set(id);
}
}
public String localized(){
@@ -47,13 +70,12 @@ public class UnitStance extends MappableContent{
}
public static void loadAll(){
stop = new UnitStance("stop", "cancel", Binding.cancelOrders);
shoot = new UnitStance("shoot", "commandAttack", Binding.unitStanceShoot);
stop = new UnitStance("stop", "cancel", Binding.cancelOrders, false);
holdFire = new UnitStance("holdfire", "none", Binding.unitStanceHoldFire);
pursueTarget = new UnitStance("pursuetarget", "right", Binding.unitStancePursueTarget);
patrol = new UnitStance("patrol", "refresh", Binding.unitStancePatrol);
ram = new UnitStance("ram", "rightOpen", Binding.unitStanceRam);
mineAuto = new UnitStance("mineauto", "settings", null);
mineAuto = new UnitStance("mineauto", "settings", null, false);
//Only vanilla items are supported for now
for(Item item : Vars.content.items()){

View File

@@ -40,10 +40,10 @@ public class CommandAI extends AIController{
protected float payloadPickupCooldown;
protected int transferState = transferStateNone;
/** Stance, usually related to firing mode. */
public UnitStance stance = UnitStance.shoot;
/** Current command this unit is following. */
public UnitCommand command;
/** Stance, usually related to firing mode. Each bit is a stance ID. */
public Bits stances = new Bits(content.unitStances().size);
/** Current controller instance based on command. */
protected @Nullable AIController commandController;
/** Last command type assigned. Used for detecting command changes. */
@@ -63,6 +63,38 @@ public class CommandAI extends AIController{
}
}
public boolean hasStance(@Nullable UnitStance stance){
return stance != null && stances.get(stance.id);
}
public void setStance(UnitStance stance, boolean enabled){
if(enabled){
setStance(stance);
}else{
disableStance(stance);
}
}
public void setStance(UnitStance stance){
//this happens when an older save reads the default "shoot" stance, or any other removed stance
if(stance == UnitStance.stop) return;
stances.andNot(stance.incompatibleBits);
stances.set(stance.id);
stanceChanged();
}
public void disableStance(UnitStance stance){
stances.clear(stance.id);
stanceChanged();
}
public void stanceChanged(){
if(commandController != null && !(commandController instanceof CommandAI)){
commandController.stanceChanged();
}
}
@Override
public void init(){
if(command == null){
@@ -82,21 +114,18 @@ public class CommandAI extends AIController{
@Override
public void updateUnit(){
//this should not be possible
if(stance == UnitStance.stop) stance = UnitStance.shoot;
//fix incorrect stance when mining
if(command == UnitCommand.mineCommand && stance != UnitStance.mineAuto && !(stance instanceof ItemUnitStance)){
stance = UnitStance.mineAuto;
if(command == UnitCommand.mineCommand && !hasStance(UnitStance.mineAuto) && !ItemUnitStance.all().contains(this::hasStance)){
setStance(UnitStance.mineAuto);
}
//pursue the target if relevant
if(stance == UnitStance.pursueTarget && target != null && attackTarget == null && targetPos == null){
if(hasStance(UnitStance.pursueTarget) && !hasStance(UnitStance.patrol) && target != null && attackTarget == null && targetPos == null){
commandTarget(target, false);
}
//pursue the target for patrol, keeping the current position
if(stance == UnitStance.patrol && target != null && attackTarget == null){
if(hasStance(UnitStance.patrol) && hasStance(UnitStance.pursueTarget) && target != null && attackTarget == null){
//commanding a target overwrites targetPos, so add it to the queue
if(targetPos != null){
commandQueue.add(targetPos.cpy());
@@ -207,6 +236,8 @@ public class CommandAI extends AIController{
finishPath();
}
boolean ramming = hasStance(UnitStance.ram);
if(attackTarget != null){
if(targetPos == null){
targetPos = new Vec2();
@@ -214,7 +245,7 @@ public class CommandAI extends AIController{
}
targetPos.set(attackTarget);
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.type.pathCostId != ControlPathfinder.costIdLegs && stance != UnitStance.ram){
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.type.pathCostId != ControlPathfinder.costIdLegs && !ramming){
Tile best = build.findClosestEdge(unit, Tile::solid);
if(best != null){
targetPos.set(best);
@@ -225,7 +256,7 @@ public class CommandAI extends AIController{
boolean alwaysArrive = false;
float engageRange = unit.type.range - 10f;
boolean withinAttackRange = attackTarget != null && unit.within(attackTarget, engageRange) && stance != UnitStance.ram;
boolean withinAttackRange = attackTarget != null && unit.within(attackTarget, engageRange) && !ramming;
if(targetPos != null){
boolean move = true, isFinalPoint = commandQueue.size == 0;
@@ -239,16 +270,17 @@ public class CommandAI extends AIController{
Building targetBuild = world.buildWorld(targetPos.x, targetPos.y);
//TODO: should the unit stop when it finds a target?
if(
(stance == UnitStance.patrol && target != null && unit.within(target, unit.type.range - 2f) && !unit.type.circleTarget) ||
(hasStance(UnitStance.patrol) && !hasStance(UnitStance.pursueTarget) && target != null && unit.within(target, unit.type.range - 2f) && !unit.type.circleTarget) ||
(command == UnitCommand.enterPayloadCommand && unit.within(targetPos, 4f) || (targetBuild != null && unit.within(targetBuild, targetBuild.block.size * tilesize/2f * 0.9f))) ||
(command == UnitCommand.loopPayloadCommand && unit.within(targetPos, 10f))
){
move = false;
}
if(unit.isGrounded() && stance != UnitStance.ram){
if(unit.isGrounded() && !ramming){
//TODO: blocking enable or disable?
if(timer.get(timerTarget3, avoidInterval)){
Vec2 dstPos = Tmp.v1.trns(unit.rotation, unit.hitSize/2f);
@@ -284,7 +316,7 @@ public class CommandAI extends AIController{
noFound[0] = false;
vecOut.set(vecMovePos);
}else{
move = controlPath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime);
move &= controlPath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime);
//TODO: what to do when there's a target and it can't be reached?
/*
@@ -320,7 +352,7 @@ public class CommandAI extends AIController{
moveTo(vecOut,
withinAttackRange ? engageRange :
unit.isGrounded() ? 0f :
attackTarget != null && stance != UnitStance.ram ? engageRange : 0f,
attackTarget != null && !ramming ? engageRange : 0f,
unit.isFlying() ? 40f : 100f, false, null, isFinalPoint || alwaysArrive);
}
}
@@ -412,7 +444,7 @@ public class CommandAI extends AIController{
commandPosition(position);
}
if(prev != null && (stance == UnitStance.patrol || command == UnitCommand.loopPayloadCommand)){
if(prev != null && (hasStance(UnitStance.patrol) || command == UnitCommand.loopPayloadCommand)){
commandQueue.add(prev.cpy());
}
@@ -454,7 +486,7 @@ public class CommandAI extends AIController{
@Override
public boolean shouldFire(){
return stance != UnitStance.holdFire;
return !hasStance(UnitStance.holdFire);
}
@Override

View File

@@ -1,7 +1,10 @@
package mindustry.ai.types;
import arc.math.*;
import arc.util.*;
import mindustry.*;
import mindustry.ai.*;
import mindustry.entities.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.world.*;
@@ -9,11 +12,19 @@ import mindustry.world.*;
import static mindustry.Vars.*;
public class GroundAI extends AIController{
float stuckTime = 0f;
float stuckX = -999f, stuckY = -999f;
static final float stuckRange = tilesize * 1.5f;
@Override
public void updateMovement(){
//if it hasn't moved the stuck range in twice the time it should have taken, it's stuck
float stuckThreshold = Math.max(1f, stuckRange * 2f / unit.type.speed);
Building core = unit.closestEnemyCore();
boolean moved = false;
if(core != null && unit.within(core, unit.range() / 1.3f + core.block.size * tilesize / 2f)){
target = core;
@@ -38,7 +49,9 @@ public class GroundAI extends AIController{
move = false;
}
if(move) pathfind(Pathfinder.fieldCore);
moved = move;
if(move) pathfind(Pathfinder.fieldCore, true, stuckTime >= stuckThreshold);
}
if(unit.type.canBoost && unit.elevation > 0.001f && !unit.onSolid()){
@@ -46,5 +59,28 @@ public class GroundAI extends AIController{
}
faceTarget();
if(moved){
if(unit.within(stuckX, stuckY, stuckRange)){
stuckTime += Time.delta;
if(stuckTime - Time.delta < stuckThreshold && stuckTime >= stuckThreshold){
float radius = unit.hitSize * Vars.unitCollisionRadiusScale * 2f;
Units.nearby(unit.team, unit.x, unit.y, radius, other -> {
if(other != unit && other.controller() instanceof GroundAI ai && other.within(unit.x, unit.y, radius + other.hitSize * unitCollisionRadiusScale)){
ai.stuckX = other.x;
ai.stuckY = other.y;
ai.stuckTime = Math.max(1f, stuckRange * 2f / other.type.speed) + 1f;
}
});
}
}else{
stuckX = unit.x;
stuckY = unit.y;
stuckTime = 0f;
}
}else{
stuckTime = 0f;
}
}
}

View File

@@ -13,6 +13,14 @@ public class MinerAI extends AIController{
public Item targetItem;
public Tile ore;
@Override
public void stanceChanged(){
if(targetItem != null && unit.controller() instanceof CommandAI ai && !ai.hasStance(UnitStance.mineAuto) && !ai.hasStance(ItemUnitStance.getByItem(targetItem))){
mining = false;
targetItem = null;
}
}
@Override
public void updateMovement(){
Building core = unit.closestCore();
@@ -23,13 +31,15 @@ public class MinerAI extends AIController{
unit.mineTile(null);
}
Item autoItem = unit.controller() instanceof CommandAI ai && ai.stance instanceof ItemUnitStance stance ? stance.item : null;
CommandAI ai = unit.controller() instanceof CommandAI a ? a : null;
if(mining){
if(autoItem != null){
targetItem = autoItem;
}else if(timer.get(timerTarget2, 60 * 4) || targetItem == null){
targetItem = unit.type.mineItems.min(i -> indexer.hasOre(i) && unit.canMine(i), i -> core.items.get(i));
if(timer.get(timerTarget2, 60 * 4) || targetItem == null){
if(ai != null && !ai.hasStance(UnitStance.mineAuto)){
targetItem = content.items().min(i -> indexer.hasOre(i) && unit.canMine(i) && ai.hasStance(ItemUnitStance.getByItem(i)), i -> core.items.get(i));
}else{
targetItem = unit.type.mineItems.min(i -> indexer.hasOre(i) && unit.canMine(i), i -> core.items.get(i));
}
}
//core full of the target item, do nothing

View File

@@ -12,7 +12,8 @@ import static mindustry.Vars.*;
public class AsyncCore{
//all processes to be executed each frame
public final Seq<AsyncProcess> processes = Seq.with(
new PhysicsProcess()
new PhysicsProcess(),
avoidance = new AvoidanceProcess()
);
//futures to be awaited

View File

@@ -0,0 +1,112 @@
package mindustry.async;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import java.util.*;
import static mindustry.Vars.*;
public class AvoidanceProcess implements AsyncProcess{
/** cached world size */
static int wwidth, wheight;
@Nullable int[] buffer1, buffer2;
volatile boolean swap;
IntSeq requests = new IntSeq();
@Nullable int[] avoidance;
boolean modified;
boolean active;
public @Nullable int[] getAvoidance(){
if(!active){
//lazily initialize and begin processing after this first request
buffer1 = new int[wwidth * wheight];
buffer2 = new int[wwidth * wheight];
active = true;
}
return avoidance;
}
@Override
public void init(){
wwidth = Vars.world.width();
wheight = Vars.world.height();
}
@Override
public void reset(){
buffer1 = buffer2 = avoidance = null;
swap = false;
modified = false;
active = false;
requests.clear();
}
@Override
public void begin(){
if(!active) return;
requests.clear();
avoidance = !swap ? buffer1 : buffer2;
for(var team : state.teams.present){
//only do avoidance if it's relevant to the team
if(team.team.isAI() && !team.team.rules().rtsAi){
for(var unit : team.units){
if(unit.collisionLayer() == PhysicsProcess.layerGround){
//scaling is oversized 2x because units need to avoid things that are at their origin tile
float scaling = 2f;
requests.add(Point2.pack(unit.tileX(), unit.tileY()), Float.floatToRawIntBits(unit.hitSize * unitCollisionRadiusScale / tilesize * scaling), unit.id);
}
}
}
}
}
@Override
public void process(){
//double buffering; one buffer is always valid (not being updated)
var buffer = swap ? buffer1 : buffer2;
swap = !swap;
if(buffer == null) return;
//technically, this is wrong, and will lead to flickering avoidance when all units are gone, but this doesn't matter because it's not being queried either way
if(modified){
Arrays.fill(buffer, 0);
}
modified = requests.size > 0;
int total = requests.size;
int[] items = requests.items;
for(int i = 0; i < total; i += 3){
int point = items[i], id = items[i + 2];
int rx = Point2.x(point), ry = Point2.y(point);
float rad = Float.intBitsToFloat(items[i + 1]);
float rad2 = rad * rad;
int r = Math.max(1, Mathf.ceil(rad));
for(int dx = -r; dx <= r; dx++){
for(int dy = -r; dy <= r; dy++){
int x = dx + rx, y = dy + ry;
if(x >= 0 && y >= 0 && x < wwidth && y < wheight && (dx*dx + dy*dy) <= rad2){
buffer[x + y * wwidth] = Math.max(buffer[x + y * wwidth], Integer.MAX_VALUE - id);
}
}
}
}
}
@Override
public boolean shouldProcess(){
return active;
}
}

View File

@@ -6264,7 +6264,7 @@ public class Blocks{
}};
tankAssembler = new UnitAssembler("tank-assembler"){{
requirements(Category.units, with(Items.thorium, 500, Items.oxide, 150, Items.carbide, 80, Items.silicon, 500));
requirements(Category.units, with(Items.thorium, 500, Items.oxide, 150, Items.carbide, 80, Items.silicon, 650));
regionSuffix = "-dark";
size = 5;
plans.add(
@@ -6274,12 +6274,12 @@ public class Blocks{
areaSize = 13;
researchCostMultiplier = 0.4f;
consumePower(3f);
consumePower(2.5f);
consumeLiquid(Liquids.cyanogen, 9f / 60f);
}};
shipAssembler = new UnitAssembler("ship-assembler"){{
requirements(Category.units, with(Items.carbide, 100, Items.oxide, 200, Items.tungsten, 500, Items.silicon, 800, Items.thorium, 400));
requirements(Category.units, with(Items.carbide, 100, Items.oxide, 200, Items.tungsten, 550, Items.silicon, 900, Items.thorium, 400));
regionSuffix = "-dark";
size = 5;
plans.add(
@@ -6288,12 +6288,12 @@ public class Blocks{
);
areaSize = 13;
consumePower(3f);
consumePower(2.5f);
consumeLiquid(Liquids.cyanogen, 12f / 60f);
}};
mechAssembler = new UnitAssembler("mech-assembler"){{
requirements(Category.units, with(Items.carbide, 200, Items.thorium, 600, Items.oxide, 200, Items.tungsten, 500, Items.silicon, 900));
requirements(Category.units, with(Items.carbide, 200, Items.thorium, 600, Items.oxide, 200, Items.tungsten, 550, Items.silicon, 1000));
regionSuffix = "-dark";
size = 5;
//TODO different reqs
@@ -6303,14 +6303,14 @@ public class Blocks{
);
areaSize = 13;
consumePower(3.5f);
consumePower(3f);
consumeLiquid(Liquids.cyanogen, 12f / 60f);
}};
//TODO requirements / only accept inputs
basicAssemblerModule = new UnitAssemblerModule("basic-assembler-module"){{
requirements(Category.units, with(Items.carbide, 300, Items.thorium, 500, Items.oxide, 200, Items.phaseFabric, 400));
consumePower(4f);
requirements(Category.units, with(Items.carbide, 300, Items.thorium, 500, Items.oxide, 250, Items.phaseFabric, 400));
consumePower(3.5f);
regionSuffix = "-dark";
researchCostMultiplier = 0.75f;
@@ -6360,7 +6360,7 @@ public class Blocks{
}};
payloadMassDriver = new PayloadMassDriver("payload-mass-driver"){{
requirements(Category.units, with(Items.tungsten, 120, Items.silicon, 120, Items.graphite, 50));
requirements(Category.units, with(Items.tungsten, 40, Items.silicon, 50, Items.graphite, 20));
regionSuffix = "-dark";
size = 3;
reload = 130f;
@@ -6372,13 +6372,13 @@ public class Blocks{
}};
largePayloadMassDriver = new PayloadMassDriver("large-payload-mass-driver"){{
requirements(Category.units, with(Items.thorium, 200, Items.tungsten, 200, Items.silicon, 200, Items.graphite, 100, Items.oxide, 30));
requirements(Category.units, with(Items.phaseFabric, 20, Items.tungsten, 200, Items.silicon, 200, Items.graphite, 100, Items.oxide, 30));
regionSuffix = "-dark";
size = 5;
reload = 130f;
chargeTime = 100f;
range = 1100f;
maxPayloadSize = 3.5f;
range = 2100f;
maxPayloadSize = 4f;
consumePower(3f);
}};
@@ -6388,24 +6388,24 @@ public class Blocks{
itemCapacity = 100;
consumePower(1f);
size = 3;
deconstructSpeed = 1f;
deconstructSpeed = 3f;
}};
deconstructor = new PayloadDeconstructor("deconstructor"){{
requirements(Category.units, with(Items.beryllium, 250, Items.oxide, 100, Items.silicon, 250, Items.carbide, 250));
requirements(Category.units, with(Items.beryllium, 250, Items.oxide, 100, Items.silicon, 250, Items.carbide, 50));
regionSuffix = "-dark";
itemCapacity = 250;
consumePower(3f);
size = 5;
deconstructSpeed = 2f;
deconstructSpeed = 6f;
}};
constructor = new Constructor("constructor"){{
requirements(Category.units, with(Items.silicon, 100, Items.beryllium, 150, Items.tungsten, 80));
requirements(Category.units, with(Items.silicon, 50, Items.beryllium, 75, Items.tungsten, 40));
regionSuffix = "-dark";
hasPower = true;
buildSpeed = 0.6f;
consumePower(2f);
consumePower(2.5f);
size = 3;
//TODO expand this list
filter = Seq.with(Blocks.tungstenWallLarge, Blocks.berylliumWallLarge, Blocks.carbideWallLarge, Blocks.reinforcedSurgeWallLarge, Blocks.reinforcedLiquidContainer, Blocks.reinforcedContainer, Blocks.beamNode);
@@ -6413,7 +6413,7 @@ public class Blocks{
//yes this block is pretty much useless
largeConstructor = new Constructor("large-constructor"){{
requirements(Category.units, with(Items.silicon, 150, Items.oxide, 150, Items.tungsten, 200, Items.phaseFabric, 40));
requirements(Category.units, with(Items.silicon, 150, Items.oxide, 100, Items.tungsten, 200, Items.thorium, 80));
regionSuffix = "-dark";
hasPower = true;
buildSpeed = 0.75f;
@@ -6421,11 +6421,11 @@ public class Blocks{
minBlockSize = 3;
size = 5;
consumePower(2f);
consumePower(3f);
}};
payloadLoader = new PayloadLoader("payload-loader"){{
requirements(Category.units, with(Items.graphite, 50, Items.silicon, 50, Items.tungsten, 80));
requirements(Category.units, with(Items.graphite, 80, Items.silicon, 160, Items.tungsten, 90));
regionSuffix = "-dark";
hasPower = true;
consumePower(2f);
@@ -6434,7 +6434,7 @@ public class Blocks{
}};
payloadUnloader = new PayloadUnloader("payload-unloader"){{
requirements(Category.units, with(Items.graphite, 50, Items.silicon, 50, Items.tungsten, 30));
requirements(Category.units, with(Items.graphite, 140, Items.silicon, 220, Items.tungsten, 180));
regionSuffix = "-dark";
hasPower = true;
consumePower(2f);

View File

@@ -415,6 +415,10 @@ public class Renderer implements ApplicationListener{
Groups.draw.draw(Drawc::draw);
if(drawDebugHitboxes){
DebugCollisionRenderer.draw();
}
Draw.reset();
Draw.flush();
Draw.sort(false);

View File

@@ -95,6 +95,10 @@ public abstract class UnlockableContent extends MappableContent{
uiIcon = Core.atlas.find(getContentType().name() + "-" + name + "-ui", fullIcon);
}
public boolean isBanned(){
return false;
}
public boolean isOnPlanet(@Nullable Planet planet){
return planet == null || planet == Planets.sun || shownPlanets.isEmpty() || shownPlanets.contains(planet);
}

View File

@@ -17,6 +17,7 @@ import mindustry.game.MapObjectives.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.logic.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
@@ -280,6 +281,15 @@ public class MapObjectivesDialog extends BaseDialog{
});
}));
setInterpreter(Alignment.class, int.class, (cont, name, type, field, remover, indexer, get, set) -> {
Alignment align = field.getAnnotation(Alignment.class);
name(cont, name, remover, indexer);
cont.button(b -> {
b.label(() -> LStatement.alignToName.get(get.get(), "center"));
b.clicked(() -> LStatement.showAlignSelect(b, get.get(), set::get, align.hor(), align.ver()));
}, () -> {});
});
// Types that use the default interpreter. It would be nice if all types could use it, but I don't know how to reliably prevent classes like [? extends Content] from using it.
for(var obj : MapObjectives.allObjectiveTypes) setInterpreter(obj.get().getClass(), defaultInterpreter());
for(var mark : MapObjectives.allMarkerTypes) setInterpreter(mark.get().getClass(), defaultInterpreter());

View File

@@ -59,7 +59,7 @@ public class Fires{
if(tile != null){
Fire fire = get(tile);
if(fire != null){
fire.time(fire.time + intensity * Time.delta);
fire.time += intensity * Time.delta;
Fx.steam.at(fire);
if(fire.time >= fire.lifetime){
Events.fire(Trigger.fireExtinguish);

View File

@@ -110,7 +110,7 @@ public class Puddles{
}
public static void register(Puddle puddle){
world.tiles.setPuddle(puddle.tile().array(), puddle);
world.tiles.setPuddle(puddle.tile.array(), puddle);
}
/** Reacts two liquids together at a location. */

View File

@@ -438,17 +438,19 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return heat;
}
/** Sets the time scale of the building to the given intensity, unless it's above that value */
public void applyBoost(float intensity, float duration){
//do not refresh time scale when getting a weaker intensity
//do not refresh time scale when getting a lower intensity
if(intensity >= this.timeScale - 0.001f){
timeScaleDuration = Math.max(timeScaleDuration, duration);
}
timeScale = Math.max(timeScale, intensity);
}
/** Sets the time scale of the building to the given intensity, unless it's below that value */
public void applySlowdown(float intensity, float duration){
//do not refresh time scale when getting a weaker intensity
if(intensity <= this.timeScale - 0.001f){
//do not refresh time scale when getting a higher intensity
if(intensity <= this.timeScale + 0.001f){
timeScaleDuration = Math.max(timeScaleDuration, duration);
}
timeScale = Math.min(timeScale, intensity);
@@ -1215,11 +1217,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
public void drawItemSelection(@Nullable UnlockableContent selection){
if(selection != null){
float dx = x - block.size * tilesize/2f, dy = y + block.size * tilesize/2f, s = iconSmall / 4f;
float dx = x - block.size * tilesize/2f, dy = y + block.size * tilesize/2f, s = iconSmall / 4f * selection.fullIcon.ratio(), h = iconSmall / 4f;
Draw.mixcol(Color.darkGray, 1f);
Draw.rect(selection.fullIcon, dx, dy - 1, s, s);
Draw.rect(selection.fullIcon, dx, dy - 1, s, h);
Draw.reset();
Draw.rect(selection.fullIcon, dx, dy, s, s);
Draw.rect(selection.fullIcon, dx, dy, s, h);
}
}

View File

@@ -18,7 +18,7 @@ import mindustry.world.meta.*;
import static mindustry.Vars.*;
@EntityDef(value = {Firec.class}, pooled = true)
@Component(base = true)
@Component(base = true, genInterface = false)
abstract class FireComp implements Timedc, Posc, Syncc, Drawc{
public static final int frames = 40, duration = 90;
@@ -88,7 +88,7 @@ abstract class FireComp implements Timedc, Posc, Syncc, Drawc{
//apply damage to nearby units & building
if((damageTimer += Time.delta) >= damageDelay){
damageTimer = 0f;
Puddlec p = Puddles.get(tile);
Puddle p = Puddles.get(tile);
puddleFlammability = p != null ? p.getFlammability() / 3f : 0;
if(damage){

View File

@@ -17,7 +17,7 @@ import static mindustry.Vars.*;
import static mindustry.entities.Puddles.*;
@EntityDef(value = {Puddlec.class}, pooled = true)
@Component(base = true)
@Component(base = true, genInterface = false)
abstract class PuddleComp implements Posc, Puddlec, Drawc, Syncc{
private static final Rect rect = new Rect(), rect2 = new Rect();

View File

@@ -12,7 +12,7 @@ import mindustry.ui.*;
/** Component/entity for labels in world space. Useful for servers. Does not save in files - create only on world load. */
@EntityDef(value = {WorldLabelc.class}, serialize = false)
@Component(base = true)
@Component(base = true, genInterface = false)
public abstract class WorldLabelComp implements Posc, Drawc, Syncc{
@Import int id;
@Import float x, y;
@@ -31,10 +31,10 @@ public abstract class WorldLabelComp implements Posc, Drawc, Syncc{
@Override
public void draw(){
drawAt(text, x, y, z, flags, fontSize);
drawAt(text, x, y, z, flags, fontSize, Align.center, Align.center);
}
public static void drawAt(String text, float x, float y, float layer, int flags, float fontSize){
public static void drawAt(String text, float x, float y, float layer, int flags, float fontSize, int align, int lineAlign){
Draw.z(layer);
float z = Drawf.text();
@@ -46,14 +46,32 @@ public abstract class WorldLabelComp implements Posc, Drawc, Syncc{
font.getData().setScale(0.25f / Scl.scl(1f) * fontSize);
layout.setText(font, text);
int border = (flags & flagBackground) != 0 ? 1 : 0;
if(Align.isBottom(align)){
y += layout.height + border * 1.5f;
}else if(Align.isTop(align)){
y -= border * 1.5f;
}else{
y += layout.height / 2;
}
if(Align.isLeft(align)){
x += layout.width / 2 + border;
}else if(Align.isRight(align)){
x -= layout.width / 2 + border;
}
if((flags & flagBackground) != 0){
Draw.color(0f, 0f, 0f, 0.3f);
Fill.rect(x, y - layout.height / 2, layout.width + 2, layout.height + 3);
Draw.color();
}
float tx = Align.isLeft(lineAlign) ? -layout.width * 0.5f : Align.isRight(lineAlign) ? layout.width * 0.5f : 0;
font.setColor(Color.white);
font.draw(text, x, y, 0, Align.center, false);
font.draw(text, x + tx, y, 0, lineAlign, false);
Draw.reset();
Pools.free(layout);

View File

@@ -4,6 +4,7 @@ import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.*;
import mindustry.async.*;
import mindustry.entities.*;
import mindustry.game.*;
import mindustry.gen.*;
@@ -51,6 +52,9 @@ public class AIController implements UnitController{
updateMovement();
}
/** Called when the parent CommandAI changes its stance. */
public void stanceChanged(){}
/**
* @return whether controller state should not be reset after reading.
* Do not override unless you know exactly what you are doing.
@@ -129,11 +133,15 @@ public class AIController implements UnitController{
}
public void pathfind(int pathTarget, boolean stopAtTargetTile){
pathfind(pathTarget, stopAtTargetTile, false);
}
public void pathfind(int pathTarget, boolean stopAtTargetTile, boolean avoidance){
int costType = unit.type.flowfieldPathType;
Tile tile = unit.tileOn();
if(tile == null) return;
Tile targetTile = pathfinder.getField(unit.team, costType, pathTarget).getNextTile(tile);
Tile targetTile = pathfinder.getField(unit.team, costType, pathTarget).getNextTile(tile, avoidance && unit.collisionLayer() == PhysicsProcess.layerGround ? unit.id : 0);
if((tile == targetTile && stopAtTargetTile) || !unit.canPass(targetTile.x, targetTile.y)) return;

View File

@@ -757,6 +757,8 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
public @Multiline String text = "frog";
public float fontSize = 1f, textHeight = 7f;
public @LabelFlag byte flags = WorldLabel.flagBackground | WorldLabel.flagOutline;
public @Alignment int textAlign = Align.center;
public @Alignment(ver = false) int lineAlign = Align.center;
public float radius = 6f, rotation = 0f;
public int sides = 4;
@@ -791,6 +793,15 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
this.textHeight = textHeight;
}
public ShapeTextMarker(String text, float x, float y, float radius, float rotation, float textHeight, int textAlign){
this.text = text;
this.pos.set(x, y);
this.radius = radius;
this.rotation = rotation;
this.textHeight = textHeight;
this.textAlign = textAlign;
}
public ShapeTextMarker(){}
@Override
@@ -812,7 +823,7 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
// font size cannot be 0
if(Mathf.equal(fontSize, 0f)) return;
WorldLabel.drawAt(fetchedText, pos.x, pos.y + radius * scaleFactor + textHeight * scaleFactor, drawLayer, flags, fontSize * scaleFactor);
WorldLabel.drawAt(fetchedText, pos.x, pos.y + radius * scaleFactor + textHeight * scaleFactor, drawLayer, flags, fontSize * scaleFactor, textAlign, lineAlign);
}
@Override
@@ -823,6 +834,8 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
switch(type){
case fontSize -> fontSize = (float)p1;
case textHeight -> textHeight = (float)p1;
case textAlign -> textAlign = (int)p1;
case lineAlign -> lineAlign = (int)p1;
case outline -> flags = (byte)Pack.bitmask(flags, WorldLabel.flagOutline, !Mathf.equal((float)p1, 0f));
case labelFlags -> flags = (byte)Pack.bitmask(flags, WorldLabel.flagBackground, !Mathf.equal((float)p1, 0f));
case radius -> radius = (float)p1;
@@ -980,6 +993,9 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
public @Multiline String text = "uwu";
public float fontSize = 1f;
public @LabelFlag byte flags = WorldLabel.flagBackground | WorldLabel.flagOutline;
public @Alignment int textAlign = Align.center;
public @Alignment(ver = false) int lineAlign = Align.center;
// Cached localized text.
private transient String fetchedText;
@@ -1006,7 +1022,7 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
fetchedText = fetchText(text);
}
WorldLabel.drawAt(fetchedText, pos.x, pos.y, drawLayer, flags, fontSize * scaleFactor);
WorldLabel.drawAt(fetchedText, pos.x, pos.y, drawLayer, flags, fontSize * scaleFactor, textAlign, lineAlign);
}
@Override
@@ -1016,6 +1032,8 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
if(!Double.isNaN(p1)){
switch(type){
case fontSize -> fontSize = (float)p1;
case textAlign -> textAlign = (int)p1;
case lineAlign -> lineAlign = (int)p1;
case outline -> flags = (byte)Pack.bitmask(flags, WorldLabel.flagOutline, !Mathf.equal((float)p1, 0f));
case labelFlags -> flags = (byte)Pack.bitmask(flags, WorldLabel.flagBackground, !Mathf.equal((float)p1, 0f));
}
@@ -1317,6 +1335,14 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
@Retention(RUNTIME)
public @interface LabelFlag{}
/** For {@code int}; treats it as an alignment from {@link Align} */
@Target(FIELD)
@Retention(RUNTIME)
public @interface Alignment{
boolean hor() default true;
boolean ver() default true;
}
/** For {@link UnlockableContent}; filters all un-researchable content. */
@Target(FIELD)
@Retention(RUNTIME)
@@ -1341,4 +1367,5 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
@Target(FIELD)
@Retention(RUNTIME)
public @interface TilePos{}
}

View File

@@ -0,0 +1,93 @@
package mindustry.graphics;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.core.*;
import mindustry.gen.*;
import mindustry.world.*;
import static arc.Core.*;
import static mindustry.Vars.*;
public class DebugCollisionRenderer{
static final float[] edges = {
1, -1,
1, 1,
-1, 1,
-1, -1,
};
public static void draw(){
Rect rect = camera.bounds(new Rect());
Draw.draw(Layer.overlayUI, () -> {
//hitboxes
Draw.color(Color.green, 0.3f);
Groups.draw.each(d -> {
if(d instanceof Hitboxc h && rect.overlaps(Tmp.r1.setCentered(d.x(), d.y(), d.clipSize()))){
Fill.square(d.x(), d.y(), h.hitSize()/2f);
}
});
//tile hitboxes for units
Lines.stroke(0.4f, Color.magenta);
int rx = Mathf.clamp((int)(Core.camera.width / tilesize / 2) + 1, 0, world.width()/2);
int ry = Mathf.clamp((int)(Core.camera.height / tilesize / 2) + 1, 0, world.height()/2);
for(int x = -rx; x <= rx; x++){
for(int y = -ry; y <= ry; y++){
int wx = World.toTile(Core.camera.position.x) + x;
int wy = World.toTile(Core.camera.position.y) + y;
Tile tile = world.tile(wx, wy);
if(tile != null && tile.solid()){
for(int i = 0; i < 4; i++){
Tile other = tile.nearby(i);
if(other == null || !other.solid()){
Lines.line(
wx * tilesize + edges[i*2] * tilesize/2f,
wy * tilesize + edges[i*2+1] * tilesize/2f,
wx * tilesize + edges[((i + 1) % 4)*2] * tilesize/2f,
wy * tilesize + edges[((i + 1) % 4)*2+1] * tilesize/2f
);
}
}
}
if(debugDrawAvoidance && tile != null){
int[] avoid = avoidance.getAvoidance();
if(avoid != null && avoid[tile.array()] != 0){
Draw.color(0f, 1f, 1f, 0.25f);
Fill.square(tile.worldx(), tile.worldy(), 4f);
}
}
}
}
Groups.draw.each(d -> {
if(d instanceof Unit u && rect.overlaps(Tmp.r1.setCentered(u.x, u.y, d.clipSize())) && !u.isFlying()){
u.hitboxTile(Tmp.r1);
Lines.rect(Tmp.r1);
}
});
//physics hitboxes
Lines.stroke(0.5f);
Draw.color(Color.red, 0.5f);
Groups.draw.each(d -> {
if(d instanceof Unit u && rect.overlaps(Tmp.r1.setCentered(u.x, u.y, u.clipSize()))){
Lines.circle(u.x, u.y, u.hitSize * unitCollisionRadiusScale);
}
});
Draw.reset();
});
Draw.reset();
}
}

View File

@@ -35,7 +35,6 @@ public class Binding{
schematicFlipY = KeyBind.add("schematic_flip_y", KeyCode.x),
schematicMenu = KeyBind.add("schematic_menu", KeyCode.t),
commandMode = KeyBind.add("command_mode", KeyCode.shiftLeft, "command"),
commandQueue = KeyBind.add("command_queue", KeyCode.mouseMiddle),
createControlGroup = KeyBind.add("create_control_group", KeyCode.controlLeft),
@@ -103,7 +102,8 @@ public class Binding{
chatHistoryNext = KeyBind.add("chat_history_next", KeyCode.down),
chatScroll = KeyBind.add("chat_scroll", new Axis(KeyCode.scroll)),
chatMode = KeyBind.add("chat_mode", KeyCode.tab),
console = KeyBind.add("console", KeyCode.f8)
console = KeyBind.add("console", KeyCode.f8),
debugHitboxes = KeyBind.add("debug_hitboxes", KeyCode.unset)
;
//dummy static class initializer

View File

@@ -234,6 +234,10 @@ public class DesktopInput extends InputHandler{
boolean detached = settings.getBool("detach-camera", false);
if(!scene.hasField() && !scene.hasDialog()){
if(input.keyTap(Binding.debugHitboxes)){
drawDebugHitboxes = !drawDebugHitboxes;
}
if(input.keyTap(Binding.detachCamera)){
settings.put("detach-camera", detached = !detached);
if(!detached){

View File

@@ -372,15 +372,18 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
//make sure its current stance is valid with its current command
stancesOut.clear();
unit.type.getUnitStances(unit, stancesOut);
if(stancesOut.size > 0 && !stancesOut.contains(ai.stance)){
ai.stance = stancesOut.first();
for(var stance : content.unitStances()){
//disable stances that the unit does not support anymore (TODO: this is slow!)
if(ai.hasStance(stance) && !stancesOut.contains(stance)){
ai.disableStance(stance);
}
}
}
}
}
@Remote(called = Loc.server, targets = Loc.both, forward = true)
public static void setUnitStance(Player player, int[] unitIds, UnitStance stance){
public static void setUnitStance(Player player, int[] unitIds, UnitStance stance, boolean enable){
if(player == null || unitIds == null || stance == null) return;
if(net.server() && !netServer.admins.allowAction(player, ActionType.commandUnits, event -> {
@@ -395,7 +398,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
if(stance == UnitStance.stop){ //not a real stance, just cancels orders
ai.clearCommands();
}else if(unit.type.allowStance(unit, stance)){
ai.stance = stance;
//if toggle is not allowed, the stance will always be set to true when pressed
ai.setStance(stance, !stance.toggle || enable);
}
unit.lastCommanded = player.coloredName();
}

View File

@@ -362,7 +362,7 @@ public class TypeIO{
public static UnitStance readStance(Reads read){
int val = read.ub();
//never returns null
return val == 255 || val >= content.unitStances().size ? UnitStance.shoot : content.unitStance(val);
return val == 255 || val >= content.unitStances().size ? UnitStance.stop : content.unitStance(val);
}
public static void writeEntity(Writes write, Entityc entity){
@@ -523,7 +523,7 @@ public class TypeIO{
write.b(3);
write.i(logic.controller.pos());
}else if(control instanceof CommandAI ai){
write.b(8);
write.b(9);
write.bool(ai.attackTarget != null);
write.bool(ai.targetPos != null);
@@ -559,7 +559,16 @@ public class TypeIO{
}
}
writeStance(write, ai.stance);
int count = content.unitStances().count(ai::hasStance);
write.b(count);
for(var stance : content.unitStances()){
if(ai.hasStance(stance)){
writeStance(write, stance);
}
}
}else if(control instanceof AssemblerAI){ //hate
write.b(5);
}else{
@@ -591,8 +600,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, type 7 is the one with the command queue, 8 adds a stance
}else if(type == 4 || type == 6 || type == 7 || type == 8){
//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, 8 adds a stance, 9 adds multiple stances
}else if(type == 4 || type == 6 || type == 7 || type == 8 || type == 9){
CommandAI ai = prev instanceof CommandAI pai ? pai : new CommandAI();
boolean hasAttack = read.bool(), hasPos = read.bool();
@@ -616,14 +625,14 @@ public class TypeIO{
ai.attackTarget = null;
}
if(type == 6 || type == 7 || type == 8){
if(type == 6 || type == 7 || type == 8 || type == 9){
byte id = read.b();
ai.command = id < 0 ? null : content.unitCommand(id);
if(ai.command == null) ai.command = UnitCommand.moveCommand;
}
//command queue only in type 7/8
if(type == 7 || type == 8){
if(type == 7 || type == 8 || type == 9){
ai.commandQueue.clear();
int length = read.ub();
for(int i = 0; i < length; i++){
@@ -646,7 +655,12 @@ public class TypeIO{
}
if(type == 8){
ai.stance = readStance(read);
ai.setStance(readStance(read));
}else if(type == 9){
int stances = read.ub();
for(int i = 0; i < stances; i++){
ai.setStance(readStance(read));
}
}
return ai;

View File

@@ -28,6 +28,17 @@ public enum ConditionOp{
this.objFunction = objFunction;
}
public boolean test(LVar va, LVar vb){
if(this == ConditionOp.strictEqual){
return va.isobj == vb.isobj && ((va.isobj && va.objval == vb.objval) || (!va.isobj && va.numval == vb.numval));
}
if(objFunction != null && va.isobj && vb.isobj){
//use object function if both are objects
return objFunction.get(va.obj(), vb.obj());
}
return function.get(va.num(), vb.num());
}
@Override
public String toString(){
return symbol;

View File

@@ -147,6 +147,8 @@ public class GlobalVars{
put("@" + sensor.name(), sensor);
}
LStatement.nameToAlign.each((name, align) -> put("@" + name, align));
logicIdToContent = new UnlockableContent[ContentType.all.length][];
contentIdToLogicId = new int[ContentType.all.length][];

View File

@@ -40,7 +40,7 @@ public enum LAccess{
cameraHeight,
displayWidth,
displayHeight,
bufferUsage,
bufferSize,
operations,
size,
solid,

View File

@@ -25,10 +25,8 @@ import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.blocks.logic.*;
import mindustry.world.blocks.logic.CanvasBlock.*;
import mindustry.world.blocks.logic.LogicBlock.*;
import mindustry.world.blocks.logic.LogicDisplay.*;
import mindustry.world.blocks.logic.MemoryBlock.*;
import mindustry.world.blocks.logic.MessageBlock.*;
import mindustry.world.blocks.payloads.*;
import mindustry.world.meta.*;
@@ -516,14 +514,14 @@ public class LExecutor{
@Override
public void run(LExecutor exec){
Object obj = target.obj();
if(obj instanceof Building b && (exec.privileged || (b.team == exec.team && exec.linkIds.contains(b.id)))){
if(obj instanceof Building b && (exec.privileged || (exec.build != null && exec.build.validLink(b)))){
if(type == LAccess.enabled && !p1.bool()){
b.lastDisabler = exec.build;
}
if(type == LAccess.enabled && p1.bool()){
b.noSleep();
if(type == LAccess.enabled){
if(p1.bool()){
b.noSleep();
}else{
b.lastDisabler = exec.build;
}
}
if(type.isObj && p1.isobj){
@@ -568,22 +566,15 @@ public class LExecutor{
@Override
public void run(LExecutor exec){
int address = position.numi();
Building from = target.building();
if(from instanceof MemoryBuild mem && (exec.privileged || (from.team == exec.team && !mem.block.privileged))){
output.setnum(address < 0 || address >= mem.memory.length ? 0 : mem.memory[address]);
}else if(from instanceof LogicBuild logic && (exec.privileged || (from.team == exec.team && !from.block.privileged)) && position.isobj && position.objval instanceof String name){
LVar fromVar = logic.executor.optionalVar(name);
if(fromVar != null && !output.constant){
output.objval = fromVar.objval;
output.numval = fromVar.numval;
output.isobj = fromVar.isobj;
Object targetObj = target.obj();
if(targetObj instanceof LReadable read){
if(!read.readable(exec)) return;
read.read(position, output);
}else{
int address = position.numi();
if(targetObj instanceof CharSequence str){
output.setnum(address < 0 || address >= str.length() ? Double.NaN : (int)str.charAt(address));
}
}else if(target.isobj && target.objval instanceof CharSequence str){
output.setnum(address < 0 || address >= str.length() ? Double.NaN : (int)str.charAt(address));
}else if(from instanceof CanvasBuild canvas && (exec.privileged || (from.team == exec.team))){
output.setnum(canvas.getPixel(address));
}
}
}
@@ -602,20 +593,10 @@ public class LExecutor{
@Override
public void run(LExecutor exec){
int address = position.numi();
Building from = target.building();
if(from instanceof MemoryBuild mem && (exec.privileged || (from.team == exec.team && !mem.block.privileged)) && address >= 0 && address < mem.memory.length){
mem.memory[address] = value.num();
}else if(from instanceof LogicBuild logic && (exec.privileged || (from.team == exec.team && !from.block.privileged)) && position.isobj && position.objval instanceof String name){
LVar toVar = logic.executor.optionalVar(name);
if(toVar != null && !toVar.constant){
toVar.objval = value.objval;
toVar.numval = value.numval;
toVar.isobj = value.isobj;
}
}else if(from instanceof CanvasBuild canvas && (exec.privileged || (from.team == exec.team))){
canvas.setPixel(address, value.numi());
Object targetObj = target.obj();
if(targetObj instanceof LWritable write){
if(!write.writable(exec)) return;
write.write(position, value);
}
}
}
@@ -658,7 +639,7 @@ public class LExecutor{
}
}
}else{
if(target instanceof CharSequence seq && sense == LAccess.size){
if(target instanceof CharSequence seq && (sense == LAccess.size || sense == LAccess.bufferSize)){
to.setnum(seq.length());
return;
}
@@ -785,15 +766,7 @@ public class LExecutor{
@Override
public void run(LExecutor exec){
if(!to.constant){
if(from.isobj){
to.objval = from.objval;
to.isobj = true;
}else{
to.numval = LVar.invalid(from.numval) ? 0 : from.numval;
to.isobj = false;
}
}
if(!to.constant) to.set(from);
}
}
@@ -829,6 +802,28 @@ public class LExecutor{
}
}
public static class SelectI implements LInstruction{
public ConditionOp op = ConditionOp.notEqual;
public LVar result, comp0, comp1, a, b;
public SelectI(ConditionOp op, LVar result, LVar comp0, LVar comp1, LVar a, LVar b){
this.op = op;
this.result = result;
this.comp0 = comp0;
this.comp1 = comp1;
this.a = a;
this.b = b;
}
public SelectI(){}
@Override
public void run(LExecutor exec){
if(result.constant) return;
result.set(op.test(comp0, comp1) ? a : b);
}
}
public static class EndI implements LInstruction{
@Override
@@ -883,7 +878,7 @@ public class LExecutor{
int advance = (int)data.spaceXadvance, lineHeight = (int)data.lineHeight;
int xOffset, yOffset;
int align = p1.id; //p1 is not a variable, it's a raw align value. what a massive hack
int align = p1.numi();
int maxWidth = 0, lines = 1, lineWidth = 0;
for(int i = 0; i < str.length(); i++){
@@ -1134,23 +1129,8 @@ public class LExecutor{
@Override
public void run(LExecutor exec){
if(address != -1){
LVar va = value;
LVar vb = compare;
boolean cmp;
if(op == ConditionOp.strictEqual){
cmp = va.isobj == vb.isobj && ((va.isobj && va.objval == vb.objval) || (!va.isobj && va.numval == vb.numval));
}else if(op.objFunction != null && va.isobj && vb.isobj){
//use object function if both are objects
cmp = op.objFunction.get(value.obj(), compare.obj());
}else{
cmp = op.function.get(value.num(), compare.num());
}
if(cmp){
exec.counter.numval = address;
}
if(address != -1 && op.test(value, compare)){
exec.counter.numval = address;
}
}
}

View File

@@ -18,6 +18,8 @@ public enum LMarkerControl{
flushText("fetch"),
fontSize("size"),
textHeight("height"),
textAlign("align"),
lineAlign("align"),
labelFlags("background", "outline"),
texture("printFlush", "name"),
textureSize("width", "height"),

View File

@@ -0,0 +1,6 @@
package mindustry.logic;
public interface LReadable{
boolean readable(LExecutor exec);
void read(LVar position, LVar output);
}

View File

@@ -23,6 +23,25 @@ import static mindustry.logic.LCanvas.*;
* A statement is an intermediate representation of an instruction, to be used mostly in UI.
* Contains all relevant variable information. */
public abstract class LStatement{
private static final String[] aligns = {"topLeft", "top", "topRight", "left", "center", "right", "bottomLeft", "bottom", "bottomRight"};
public static final ObjectMap<String, Integer> nameToAlign = ObjectMap.of(
"center", Align.center,
"top", Align.top,
"bottom", Align.bottom,
"left", Align.left,
"right", Align.right,
"topLeft", Align.topLeft,
"topRight", Align.topRight,
"bottomLeft", Align.bottomLeft,
"bottomRight", Align.bottomRight
);
public static final IntMap<String> alignToName = new IntMap<>();
static {
nameToAlign.each((k, v) -> alignToName.put(v, k));
}
public transient @Nullable StatementElem elem;
public abstract void build(Table table);
@@ -175,7 +194,36 @@ public abstract class LStatement{
showSelect(b, values, current, getter, 4, c -> {});
}
protected void showSelectTable(Button b, Cons2<Table, Runnable> hideCons){
protected void fieldAlignSelect(Table t, Prov<String> get, Cons<String> set, boolean hor, boolean ver) {
t.button(b -> {
b.image(Icon.pencilSmall);
b.clicked(() -> {
var current = get.get();
showAlignSelect(b, current.startsWith("@") ? nameToAlign.get(current.substring(1), -1) : -1, align -> set.get("@" + alignToName.get(align)), hor, ver);
});
}, Styles.logict, () -> {}).size(40f).color(t.color).left().padLeft(-10);
}
public static void showAlignSelect(Button b, int current, Intc setter, boolean hor, boolean ver) {
showSelectTable(b, (t, hide) -> {
t.defaults().size(150f, 40f);
int i = 0;
for(String align : aligns){
int val = nameToAlign.get(align);
if(!hor && !Align.isCenterHorizontal(val)) continue;
if(!ver && !Align.isCenterVertical(val)) continue;
t.button(align, Styles.logicTogglet, () -> {
setter.get(val);
hide.run();
}).checked(current == nameToAlign.get(align)).grow();
if (++i % 3 == 0) t.row();
}
});
}
protected static void showSelectTable(Button b, Cons2<Table, Runnable> hideCons){
Table t = new Table(Tex.paneSolid){
@Override
public float getPrefHeight(){

View File

@@ -6,7 +6,6 @@ import arc.graphics.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.annotations.Annotations.*;
@@ -123,19 +122,6 @@ public class LStatements{
@RegisterStatement("draw")
public static class DrawStatement extends LStatement{
static final String[] aligns = {"center", "top", "bottom", "left", "right", "topLeft", "topRight", "bottomLeft", "bottomRight"};
//yes, boxing Integer is gross but this is easier to construct and Integers <128 don't allocate anyway
static final ObjectMap<String, Integer> nameToAlign = ObjectMap.of(
"center", Align.center,
"top", Align.top,
"bottom", Align.bottom,
"left", Align.left,
"right", Align.right,
"topLeft", Align.topLeft,
"topRight", Align.topRight,
"bottomLeft", Align.bottomLeft,
"bottomRight", Align.bottomRight
);
public GraphicsType type = GraphicsType.clear;
public String x = "0", y = "0", p1 = "0", p2 = "0", p3 = "0", p4 = "0";
@@ -165,7 +151,7 @@ public class LStatements{
}
if(type == GraphicsType.print){
p1 = "bottomLeft";
p1 = "@bottomLeft";
}
rebuild(table);
@@ -253,13 +239,11 @@ public class LStatements{
row(s);
s.add("align ");
s.button(b -> {
b.label(() -> nameToAlign.containsKey(p1) ? p1 : "bottomLeft");
b.clicked(() -> showSelect(b, aligns, p1, t -> {
p1 = t;
}, 2, cell -> cell.size(165, 50)));
}, Styles.logict, () -> {}).size(165, 40).color(s.color).left().padLeft(2);
fields(s, "align", p1, v -> p1 = v);
fieldAlignSelect(s, () -> p1, v -> {
p1 = v;
rebuild(table);
}, true, true);
}
case translate, scale -> {
fields(s, "x", x, v -> x = v);
@@ -278,12 +262,15 @@ public class LStatements{
if(type == GraphicsType.color && p2.equals("0")){
p2 = "255";
}
if(type == GraphicsType.print && nameToAlign.get(p1) != null){
p1 = "@" + p1;
}
}
@Override
public LInstruction build(LAssembler builder){
return new DrawI((byte)type.ordinal(), builder.var(x), builder.var(y),
type == GraphicsType.print ? new LVar(p1, nameToAlign.get(p1, Align.bottomLeft), true) : builder.var(p1), builder.var(p2), builder.var(p3), builder.var(p4));
return new DrawI((byte)type.ordinal(), builder.var(x), builder.var(y), builder.var(p1), builder.var(p2), builder.var(p3), builder.var(p4));
}
@Override
@@ -793,6 +780,55 @@ public class LStatements{
}
}
@RegisterStatement("select")
public static class SelectStatement extends LStatement{
public String result = "result";
public ConditionOp op = ConditionOp.notEqual;
public String comp0 = "x", comp1 = "false", a = "a", b = "b";
@Override
public void build(Table table){
rebuild(table);
}
private void rebuild(Table table){
table.clearChildren();
table.left();
table.table(t -> {
t.setColor(table.color);
field(t, result, str -> result = str);
t.add(" = if ");
JumpStatement.addOp(this, t, op, o -> {
op = o;
rebuild(table);
}, comp0, str -> comp0 = str, comp1, str -> comp1 = str);
}).left();
table.row();
table.table(t -> {
t.setColor(table.color);
t.add("then ");
field(t, a, str -> a = str);
t.add(" else ");
field(t, b, str -> b = str);
}).left();
}
@Override
public LInstruction build(LAssembler builder){
return new SelectI(op, builder.var(result), builder.var(comp0), builder.var(comp1), builder.var(a), builder.var(b));
}
@Override
public LCategory category(){
return LCategory.operation;
}
}
@RegisterStatement("wait")
public static class WaitStatement extends LStatement{
public String value = "0.5";
@@ -984,17 +1020,22 @@ public class LStatements{
table.clearChildren();
table.setColor(last);
if(op != ConditionOp.always) field(table, value, str -> value = str);
addOp(this, table, op, o -> {
op = o;
rebuild(table);
}, value, str -> value = str, compare, str -> compare = str);
}
table.button(b -> {
b.label(() -> op.symbol);
b.clicked(() -> showSelect(b, ConditionOp.all, op, o -> {
op = o;
rebuild(table);
}));
}, Styles.logict, () -> {}).size(op == ConditionOp.always ? 80f : 48f, 40f).pad(4f).color(table.color);
public static void addOp(LStatement st, Table t, ConditionOp op, Cons<ConditionOp> getter, String comp0, Cons<String> set0, String comp1, Cons<String> set2){
if(op != ConditionOp.always) st.field(t, comp0, set0);
if(op != ConditionOp.always) field(table, compare, str -> compare = str);
t.button(b -> {
b.add(op.symbol);
b.clicked(() -> st.showSelect(b, ConditionOp.all, op, getter));
}, Styles.logict, () -> {
}).size(op == ConditionOp.always ? 80f : 48f, 40f).pad(4f).color(t.color);
if(op != ConditionOp.always) st.field(t, comp1, set2);
}
//elements need separate conversion logic
@@ -2345,6 +2386,11 @@ public class LStatements{
}).width(240f).left();
}));
}, Styles.logict, () -> {}).size(40f).padLeft(-11).color(table.color);
}else if(type == LMarkerControl.textAlign || type == LMarkerControl.lineAlign){
fieldAlignSelect(t, () -> p1, v -> {
p1 = v;
rebuild(table);
}, true, type != LMarkerControl.lineAlign);
}
});

View File

@@ -101,6 +101,12 @@ public class LVar{
isobj = true;
}
public void set(LVar other){
isobj = other.isobj;
objval = other.objval;
numval = other.numval;
}
public static boolean invalid(double d){
return Double.isNaN(d) || Double.isInfinite(d);
}

View File

@@ -0,0 +1,6 @@
package mindustry.logic;
public interface LWritable{
boolean writable(LExecutor exec);
void write(LVar position, LVar value);
}

View File

@@ -798,7 +798,7 @@ public class Mods implements Loadable{
public void reload(){
newImports.each(this::updateDependencies);
newImports.remove(m -> m.missingDependencies.isEmpty() && m.softDependencies.isEmpty());
newImports.removeAll(m -> m.missingDependencies.isEmpty() && m.softDependencies.isEmpty());
if(newImports.any()){
checkDependencies(newImports, newImports.contains(m -> m.softDependencies.any()));

View File

@@ -700,6 +700,7 @@ public class UnitType extends UnlockableContent implements Senseable{
return (envEnabled & env) != 0 && (envDisabled & env) == 0 && (envRequired == 0 || (envRequired & env) == envRequired);
}
@Override
public boolean isBanned(){
return state.rules.isBanned(this);
}
@@ -1038,7 +1039,7 @@ public class UnitType extends UnlockableContent implements Senseable{
if(stances.size == 0){
if(canAttack){
stances.addAll(UnitStance.stop, UnitStance.shoot, UnitStance.holdFire, UnitStance.pursueTarget, UnitStance.patrol);
stances.addAll(UnitStance.stop, UnitStance.holdFire, UnitStance.pursueTarget, UnitStance.patrol);
if(!flying){
stances.add(UnitStance.ram);
}

View File

@@ -17,7 +17,6 @@ import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.world.*;
import static arc.Core.*;
import static mindustry.Vars.*;
@@ -95,7 +94,7 @@ public class DatabaseDialog extends BaseDialog{
if(++i % 10 == 0) t.row();
}
}).row();;
}).row();
for(int j = 0; j < allContent.length; j++){
ContentType type = ContentType.all[j];
@@ -106,6 +105,11 @@ public class DatabaseDialog extends BaseDialog{
if(array.size == 0) continue;
//sorting only makes sense when in-game; otherwise, banned blocks can't exist
if(state.isGame()){
array.sort(Structs.comps(Structs.comparingBool(UnlockableContent::isBanned), Structs.comparingInt(u -> u.id)));
}
all.add("@content." + type.name() + ".name").growX().left().color(Pal.accent);
all.row();
all.image().growX().pad(5).padLeft(0).padRight(0).height(3).color(Pal.accent);
@@ -116,13 +120,11 @@ public class DatabaseDialog extends BaseDialog{
int cols = (int)Mathf.clamp((Core.graphics.getWidth() - Scl.scl(30)) / Scl.scl(32 + 12), 1, 22);
int count = 0;
for(int i = 0; i < array.size; i++){
UnlockableContent unlock = array.get(i);
for(var unlock : array){
Image image = unlocked(unlock) ? new Image(new TextureRegionDrawable(unlock.uiIcon), mobile ? Color.white : Color.lightGray).setScaling(Scaling.fit) : new Image(Icon.lock, Pal.gray);
//banned cross
if(state.isGame() && (unlock instanceof UnitType u && u.isBanned() || unlock instanceof Block b && state.rules.isBanned(b))){
if(state.isGame() && unlock.isBanned()){
list.stack(image, new Image(Icon.cancel){{
setColor(Color.scarlet);
touchable = Touchable.disabled;

View File

@@ -1274,7 +1274,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{
if(sector.isAttacked()){
addSurvivedInfo(sector, stable, false);
}else if(sector.hasBase() && sector.planet.campaignRules.sectorInvasion && sector.near().contains(Sector::hasEnemyBase)){
}else if(sector.hasBase() && sector.planet.campaignRules.sectorInvasion && sector.near().contains(s -> s.hasEnemyBase() && (s.preset == null || !s.preset.requireUnlock))){
stable.add("@sectors.vulnerable");
stable.row();
}else if(!sector.hasBase() && sector.hasEnemyBase()){

View File

@@ -474,6 +474,9 @@ public class SettingsMenuDialog extends BaseDialog{
}
graphics.checkPref("minimap", !mobile);
graphics.checkPref("smoothcamera", true);
if(!mobile){
graphics.checkPref("detach-camera", false);
}
graphics.checkPref("position", false);
if(!mobile){
graphics.checkPref("mouseposition", false);

View File

@@ -581,7 +581,7 @@ public class PlacementFragment{
for(var stance : stances){
coms.button(stance.getIcon(), Styles.clearNoneTogglei, () -> {
Call.setUnitStance(player, units.mapInt(un -> un.id, un -> un.type.allowStance(un, stance)).toArray(), stance);
Call.setUnitStance(player, units.mapInt(un -> un.id, un -> un.type.allowStance(un, stance)).toArray(), stance, !activeStances.get(stance.id));
}).checked(i -> activeStances.get(stance.id)).size(50f).tooltip(stance.localized(), true);
if(++scol % 6 == 0) coms.row();
@@ -604,7 +604,7 @@ public class PlacementFragment{
for(var unit : control.input.selectedUnits){
if(unit.controller() instanceof CommandAI cmd){
activeCommands.set(cmd.command.id);
activeStances.set(cmd.stance.id);
activeStances.set(cmd.stances);
}
stancesOut.clear();
@@ -631,7 +631,7 @@ public class PlacementFragment{
for(UnitStance stance : stances){
//first stance must always be the stop stance
if(stance.keybind != null && Core.input.keyTap(stance.keybind)){
Call.setUnitStance(player, control.input.selectedUnits.mapInt(un -> un.id, un -> un.type.allowStance(un, stance)).toArray(), stance);
Call.setUnitStance(player, control.input.selectedUnits.mapInt(un -> un.id, un -> un.type.allowStance(un, stance)).toArray(), stance, !activeStances.get(stance.id));
}
}

View File

@@ -923,11 +923,16 @@ public class Block extends UnlockableContent implements Senseable{
}
public boolean isVisible(){
return !isHidden() && (state.rules.editor || (!state.rules.hideBannedBlocks || !state.rules.isBanned(this)));
return !isHidden() && (state.rules.editor || (!state.rules.hideBannedBlocks || !isBanned()));
}
public boolean isPlaceable(){
return isVisible() && (!state.rules.isBanned(this) || state.rules.editor) && supportsEnv(state.rules.env);
return isVisible() && (!isBanned() || state.rules.editor) && supportsEnv(state.rules.env);
}
@Override
public boolean isBanned(){
return state.rules.isBanned(this);
}
/** @return whether this block supports a specific environment. */

View File

@@ -94,7 +94,7 @@ public class LiquidTurret extends Turret{
var fire = Fires.get(x + tx, y + ty);
float dst = fire == null ? 0 : dst2(fire);
//do not extinguish fires on other team blocks
if(other != null && fire != null && Fires.has(other.x, other.y) && dst <= range * range && (result == null || dst < mindst) && (other.build == null || other.team() == team)){
if(other != null && fire != null && other.build != this && Fires.has(other.x, other.y) && dst <= range * range && (result == null || dst < mindst) && (other.build == null || other.team() == team)){
result = fire;
mindst = dst;
}

View File

@@ -96,8 +96,7 @@ public class DirectionalUnloader extends Block{
front.handleItem(this, item);
back.items.remove(item, 1);
back.itemTaken(item);
offset ++;
offset %= itemc;
offset = item.id + 1;
break;
}
}

View File

@@ -376,6 +376,11 @@ public class Floor extends Block{
return (other.realBlendId(otherTile) > realBlendId(tile) || edges(tile.x, tile.y) == null);
}
/** @return whether being on this floor can damage a unit */
public boolean damages(){
return damageTaken > 0f || (status != null && status.damage > 0f);
}
public static class UpdateRenderState{
public Tile tile;
public Floor floor;

View File

@@ -146,7 +146,7 @@ public class CanvasBlock extends Block{
return result;
}
public class CanvasBuild extends Building{
public class CanvasBuild extends Building implements LReadable, LWritable{
public @Nullable Texture texture;
public byte[] data = new byte[Mathf.ceil(canvasSize * canvasSize * bitsPerPixel / 8f)];
public int blending;
@@ -226,6 +226,25 @@ public class CanvasBlock extends Block{
}
}
public boolean readable(LExecutor exec){
return exec.privileged || this.team == exec.team;
}
@Override
public void read(LVar position, LVar output){
output.setnum(getPixel(position.numi()));
}
@Override
public boolean writable(LExecutor exec){
return exec.privileged || this.team == exec.team;
}
@Override
public void write(LVar position, LVar value){
setPixel(position.numi(), value.numi());
}
boolean blends(Tile other){
return other != null && other.build != null && other.build.block == block && other.build.tileX() == other.x && other.build.tileY() == other.y;
}

View File

@@ -82,17 +82,11 @@ public class LogicBlock extends Block{
int x = lbuild.tileX(), y = lbuild.tileY();
LogicLink link = entity.links.find(l -> l.x == x && l.y == y);
String bname = getLinkName(lbuild.block);
if(link != null){
link.active = !link.active;
//find a name when the base name differs (new block type)
if(!link.name.startsWith(bname)){
link.name = "";
link.name = entity.findLinkName(lbuild.block);
}
entity.links.remove(link);
//disable when unlinking
if(!link.active && lbuild.block.autoResetEnabled && lbuild.lastDisabler == entity){
if(lbuild.block.autoResetEnabled && lbuild.lastDisabler == entity){
lbuild.enabled = true;
}
}else{
@@ -148,12 +142,8 @@ public class LogicBlock extends Block{
stream.writeInt(bytes.length);
stream.write(bytes);
int actives = links.count(l -> l.active);
stream.writeInt(actives);
stream.writeInt(links.size);
for(LogicLink link : links){
if(!link.active) continue;
stream.writeUTF(link.name);
stream.writeShort(link.x);
stream.writeShort(link.y);
@@ -207,11 +197,7 @@ public class LogicBlock extends Block{
String name = stream.readUTF();
short x = stream.readShort(), y = stream.readShort();
Tmp.p2.set((int)(offset / (tilesize/2)), (int)(offset / (tilesize/2)));
transformer.get(Tmp.p1.set(x * 2, y * 2).sub(Tmp.p2));
Tmp.p1.add(Tmp.p2);
Tmp.p1.x /= 2;
Tmp.p1.y /= 2;
transformer.get(Tmp.p1.set(x, y));
links.add(new LogicLink(Tmp.p1.x, Tmp.p1.y, name, true));
}
@@ -224,7 +210,7 @@ public class LogicBlock extends Block{
}
public static class LogicLink{
public boolean active = true, valid;
public boolean valid;
public int x, y;
public String name;
public Building lastBuild;
@@ -237,13 +223,11 @@ public class LogicBlock extends Block{
}
public LogicLink copy(){
LogicLink out = new LogicLink(x, y, name, valid);
out.active = active;
return out;
return new LogicLink(x, y, name, valid);
}
}
public class LogicBuild extends Building implements Ranged{
public class LogicBuild extends Building implements Ranged, LReadable, LWritable{
/** logic "source code" as list of asm statements */
public String code = "";
public LExecutor executor = new LExecutor();
@@ -357,18 +341,19 @@ public class LogicBlock extends Block{
//store connections
for(LogicLink link : links){
if(link.active && (link.valid = validLink(world.build(link.x, link.y)))){
link.valid = validLink(world.build(link.x, link.y));
if(link.valid){
asm.putConst(link.name, world.build(link.x, link.y));
}
}
//store link objects
executor.links = new Building[links.count(l -> l.valid && l.active)];
executor.links = new Building[links.count(l -> l.valid)];
executor.linkIds.clear();
int index = 0;
for(LogicLink link : links){
if(link.active && link.valid){
if(link.valid){
Building build = world.build(link.x, link.y);
executor.links[index ++] = build;
if(build != null) executor.linkIds.add(build.id);
@@ -387,9 +372,7 @@ public class LogicBlock extends Block{
if(!var.constant){
LVar dest = asm.getVar(var.name);
if(dest != null && !dest.constant){
dest.isobj = var.isobj;
dest.objval = var.objval;
dest.numval = var.numval;
dest.set(var);
}
}
}
@@ -497,8 +480,6 @@ public class LogicBlock extends Block{
for(int i = 0; i < links.size; i++){
LogicLink l = links.get(i);
if(!l.active) continue;
var cur = world.build(l.x, l.y);
boolean valid = validLink(cur);
@@ -551,6 +532,38 @@ public class LogicBlock extends Block{
}
}
@Override
public boolean readable(LExecutor exec){
return exec.privileged || (this.team == exec.team && !this.block.privileged);
}
@Override
public void read(LVar position, LVar output){
if(position.isobj && position.objval instanceof String varName){
LVar ret = executor.optionalVar(varName);
if(ret == null){
output.setnum(Double.NaN);
return;
}
if(output.constant) return;
output.set(ret);
}
}
@Override
public boolean writable(LExecutor exec){
return exec.privileged || (this.team == exec.team && !this.block.privileged);
}
@Override
public void write(LVar position, LVar value){
if(position.isobj && position.objval instanceof String varName){
LVar at = executor.optionalVar(varName);
if(at == null || at.constant) return;
at.set(value);
}
}
@Override
public byte[] config(){
return compress(code, relativeConnections());
@@ -577,7 +590,7 @@ public class LogicBlock extends Block{
for(LogicLink l : links){
Building build = world.build(l.x, l.y);
if(l.active && validLink(build)){
if(validLink(build)){
Drawf.square(build.x, build.y, build.block.size * tilesize / 2f + 1f, Pal.place);
}
}
@@ -585,7 +598,7 @@ public class LogicBlock extends Block{
//draw top text on separate layer
for(LogicLink l : links){
Building build = world.build(l.x, l.y);
if(l.active && validLink(build)){
if(validLink(build)){
build.block.drawPlaceText(l.name, build.tileX(), build.tileY(), true);
}
}

View File

@@ -111,7 +111,7 @@ public class LogicDisplay extends Block{
public double sense(LAccess sensor){
return switch(sensor){
case displayWidth, displayHeight -> displaySize;
case bufferUsage -> commands.size;
case bufferSize -> commands.size;
case operations -> operations;
default -> super.sense(sensor);
};

View File

@@ -37,7 +37,7 @@ public class MemoryBlock extends Block{
return accessible();
}
public class MemoryBuild extends Building{
public class MemoryBuild extends Building implements LReadable, LWritable{
public double[] memory = new double[memoryCapacity];
//massive byte size means picking up causes sync issues
@@ -56,6 +56,29 @@ public class MemoryBlock extends Block{
return accessible();
}
public boolean readable(LExecutor exec){
return exec.privileged || (this.team == exec.team && !this.block.privileged);
}
@Override
public void read(LVar position, LVar output){
int address = position.numi();
//Return null when out of bounds. (instead of 0)
output.setnum(address < 0 || address >= memory.length ? Double.NaN : memory[address]);
}
@Override
public boolean writable(LExecutor exec){
return exec.privileged || (this.team == exec.team && !this.block.privileged);
}
@Override
public void write(LVar position, LVar value){
int address = position.numi();
if(address < 0 || address >= memory.length) return;
memory[address] = value.num();
}
@Override
public double sense(LAccess sensor){
return switch(sensor){

View File

@@ -14,6 +14,7 @@ import arc.util.io.*;
import arc.util.pooling.*;
import mindustry.core.*;
import mindustry.gen.*;
import mindustry.logic.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.world.*;
@@ -67,7 +68,7 @@ public class MessageBlock extends Block{
return accessible();
}
public class MessageBuild extends Building{
public class MessageBuild extends Building implements LReadable{
public StringBuilder message = new StringBuilder();
@Override
@@ -165,6 +166,25 @@ public class MessageBlock extends Block{
return !accessible() ? SystemCursor.arrow : super.getCursor();
}
@Override
public boolean readable(LExecutor exec){
return true;
}
@Override
public void read(LVar position, LVar output){
int address = position.numi();
output.setnum(address < 0 || address >= message.length() ? Double.NaN : message.charAt(address));
}
@Override
public double sense(LAccess sensor){
return switch(sensor){
case bufferSize -> message.length();
default -> super.sense(sensor);
};
}
@Override
public void damage(float damage){
if(privileged) return;

View File

@@ -228,7 +228,7 @@ public class PayloadLoader extends PayloadBlock{
return payload != null && (
exporting ||
(payload.block().hasLiquids && liquids.currentAmount() >= 0.1f && payload.build.liquids.currentAmount() >= payload.block().liquidCapacity - 0.001f) ||
(payload.block().hasItems && items.any() && payload.block().separateItemCapacity && content.items().contains(i -> payload.build.items.get(i) >= payload.block().itemCapacity)) ||
(payload.block().hasItems && items.any() && payload.block().separateItemCapacity && content.items().contains(i -> (payload.build.items.get(i) >= payload.block().itemCapacity) && items.has(i))) ||
(hasBattery() && payload.build.power.status >= 0.999999999f));
}

View File

@@ -246,7 +246,7 @@ public class Drill extends Block{
@Override
public boolean shouldConsume(){
return items.total() < itemCapacity && enabled;
return items.total() < itemCapacity && enabled && dominantItem != null;
}
@Override

View File

@@ -33,7 +33,7 @@ public class Incinerator extends Block{
@Override
public BlockStatus status(){
return heat > 0.5f ? BlockStatus.active : BlockStatus.noInput;
return !enabled ? BlockStatus.logicDisable : heat > 0.5f ? BlockStatus.active : BlockStatus.noInput;
}
@Override

View File

@@ -38,7 +38,7 @@ public class ItemIncinerator extends Block{
@Override
public BlockStatus status(){
return efficiency > 0 ? BlockStatus.active : BlockStatus.noInput;
return !enabled ? BlockStatus.logicDisable : efficiency > 0 ? BlockStatus.active : BlockStatus.noInput;
}
@Override

View File

@@ -220,6 +220,14 @@ public class UnitFactory extends UnitBlock{
}
}
@Override
public void drawSelect(){
super.drawSelect();
if(plans.size > 1 && currentPlan != -1 && currentPlan < plans.size){
drawItemSelection(plans.get(currentPlan).unit);
}
}
@Override
public Vec2 getCommandPosition(){
return commandPos;