Compare commits

...
Author SHA1 Message Date
Github Actions b70ea8afd6 Automatic class mapping update 2026-06-17 17:09:10 +00:00
Anuken fe6e3b03ba it works but it's bad 2026-06-17 13:06:59 -04:00
Anuken 0849ed8cfc progress 2026-06-17 13:01:14 -04:00
Anuken 715c099dcc progress 2026-06-17 12:48:04 -04:00
Anuken f9da9a3fe1 progress 2026-06-17 12:47:21 -04:00
Anuken d4449289a8 it actually works 2026-06-17 12:36:28 -04:00
Anuken adaf3112ed progress 2026-06-17 12:18:58 -04:00
Anuken 6524fc07a7 Merge branch 'master' of https://github.com/Anuken/Mindustry into liquid-thread-test 2026-06-17 12:02:25 -04:00
Anuken 5c224a49b6 progress 2026-06-17 11:45:17 -04:00
18 changed files with 276 additions and 74 deletions
+3
View File
@@ -8,6 +8,7 @@ import mindustry.gen.*;
import mindustry.maps.*;
import mindustry.mod.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import static mindustry.Vars.*;
@@ -43,6 +44,8 @@ public class GameState{
public Attributes envAttrs = new Attributes();
/** Team data. Gets reset every new game. */
public Teams teams = new Teams();
/** Runs liquids in a new thread. */
public LiquidManager liquids = new LiquidManager();
/** Handles JSON edits of game content. */
public DataManager data = new DataManager();
/** Number of enemies in the game; only used clientside in servers. */
+3
View File
@@ -302,6 +302,7 @@ public class Logic implements ApplicationListener{
Events.fire(new ResetEvent());
world.tiles = new Tiles(0, 0);
state.liquids.stop();
state.data.unload();
State prev = state.getState();
//recreate gamestate - sets state to menu
@@ -559,6 +560,8 @@ public class Logic implements ApplicationListener{
runWave();
}
state.liquids.checkUpdate();
//apply weather attributes
state.envAttrs.clear();
state.envAttrs.add(state.rules.attributes);
@@ -126,6 +126,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
if(shouldAdd){
add();
}
if(this instanceof LiquidUpdater lu) state.liquids.add(lu);
checkAllowUpdate();
created();
@@ -884,12 +885,19 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
if(liquids.get(liquid) <= 0.0001f) return;
if(!net.client() && state.isCampaign() && team == state.rules.defaultTeam) liquid.unlock();
//TODO add this back
//if(!net.client() && state.isCampaign() && team == state.rules.defaultTeam) liquid.unlock();
for(int i = 0; i < proximity.size; i++){
incrementDump(proximity.size);
int psize = proximity.size;
Building[] prox = proximity.items;
Building other = proximity.get((i + dump) % proximity.size);
if(psize == 0) return;
for(int i = 0; i < psize; i++){
incrementDump(psize);
Building other = prox[((i + dump) % psize)];
if(other == null) continue;
if(outputDir != -1 && (outputDir + rotation) % 4 != relativeTo(other)) continue;
other = other.getLiquidDestination(self(), liquid);
@@ -916,22 +924,23 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
}
public float moveLiquidForward(boolean leaks, Liquid liquid){
Tile next = tile.nearby(rotation);
public float moveLiquidForward(@Nullable Building next, boolean leaks, Liquid liquid, float delta){
if(next != null){
return moveLiquid(next, liquid, delta);
}else if(leaks){
Tile nextTile = tile.nearby(rotation);
if(next == null) return 0;
if(next.build != null){
return moveLiquid(next.build, liquid);
}else if(leaks && !next.block().solid && !next.block().hasLiquids){
float leakAmount = liquids.get(liquid) / 1.5f;
Puddles.deposit(next, tile, liquid, leakAmount, true, true);
liquids.remove(liquid, leakAmount);
if(!nextTile.block().solid && !nextTile.block().hasLiquids){
float leakAmount = liquids.get(liquid) / 1.5f;
liquids.remove(liquid, leakAmount);
//TODO: laggy
Core.app.post(() -> Puddles.deposit(nextTile, tile, liquid, leakAmount, true, true));
}
}
return 0;
}
public float moveLiquid(Building next, Liquid liquid){
public float moveLiquid(Building next, Liquid liquid, float delta){
if(next == null) return 0;
next = next.getLiquidDestination(self(), liquid);
@@ -956,15 +965,20 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
if(other.blockReactive && liquid.blockReactive){
//TODO liquid reaction handler for extensibility
if((other.flammability > 0.3f && liquid.temperature > 0.7f) || (liquid.flammability > 0.3f && other.temperature > 0.7f)){
damageContinuous(1);
next.damageContinuous(1);
if(Mathf.chanceDelta(0.1)){
Fx.fire.at(fx, fy);
}
var fnext = next;
Core.app.post(() -> {
damageContinuous(1);
fnext.damageContinuous(1);
if(Mathf.chanceDelta(0.1)){
Fx.fire.at(fx, fy);
}
});
}else if((liquid.temperature > 0.7f && other.temperature < 0.55f) || (other.temperature > 0.7f && liquid.temperature < 0.55f)){
liquids.remove(liquid, Math.min(liquids.get(liquid), 0.7f * Time.delta));
liquids.remove(liquid, Math.min(liquids.get(liquid), 0.7f * delta));
if(Mathf.chanceDelta(0.2f)){
Fx.steam.at(fx, fy);
Core.app.post(() -> {
Fx.steam.at(fx, fy);
});
}
}
}
@@ -2012,7 +2026,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
@Replace
@Override
public boolean isValid(){
return tile.build == self() && !dead();
return tile.build == self() && !dead;
}
@MethodPriority(100)
+1
View File
@@ -150,6 +150,7 @@ public class ClassMap{
classes.put("ExplosionShield", mindustry.world.blocks.ExplosionShield.class);
classes.put("ItemSelection", mindustry.world.blocks.ItemSelection.class);
classes.put("LaunchAnimator", mindustry.world.blocks.LaunchAnimator.class);
classes.put("LiquidUpdater", mindustry.world.blocks.LiquidUpdater.class);
classes.put("RotBlock", mindustry.world.blocks.RotBlock.class);
classes.put("TileBitmask", mindustry.world.blocks.TileBitmask.class);
classes.put("UnitTetherBlock", mindustry.world.blocks.UnitTetherBlock.class);
@@ -0,0 +1,95 @@
package mindustry.world;
import arc.struct.*;
import arc.util.*;
import mindustry.world.blocks.*;
import java.util.concurrent.*;
//TODO: consumers read AND write from the liquids array... on the main thread. as do various other blocks. see remove() usage
//TODO: payload loaders/unloaders don't use this system
public class LiquidManager{
private static final int targetFps = 60;
private static final long targetNanos = 1000 * Time.nanosPerMilli / targetFps;
private UpdaterThread thread;
private int curTick;
public void checkUpdate(){
//ensures volatile read
if(thread != null){
curTick = thread.tick;
}
}
public void add(LiquidUpdater building){
if(!building.shouldLiquidUpdate()) return;
if(thread == null){
thread = new UpdaterThread();
thread.start();
}
thread.addQueue.add(building);
}
public void stop(){
if(thread != null){
thread.running = false;
thread.interrupt();
thread = null;
}
}
static class UpdaterThread extends Thread{
Seq<LiquidUpdater> updaters = new Seq<>(false, 20, LiquidUpdater.class);
ConcurrentLinkedQueue<LiquidUpdater> addQueue = new ConcurrentLinkedQueue<>();
volatile boolean running = true;
volatile int tick;
long lastNanos = Time.nanos();
@Override
public void run(){
try{
while(running){
long nanos = Time.nanos();
long deltaNanos = Time.timeSinceNanos(lastNanos);
float deltaTicks = (float)deltaNanos / Time.nanosPerMilli / 1000f * 60f;
lastNanos = nanos;
LiquidUpdater toAdd;
while((toAdd = addQueue.poll()) != null){
updaters.add(toAdd);
}
LiquidUpdater[] items = updaters.items;
int size = updaters.size;
for(int i = 0; i < size; i++){
var item = items[i];
if(!item.isValid()){
//note: this is an un-ordered seq, so this is just a O(1) swap
updaters.remove(i);
i --;
size --;
}else{
item.updateLiquids(deltaTicks);
}
}
//update 'global tick' as a volatile memory barrier
tick ++;
long elapsed = Time.timeSinceNanos(nanos);
if(elapsed < targetNanos){
long remaining = targetNanos - elapsed;
Thread.sleep(remaining / Time.nanosPerMilli, (int)(remaining % Time.nanosPerMilli));
}
}
}catch(InterruptedException e){
//stop the thread
}
}
}
}
@@ -0,0 +1,13 @@
package mindustry.world.blocks;
import mindustry.game.*;
import mindustry.gen.*;
public interface LiquidUpdater extends Healthc{
default boolean shouldLiquidUpdate(){
return true;
}
void updateLiquids(float delta);
Team team();
}
@@ -6,6 +6,7 @@ import mindustry.annotations.Annotations.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.liquid.*;
import mindustry.world.meta.*;
@@ -33,7 +34,8 @@ public class DirectionLiquidBridge extends DirectionBridge{
return new TextureRegion[]{bottomRegion, region, dirRegion};
}
public class DuctBridgeBuild extends DirectionBridgeBuild{
public class DuctBridgeBuild extends DirectionBridgeBuild implements LiquidUpdater{
Building next;
@Override
public void drawCached(){
@@ -56,20 +58,20 @@ public class DirectionLiquidBridge extends DirectionBridge{
}
}
@Override
public void onProximityUpdate(){
super.onProximityUpdate();
next = front();
}
@Override
public void updateTile(){
var link = lastLink = findLink();
if(link != null){
moveLiquid(link, liquids.current());
link.occupied[rotation % 4] = this;
}
if(link == null){
if(liquids.currentAmount() > 0.0001f && timer(timerFlow, 1)){
moveLiquidForward(false, liquids.current());
}
}
for(int i = 0; i < 4; i++){
if(occupied[i] == null || occupied[i].rotation != i || !occupied[i].isValid() || occupied[i].lastLink != this){
occupied[i] = null;
@@ -77,11 +79,22 @@ public class DirectionLiquidBridge extends DirectionBridge{
}
}
@Override
public void updateLiquids(float delta){
var link = lastLink;
if(link != null){
moveLiquid(link, liquids.current(), delta);
}else{
if(liquids.currentAmount() > 0.0001f && timer(timerFlow, 1)){
moveLiquidForward(next, false, liquids.current(), Time.delta);
}
}
}
@Override
public boolean acceptLiquid(Building source, Liquid liquid){
var link = findLink();
//only accept if there's an output point, or it comes from a link
if(link == null && !(source instanceof DirectionBridgeBuild b && b.findLink() == this)) return false;
if(lastLink == null && !(source instanceof DirectionBridgeBuild b && b.findLink() == this)) return false;
int rel = this.relativeToEdge(source.tile);
@@ -201,6 +201,7 @@ public class ItemBridge extends Block{
public float time = -8f, timeSpeed;
public boolean wasMoved, moved, hadValidLink;
public float transportCounter;
public Building lastValidLink;
@Override
public void pickedUp(){
@@ -333,9 +334,11 @@ public class ItemBridge extends Block{
hadValidLink = linkValid(tile, other);
if(!hadValidLink){
lastValidLink = null;
doDump();
warmup = 0f;
}else{
lastValidLink = other.build;
var inc = ((ItemBridgeBuild)other.build).incoming;
int pos = tile.pos();
if(!inc.contains(pos)){
@@ -55,6 +55,9 @@ public class Conduit extends LiquidBlock implements Autotiler{
priority = TargetPriority.transport;
drawCached = true;
buildingCacheLayer = BuildingCacheLayer.under;
update = false;
destructible = true;
}
@Override
@@ -153,10 +156,11 @@ public class Conduit extends LiquidBlock implements Autotiler{
return new TextureRegion[]{Core.atlas.find("conduit-bottom"), topRegions[0]};
}
public class ConduitBuild extends LiquidBuild implements ChainedBuilding{
public class ConduitBuild extends LiquidBuild implements ChainedBuilding, LiquidUpdater{
public float smoothLiquid;
public int blendbits, xscl = 1, yscl = 1, blending;
public boolean capped, backCapped = false;
Building next;
@Override
public void drawCached(){
@@ -236,24 +240,21 @@ public class Conduit extends LiquidBlock implements Autotiler{
Building next = front(), prev = back();
capped = next == null || next.team != team || !next.block.hasLiquids;
backCapped = blendbits == 0 && (prev == null || prev.team != team || !prev.block.hasLiquids);
this.next = next;
}
@Override
public boolean acceptLiquid(Building source, Liquid liquid){
noSleep();
return (liquids.current() == liquid || liquids.currentAmount() < 0.2f)
&& (tile == null || source == this || (source.relativeTo(tile.x, tile.y) + 2) % 4 != rotation);
}
@Override
public void updateTile(){
smoothLiquid = Mathf.lerpDelta(smoothLiquid, liquids.currentAmount() / liquidCapacity, 0.05f);
public void updateLiquids(float delta){
smoothLiquid = Mathf.lerp(smoothLiquid, liquids.currentAmount() / liquidCapacity, 0.05f * delta);
if(liquids.currentAmount() > 0.0001f && timer(timerFlow, 1)){
moveLiquidForward(leaks, liquids.current());
noSleep();
}else{
sleep();
moveLiquidForward(next, leaks, liquids.current(), delta);
}
}
@@ -1,6 +1,8 @@
package mindustry.world.blocks.liquid;
import arc.util.*;
import mindustry.gen.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.distribution.*;
import mindustry.world.meta.*;
@@ -16,18 +18,27 @@ public class LiquidBridge extends ItemBridge{
envEnabled = Env.any;
}
public class LiquidBridgeBuild extends ItemBridgeBuild{
public class LiquidBridgeBuild extends ItemBridgeBuild implements LiquidUpdater{
@Override
public void updateTransport(Building other){
if(warmup >= 0.25f){
moved |= moveLiquid(other, liquids.current()) > 0.05f;
}
}
@Override
public void doDump(){
dumpLiquid(liquids.current(), 1f);
}
@Override
public void updateLiquids(float delta){
var link = lastValidLink;
if(link == null){
dumpLiquid(liquids.current(), 1f);
}else if(warmup >= 0.25f){
moved |= moveLiquid(link, liquids.current(), Time.delta) > 0.05f;
}
}
}
}
@@ -3,6 +3,7 @@ package mindustry.world.blocks.liquid;
import arc.graphics.g2d.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.blocks.*;
public class LiquidRouter extends LiquidBlock{
public float liquidPadding = 0f;
@@ -13,6 +14,9 @@ public class LiquidRouter extends LiquidBlock{
noUpdateDisabled = true;
canOverdrive = false;
floating = true;
update = false;
destructible = true;
}
@Override
@@ -20,9 +24,10 @@ public class LiquidRouter extends LiquidBlock{
return new TextureRegion[]{bottomRegion, region};
}
public class LiquidRouterBuild extends LiquidBuild{
public class LiquidRouterBuild extends LiquidBuild implements LiquidUpdater{
@Override
public void updateTile(){
public void updateLiquids(float delta){
dumpLiquid(liquids.current());
}
@@ -10,6 +10,7 @@ import mindustry.entities.*;
import mindustry.game.EventType.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.world.blocks.*;
import mindustry.world.consumers.*;
import mindustry.world.meta.*;
@@ -93,7 +94,7 @@ public class ConsumeGenerator extends PowerGenerator{
}
}
public class ConsumeGeneratorBuild extends GeneratorBuild{
public class ConsumeGeneratorBuild extends GeneratorBuild implements LiquidUpdater{
public float warmup, totalTime, efficiencyMultiplier = 1f, itemDurationMultiplier = 1;
@Override
@@ -133,19 +134,29 @@ public class ConsumeGenerator extends PowerGenerator{
generateTime = 1f;
}
//generation time always goes down, but only at the end so consumeTriggerValid doesn't assume fake items
generateTime -= delta() / (itemDuration * itemDurationMultiplier);
}
@Override
public boolean shouldLiquidUpdate(){
return outputLiquid != null;
}
@Override
public void updateLiquids(float delta){
if(outputLiquid != null){
float added = Math.min(productionEfficiency * delta() * outputLiquid.amount, liquidCapacity - liquids.get(outputLiquid.liquid));
float added = Math.min(productionEfficiency * delta * timeScale * outputLiquid.amount, liquidCapacity - liquids.get(outputLiquid.liquid));
liquids.add(outputLiquid.liquid, added);
dumpLiquid(outputLiquid.liquid);
if(explodeOnFull && liquids.get(outputLiquid.liquid) >= liquidCapacity - 0.01f){
kill();
Events.fire(new GeneratorPressureExplodeEvent(this));
Core.app.post(() -> {
kill();
Events.fire(new GeneratorPressureExplodeEvent(this));
});
}
}
//generation time always goes down, but only at the end so consumeTriggerValid doesn't assume fake items
generateTime -= delta() / (itemDuration * itemDurationMultiplier);
}
@Override
@@ -10,6 +10,7 @@ import mindustry.game.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.meta.*;
public class ThermalGenerator extends PowerGenerator{
@@ -71,7 +72,7 @@ public class ThermalGenerator extends PowerGenerator{
return tile.getLinkedTilesAs(this, tempTiles).sumf(other -> other.floor().attributes.get(attribute)) > minEfficiency;
}
public class ThermalGeneratorBuild extends GeneratorBuild{
public class ThermalGeneratorBuild extends GeneratorBuild implements LiquidUpdater{
public float sum;
@Override
@@ -81,9 +82,17 @@ public class ThermalGenerator extends PowerGenerator{
if(productionEfficiency > 0.1f && Mathf.chanceDelta(effectChance)){
generateEffect.at(x + Mathf.range(3f), y + Mathf.range(3f));
}
}
@Override
public boolean shouldLiquidUpdate(){
return outputLiquid != null;
}
@Override
public void updateLiquids(float delta){
if(outputLiquid != null){
float added = Math.min(productionEfficiency * delta() * outputLiquid.amount, liquidCapacity - liquids.get(outputLiquid.liquid));
float added = Math.min(productionEfficiency * delta * timeScale * outputLiquid.amount, liquidCapacity - liquids.get(outputLiquid.liquid));
liquids.add(outputLiquid.liquid, added);
dumpLiquid(outputLiquid.liquid);
}
@@ -39,8 +39,12 @@ public class Fracker extends SolidPump{
}else{
warmup = Mathf.lerpDelta(warmup, 0f, 0.02f);
lastPump = 0f;
dumpLiquid(result);
}
}
@Override
public void updateLiquids(float delta){
dumpLiquid(result);
}
}
}
@@ -14,6 +14,7 @@ import mindustry.logic.*;
import mindustry.mod.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.liquid.Conduit.*;
import mindustry.world.draw.*;
import mindustry.world.meta.*;
@@ -183,7 +184,7 @@ public class GenericCrafter extends Block{
}
}
public class GenericCrafterBuild extends Building{
public class GenericCrafterBuild extends Building implements LiquidUpdater{
public float progress;
public float totalProgress;
public float warmup;
@@ -321,7 +322,10 @@ public class GenericCrafter extends Block{
dump(output.item);
}
}
}
@Override
public void updateLiquids(float delta){
if(outputLiquids != null){
for(int i = 0; i < outputLiquids.length; i++){
int dir = liquidOutputDirections.length > i ? liquidOutputDirections[i] : -1;
@@ -9,6 +9,7 @@ import mindustry.game.*;
import mindustry.logic.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.liquid.*;
import mindustry.world.draw.*;
import mindustry.world.meta.*;
@@ -107,12 +108,14 @@ public class Pump extends LiquidBlock{
return tile != null && tile.floor().liquidDrop != null;
}
public class PumpBuild extends LiquidBuild{
public class PumpBuild extends LiquidBuild implements LiquidUpdater{
public float warmup, totalProgress;
public float consTimer;
public float amount = 0f;
public @Nullable Liquid liquidDrop = null;
float lastPumped;
@Override
public void draw(){
drawer.draw(this);
@@ -159,24 +162,29 @@ public class Pump extends LiquidBlock{
@Override
public void updateTile(){
if(efficiency > 0 && liquidDrop != null){
float maxPump = Math.min(liquidCapacity - liquids.get(liquidDrop), amount * pumpAmount * edelta());
liquids.add(liquidDrop, maxPump);
//does nothing for most pumps, as those do not require items.
if((consTimer += delta()) >= consumeTime){
consume();
consTimer %= 1f;
}
warmup = Mathf.approachDelta(warmup, maxPump > 0.001f ? 1f : 0f, warmupSpeed);
warmup = Mathf.approachDelta(warmup, lastPumped > 0.001f ? 1f : 0f, warmupSpeed);
}else{
warmup = Mathf.approachDelta(warmup, 0f, warmupSpeed);
}
totalProgress += warmup * Time.delta;
}
if(liquidDrop != null){
dumpLiquid(liquidDrop);
@Override
public void updateLiquids(float delta){
var ld = liquidDrop;
if(ld != null){
float maxPump = Math.min(liquidCapacity - liquids.get(liquidDrop), amount * pumpAmount * efficiency * timeScale * delta);
liquids.add(liquidDrop, maxPump);
lastPumped = maxPump;
dumpLiquid(ld);
}
}
@@ -121,12 +121,7 @@ public class SolidPump extends Pump{
@Override
public void updateTile(){
liquidDrop = result;
float fraction = Math.max(validTiles + boost + (attribute == null ? 0 : attribute.env()), 0);
if(efficiency > 0 && typeLiquid() < liquidCapacity - 0.001f){
float maxPump = Math.min(liquidCapacity - typeLiquid(), pumpAmount * delta() * fraction * efficiency);
liquids.add(result, maxPump);
lastPump = maxPump / Time.delta;
warmup = Mathf.lerpDelta(warmup, 1f, 0.02f);
if(Mathf.chance(delta() * updateEffectChance))
updateEffect.at(x + Mathf.range(size * 2f), y + Mathf.range(size * 2f));
@@ -136,6 +131,14 @@ public class SolidPump extends Pump{
}
pumpTime += warmup * edelta();
}
@Override
public void updateLiquids(float delta){
float fraction = Math.max(validTiles + boost + (attribute == null ? 0 : attribute.env()), 0);
float maxPump = Math.min(liquidCapacity - typeLiquid(), pumpAmount * delta * timeScale * fraction * efficiency);
liquids.add(result, maxPump);
lastPump = maxPump / Time.delta;
dumpLiquid(result);
}
@@ -21,7 +21,8 @@ public class LiquidSource extends Block{
public LiquidSource(String name){
super(name);
update = true;
update = false;
destructible = true;
solid = true;
hasLiquids = true;
liquidCapacity = 10000f;
@@ -55,11 +56,11 @@ public class LiquidSource extends Block{
return new TextureRegion[]{bottomRegion, region};
}
public class LiquidSourceBuild extends Building{
public class LiquidSourceBuild extends Building implements LiquidUpdater{
public @Nullable Liquid source = null;
@Override
public void updateTile(){
public void updateLiquids(float delta){
if(source == null){
liquids.clear();
}else{