Merge branch 'master' of https://github.com/Anuken/Mindustry into 7.0-features

 Conflicts:
	core/src/mindustry/content/Blocks.java
This commit is contained in:
Anuken
2021-06-09 17:10:49 -04:00
93 changed files with 1617 additions and 574 deletions

View File

@@ -27,8 +27,6 @@ public class BlockIndexer{
private static final Rect rect = new Rect();
private static boolean returnBool = false;
private final IntSet intSet = new IntSet();
private int quadWidth, quadHeight;
/** Stores all ore quadrants on the map. Maps ID to qX to qY to a list of tiles with that ore. */
@@ -41,9 +39,9 @@ public class BlockIndexer{
private Seq<Team> activeTeams = new Seq<>(Team.class);
/** Maps teams to a map of flagged tiles by flag. */
private TileArray[][] flagMap = new TileArray[Team.all.length][BlockFlag.all.length];
/** Counts whether a certain floor is present in the world upon load. */
private boolean[] blocksPresent;
/** Empty set used for returning. */
private TileArray emptySet = new TileArray();
/** Array used for returning and reusing. */
private Seq<Tile> returnArray = new Seq<>();
/** Array used for returning and reusing. */
@@ -74,6 +72,7 @@ public class BlockIndexer{
ores = new IntSeq[content.items().size][][];
quadWidth = Mathf.ceil(world.width() / (float)quadrantSize);
quadHeight = Mathf.ceil(world.height() / (float)quadrantSize);
blocksPresent = new boolean[content.blocks().size];
for(Tile tile : world.tiles){
process(tile);
@@ -153,6 +152,12 @@ public class BlockIndexer{
seq.removeValue(pos);
}
}
}
/** @return whether a certain block is anywhere on this map. */
public boolean isBlockPresent(Block block){
return blocksPresent != null && blocksPresent[block.id];
}
private TileArray[] getFlagged(Team team){
@@ -383,6 +388,13 @@ public class BlockIndexer{
}
data.buildings.insert(tile.build);
}
if(!tile.block().isStatic()){
blocksPresent[tile.floorID()] = true;
blocksPresent[tile.overlayID()] = true;
}
//bounds checks only needed in very specific scenarios
if(tile.blockID() < blocksPresent.length) blocksPresent[tile.blockID()] = true;
}
public static class TileArray implements Iterable<Tile>{

View File

@@ -156,7 +156,7 @@ public class WaveSpawner{
}
if(state.rules.attackMode && state.teams.isActive(state.rules.waveTeam)){
for(Building core : state.teams.get(state.rules.waveTeam).cores){
for(Building core : state.rules.waveTeam.data().cores){
cons.get(core.x, core.y);
}
}

View File

@@ -75,8 +75,4 @@ public class MinerAI extends AIController{
circle(core, unit.type.range / 1.8f);
}
}
@Override
protected void updateTargeting(){
}
}

View File

@@ -209,7 +209,7 @@ public class SoundControl{
/** Whether to play dark music.*/
protected boolean isDark(){
if(state.teams.get(player.team()).hasCore() && state.teams.get(player.team()).core().healthf() < 0.85f){
if(player.team().data().hasCore() && player.team().data().core().healthf() < 0.85f){
//core damaged -> dark
return true;
}

View File

@@ -43,7 +43,7 @@ public class Blocks implements ContentList{
regolithWall, yellowStoneWall, rhyoliteWall, carbonWall,
graphiticStone,
iceSnow, sandWater, darksandWater, duneWall, sandWall, moss, sporeMoss, shale, shaleWall, shaleBoulder, sandBoulder, daciteBoulder, boulder, snowBoulder, basaltBoulder, grass, salt,
metalFloor, metalFloorDamaged, metalFloor2, metalFloor3, metalFloor5, basalt, magmarock, hotrock, snowWall, saltWall,
metalFloor, metalFloorDamaged, metalFloor2, metalFloor3, metalFloor4, metalFloor5, basalt, magmarock, hotrock, snowWall, saltWall,
darkPanel1, darkPanel2, darkPanel3, darkPanel4, darkPanel5, darkPanel6, darkMetal,
pebbles, tendrils,
@@ -493,32 +493,24 @@ public class Blocks implements ContentList{
wall = sporeWall;
}};
metalFloor = new Floor("metal-floor"){{
metalFloor = new MetalFloor("metal-floor"){{
variants = 0;
attributes.set(Attribute.water, -1f);
}};
metalFloorDamaged = new Floor("metal-floor-damaged"){{
variants = 3;
}};
metalFloorDamaged = new MetalFloor("metal-floor-damaged", 3);
metalFloor2 = new Floor("metal-floor-2"){{
variants = 0;
}};
metalFloor2 = new MetalFloor("metal-floor-2");
metalFloor3 = new MetalFloor("metal-floor-3");
metalFloor4 = new MetalFloor("metal-floor-4");
metalFloor5 = new MetalFloor("metal-floor-5");
metalFloor3 = new Floor("metal-floor-3"){{
variants = 0;
}};
metalFloor5 = new Floor("metal-floor-5"){{
variants = 0;
}};
darkPanel1 = new Floor("dark-panel-1"){{ variants = 0; }};
darkPanel2 = new Floor("dark-panel-2"){{ variants = 0; }};
darkPanel3 = new Floor("dark-panel-3"){{ variants = 0; }};
darkPanel4 = new Floor("dark-panel-4"){{ variants = 0; }};
darkPanel5 = new Floor("dark-panel-5"){{ variants = 0; }};
darkPanel6 = new Floor("dark-panel-6"){{ variants = 0; }};
darkPanel1 = new MetalFloor("dark-panel-1");
darkPanel2 = new MetalFloor("dark-panel-2");
darkPanel3 = new MetalFloor("dark-panel-3");
darkPanel4 = new MetalFloor("dark-panel-4");
darkPanel5 = new MetalFloor("dark-panel-5");
darkPanel6 = new MetalFloor("dark-panel-6");
darkMetal = new StaticWall("dark-metal");
@@ -1773,8 +1765,8 @@ public class Blocks implements ContentList{
);
size = 2;
range = 180f;
reloadTime = 38f;
range = 190f;
reloadTime = 34f;
restitution = 0.03f;
ammoEjectBack = 3f;
cooldown = 0.03f;
@@ -2013,7 +2005,7 @@ public class Blocks implements ContentList{
}};
health = 200 * size * size;
consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, 0.5f)).update(false);
consumes.add(new ConsumeCoolant(0.5f)).update(false);
}};
//endregion
@@ -2191,10 +2183,11 @@ public class Blocks implements ContentList{
payloadPropulsionTower = new PayloadMassDriver("payload-propulsion-tower"){{
requirements(Category.units, with(Items.thorium, 300, Items.silicon, 200, Items.plastanium, 200, Items.phaseFabric, 50));
size = 5;
reloadTime = 150f;
reloadTime = 140f;
chargeTime = 100f;
range = 300f;
consumes.power(10f);
range = 500f;
maxPayloadSize = 3.5f;
consumes.power(6f);
}};
//endregion

View File

@@ -1231,10 +1231,10 @@ public class Fx{
});
}),
shootLiquid = new Effect(40f, 80f, e -> {
color(e.color, Color.white, e.fout() / 6f + Mathf.randomSeedRange(e.id, 0.1f));
shootLiquid = new Effect(15f, 80f, e -> {
color(e.color);
randLenVectors(e.id, 6, e.finpow() * 60f, e.rotation, 11f, (x, y) -> {
randLenVectors(e.id, 2, e.finpow() * 15f, e.rotation, 11f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.5f + e.fout() * 2.5f);
});
}),

View File

@@ -709,7 +709,7 @@ public class TechTree implements ContentList{
/** Item requirements for this content. */
public ItemStack[] requirements;
/** Requirements that have been fulfilled. Always the same length as the requirement array. */
public final ItemStack[] finishedRequirements;
public ItemStack[] finishedRequirements;
/** Extra objectives needed to research this. */
public Seq<Objective> objectives = new Seq<>();
/** Nodes that depend on this node. */
@@ -720,14 +720,8 @@ public class TechTree implements ContentList{
this.parent = parent;
this.content = content;
this.requirements = requirements;
this.depth = parent == null ? 0 : parent.depth + 1;
this.finishedRequirements = new ItemStack[requirements.length];
//load up the requirements that have been finished if settings are available
for(int i = 0; i < requirements.length; i++){
finishedRequirements[i] = new ItemStack(requirements[i].item, Core.settings == null ? 0 : Core.settings.getInt("req-" + content.name + "-" + requirements[i].item.name));
}
setupRequirements(requirements);
var used = new ObjectSet<Content>();
@@ -742,6 +736,16 @@ public class TechTree implements ContentList{
all.add(this);
}
public void setupRequirements(ItemStack[] requirements){
this.requirements = requirements;
this.finishedRequirements = new ItemStack[requirements.length];
//load up the requirements that have been finished if settings are available
for(int i = 0; i < requirements.length; i++){
finishedRequirements[i] = new ItemStack(requirements[i].item, Core.settings == null ? 0 : Core.settings.getInt("req-" + content.name + "-" + requirements[i].item.name));
}
}
/** Resets finished requirements and saves. */
public void reset(){
for(ItemStack stack : finishedRequirements){

View File

@@ -611,7 +611,7 @@ public class UnitTypes implements ContentList{
bullet = new LiquidBulletType(Liquids.slag){{
damage = 11;
speed = 2.3f;
speed = 2.4f;
drag = 0.01f;
shootEffect = Fx.shootSmall;
lifetime = 56f;

View File

@@ -117,5 +117,4 @@ public class Weathers implements ContentList{
baseSpeed = 0.03f;
}};
}
}

View File

@@ -247,12 +247,12 @@ public class Control implements ApplicationListener, Loadable{
saves.load();
}
/** Automatically unlocks things with no requirements. */
/** Automatically unlocks things with no requirements and no locked parents. */
void checkAutoUnlocks(){
if(net.client()) return;
for(TechNode node : TechTree.all){
if(!node.content.unlocked() && node.requirements.length == 0 && !node.objectives.contains(o -> !o.complete())){
if(!node.content.unlocked() && (node.parent == null || node.parent.content.unlocked()) && node.requirements.length == 0 && !node.objectives.contains(o -> !o.complete())){
node.content.unlock();
}
}

View File

@@ -43,7 +43,7 @@ public class Logic implements ApplicationListener{
Events.on(BlockBuildEndEvent.class, event -> {
if(!event.breaking){
TeamData data = state.teams.get(event.team);
TeamData data = event.team.data();
Iterator<BlockPlan> it = data.blocks.iterator();
while(it.hasNext()){
BlockPlan b = it.next();

View File

@@ -21,7 +21,6 @@ import mindustry.gen.*;
import mindustry.net.Administration.*;
import mindustry.net.*;
import mindustry.net.Packets.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.modules.*;
@@ -160,6 +159,18 @@ public class NetClient implements ApplicationListener{
clientPacketReliable(type, contents);
}
@Remote(variants = Variant.both, unreliable = true)
public static void effect(Effect effect, float x, float y, float rotation, Color color){
if(effect == null) return;
effect.at(x, y, rotation, color);
}
@Remote(variants = Variant.both)
public static void effectReliable(Effect effect, float x, float y, float rotation, Color color){
effect(effect, x, y, rotation, color);
}
//called on all clients
@Remote(targets = Loc.server, variants = Variant.both)
public static void sendMessage(String message, String sender, Player playersender){
@@ -314,78 +325,6 @@ public class NetClient implements ApplicationListener{
ui.loadfrag.hide();
}
@Remote(variants = Variant.both, unreliable = true)
public static void setHudText(String message){
if(message == null) return;
ui.hudfrag.setHudText(message);
}
@Remote(variants = Variant.both)
public static void hideHudText(){
ui.hudfrag.toggleHudText(false);
}
/** TCP version */
@Remote(variants = Variant.both)
public static void setHudTextReliable(String message){
setHudText(message);
}
@Remote(variants = Variant.both)
public static void announce(String message){
if(message == null) return;
ui.announce(message);
}
@Remote(variants = Variant.both)
public static void infoMessage(String message){
if(message == null) return;
ui.showText("", message);
}
@Remote(variants = Variant.both)
public static void infoPopup(String message, float duration, int align, int top, int left, int bottom, int right){
if(message == null) return;
ui.showInfoPopup(message, duration, align, top, left, bottom, right);
}
@Remote(variants = Variant.both)
public static void label(String message, float duration, float worldx, float worldy){
if(message == null) return;
ui.showLabel(message, duration, worldx, worldy);
}
@Remote(variants = Variant.both, unreliable = true)
public static void effect(Effect effect, float x, float y, float rotation, Color color){
if(effect == null) return;
effect.at(x, y, rotation, color);
}
@Remote(variants = Variant.both)
public static void effectReliable(Effect effect, float x, float y, float rotation, Color color){
effect(effect, x, y, rotation, color);
}
@Remote(variants = Variant.both)
public static void infoToast(String message, float duration){
if(message == null) return;
ui.showInfoToast(message, duration);
}
@Remote(variants = Variant.both)
public static void warningToast(int unicode, String text){
if(text == null || Fonts.icon.getData().getGlyph((char)unicode) == null) return;
ui.hudfrag.showToast(Fonts.getGlyph(Fonts.icon, (char)unicode), text);
}
@Remote(variants = Variant.both)
public static void setRules(Rules rules){
state.rules = rules;
@@ -518,7 +457,7 @@ public class NetClient implements ApplicationListener{
int teams = input.readUnsignedByte();
for(int i = 0; i < teams; i++){
int team = input.readUnsignedByte();
TeamData data = state.teams.get(Team.all[team]);
TeamData data = Team.all[team].data();
if(data.cores.any()){
data.cores.first().items.read(dataReads);
}else{

View File

@@ -335,7 +335,7 @@ public class NetServer implements ApplicationListener{
votes += d;
voted.addAll(player.uuid(), admins.getInfo(player.uuid()).lastIP);
Call.sendMessage(Strings.format("[lightgray]@[lightgray] has voted on kicking[orange] @[].[accent] (@/@)\n[lightgray]Type[orange] /vote <y/n>[] to agree.",
Call.sendMessage(Strings.format("[lightgray]@[lightgray] has voted on kicking[orange] @[lightgray].[accent] (@/@)\n[lightgray]Type[orange] /vote <y/n>[] to agree.",
player.name, target.name, votes, votesRequired()));
checkPass();

View File

@@ -7,6 +7,7 @@ import arc.math.*;
import arc.struct.*;
import arc.util.*;
import arc.util.serialization.*;
import mindustry.*;
import mindustry.mod.*;
import mindustry.net.*;
import mindustry.net.Net.*;
@@ -20,9 +21,29 @@ import static mindustry.Vars.*;
public interface Platform{
/** Dynamically creates a class loader for a jar file. */
/** Dynamically creates a class loader for a jar file. This loader must be child-first. */
default ClassLoader loadJar(Fi jar, ClassLoader parent) throws Exception{
return new URLClassLoader(new URL[]{jar.file().toURI().toURL()}, parent);
return new URLClassLoader(new URL[]{jar.file().toURI().toURL()}, parent){
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//check for loaded state
Class<?> loadedClass = findLoadedClass(name);
if(loadedClass == null){
try{
//try to load own class first
loadedClass = findClass(name);
}catch(ClassNotFoundException e){
//use parent if not found
loadedClass = super.loadClass(name, resolve);
}
}
if(resolve){
resolveClass(loadedClass);
}
return loadedClass;
}
};
}
/** Steam: Update lobby visibility.*/
@@ -64,6 +85,9 @@ public interface Platform{
protected Context makeContext(){
Context ctx = super.makeContext();
ctx.setClassShutter(Scripts::allowClass);
if(Vars.mods != null){
ctx.setApplicationClassLoader(Vars.mods.mainLoader());
}
return ctx;
}
});

View File

@@ -201,6 +201,7 @@ public class Renderer implements ApplicationListener{
Draw.proj(camera);
blocks.checkChanges();
blocks.floor.checkChanges();
blocks.processBlocks();

View File

@@ -539,6 +539,38 @@ public class UI implements ApplicationListener, Loadable{
dialog.show();
}
/** Shows a menu that fires a callback when an option is selected. If nothing is selected, -1 is returned. */
public void showMenu(String title, String message, String[][] options, Intc callback){
new Dialog(title){{
cont.row();
cont.image().width(400f).pad(2).colspan(2).height(4f).color(Pal.accent);
cont.row();
cont.add(message).width(400f).wrap().get().setAlignment(Align.center);
cont.row();
int option = 0;
for(var optionsRow : options){
Table buttonRow = buttons.row().table().get().row();
int fullWidth = 400 - (optionsRow.length - 1) * 8; // adjust to count padding as well
int width = fullWidth / optionsRow.length;
int lastWidth = fullWidth - width * (optionsRow.length - 1); // take the rest of space for uneven table
for(int i = 0; i < optionsRow.length; i++){
if(optionsRow[i] == null) continue;
String optionName = optionsRow[i];
int finalOption = option;
buttonRow.button(optionName, () -> {
callback.get(finalOption);
hide();
}).size(i == optionsRow.length - 1 ? lastWidth : width, 50).pad(4);
option++;
}
}
closeOnBack(() -> callback.get(-1));
}}.show();
}
public static String formatTime(float ticks){
int time = (int)(ticks / 60);
if(time < 60) return "0:" + (time < 10 ? "0" : "") + time;

View File

@@ -6,7 +6,6 @@ import arc.math.*;
import arc.math.geom.*;
import arc.math.geom.Geometry.*;
import arc.struct.*;
import arc.struct.ObjectIntMap.*;
import arc.util.*;
import arc.util.noise.*;
import mindustry.content.*;
@@ -21,7 +20,6 @@ import mindustry.maps.*;
import mindustry.maps.filters.*;
import mindustry.maps.filters.GenerateFilter.*;
import mindustry.type.*;
import mindustry.type.Weather.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.blocks.legacy.*;
@@ -336,7 +334,7 @@ public class World{
ui.showErrorMessage("@map.nospawn.pvp");
}
}else if(checkRules.attackMode){ //attack maps need two cores to be valid
invalidMap = state.teams.get(state.rules.waveTeam).noCores();
invalidMap = state.rules.waveTeam.data().noCores();
if(invalidMap){
ui.showErrorMessage("@map.nospawn.attack");
}

View File

@@ -241,14 +241,14 @@ public class MapView extends Element implements GestureListener{
image.setImageSize(editor.width(), editor.height());
if(!ScissorStack.push(rect.set(x, y + Core.scene.marginBottom, width, height))){
if(!ScissorStack.push(rect.set(x + Core.scene.marginLeft, y + Core.scene.marginBottom, width, height))){
return;
}
Draw.color(Pal.remove);
Lines.stroke(2f);
Lines.rect(centerx - sclwidth / 2 - 1, centery - sclheight / 2 - 1, sclwidth + 2, sclheight + 2);
editor.renderer.draw(centerx - sclwidth / 2, centery - sclheight / 2 + Core.scene.marginBottom, sclwidth, sclheight);
editor.renderer.draw(centerx - sclwidth / 2 + Core.scene.marginLeft, centery - sclheight / 2 + Core.scene.marginBottom, sclwidth, sclheight);
Draw.reset();
if(grid){

View File

@@ -31,11 +31,16 @@ public class Damage{
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null);
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null, Fx.dynamicExplosion);
}
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, boolean fire, @Nullable Team ignoreTeam){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, fire, ignoreTeam, Fx.dynamicExplosion);
}
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, boolean fire, @Nullable Team ignoreTeam, Effect explosion){
if(damage){
for(int i = 0; i < Mathf.clamp(power / 700, 0, 8); i++){
int length = 5 + Mathf.clamp((int)(power / 500), 1, 20);
@@ -69,7 +74,7 @@ public class Damage{
float shake = Math.min(explosiveness / 4f + 3f, 9f);
Effect.shake(shake, shake, x, y);
Fx.dynamicExplosion.at(x, y, radius / 8f);
explosion.at(x, y, radius / 8f);
}
public static void createIncend(float x, float y, float range, int amount){

View File

@@ -163,6 +163,8 @@ public class BulletType extends Content implements Cloneable{
public float puddleAmount = 5f;
public Liquid puddleLiquid = Liquids.water;
public boolean displayAmmoMultiplier = true;
public float lightRadius = -1f;
public float lightOpacity = 0.3f;
public Color lightColor = Pal.powerLight;
@@ -405,10 +407,10 @@ public class BulletType extends Content implements Cloneable{
if(status == StatusEffects.none){
status = StatusEffects.shocked;
}
}
if(lightningType == null){
lightningType = !collidesAir ? Bullets.damageLightningGround : Bullets.damageLightning;
}
if(lightningType == null){
lightningType = !collidesAir ? Bullets.damageLightningGround : Bullets.damageLightning;
}
}

View File

@@ -36,6 +36,7 @@ public class LiquidBulletType extends BulletType{
shootEffect = Fx.none;
drag = 0.001f;
knockback = 0.55f;
displayAmmoMultiplier = false;
}
public LiquidBulletType(){

View File

@@ -1,7 +1,6 @@
package mindustry.entities.comp;
import arc.math.*;
import arc.math.geom.*;
import mindustry.annotations.Annotations.*;
import mindustry.gen.*;
@@ -12,7 +11,6 @@ abstract class BoundedComp implements Velc, Posc, Healthc, Flyingc{
static final float warpDst = 40f;
@Import float x, y;
@Import Vec2 vel;
@Override
public void update(){

View File

@@ -222,7 +222,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
}
TeamData data = state.teams.get(team);
TeamData data = team.data();
if(checkPrevious){
//remove existing blocks that have been placed here.
@@ -1399,7 +1399,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return switch(sensor){
case type -> block;
case firstItem -> items == null ? null : items.first();
case config -> block.configurations.containsKey(Item.class) || block.configurations.containsKey(Liquid.class) ? config() : null;
case config -> block.configSenseable() ? config() : null;
case payloadType -> getPayload() instanceof UnitPayload p1 ? p1.unit.type : getPayload() instanceof BuildPayload p2 ? p2.block() : null;
default -> noSensed;
};

View File

@@ -56,7 +56,7 @@ abstract class FireComp implements Timedc, Posc, Syncc, Drawc{
return;
}
if(time >= lifetime || tile == null){
if(time >= lifetime || tile == null || Float.isNaN(lifetime)){
remove();
return;
}

View File

@@ -49,7 +49,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
transient float healTime;
private transient float resupplyTime = Mathf.random(10f);
private transient boolean wasPlayer;
private transient float lastHealth;
private transient boolean wasHealed;
public void moveAt(Vec2 vector){
moveAt(vector, type.accel);
@@ -320,16 +320,23 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
type.landed(self());
}
@Override
public void heal(float amount){
if(health < maxHealth && amount > 0){
wasHealed = true;
}
}
@Override
public void update(){
type.update(self());
if(health > lastHealth && lastHealth > 0 && healTime <= -1f){
if(wasHealed && healTime <= -1f){
healTime = 1f;
}
healTime -= Time.delta / 20f;
lastHealth = health;
wasHealed = false;
//check if environment is unsupported
if(!type.supportsEnv(state.rules.environment) && !dead){
@@ -450,7 +457,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
float power = item().charge * stack().amount * 150f;
if(!spawnedByCore){
Damage.dynamicExplosion(x, y, flammability, explosiveness, power, bounds() / 2f, state.rules.damageExplosions, item().flammability > 1, team);
Damage.dynamicExplosion(x, y, flammability, explosiveness, power, bounds() / 2f, state.rules.damageExplosions, item().flammability > 1, team, type.deathExplosionEffect);
}
float shake = hitSize / 3f;

View File

@@ -134,6 +134,18 @@ public class EventType{
}
}
/** Consider using Menus.registerMenu instead. */
public static class MenuOptionChooseEvent{
public final Player player;
public final int menuId, option;
public MenuOptionChooseEvent(Player player, int menuId, int option){
this.player = player;
this.option = option;
this.menuId = menuId;
}
}
public static class PlayerChatEvent{
public final Player player;
public final String message;

View File

@@ -13,7 +13,6 @@ import mindustry.content.*;
import mindustry.game.EventType.*;
import mindustry.game.Teams.*;
import mindustry.gen.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.power.*;
@@ -142,11 +141,21 @@ public class BlockRenderer{
Tile other = world.tile(cx, cy);
if(other != null){
darkEvents.add(other.pos());
floor.recacheTile(other);
}
}
}
}
public void checkChanges(){
darkEvents.each(pos -> {
var tile = world.tile(pos);
if(tile != null){
tile.data = world.getWallDarkness(tile);
}
});
}
public void drawDarkness(){
if(!darkEvents.isEmpty()){
Draw.flush();
@@ -156,10 +165,9 @@ public class BlockRenderer{
darkEvents.each(pos -> {
var tile = world.tile(pos);
tile.data = world.getWallDarkness(tile);
float darkness = world.getDarkness(tile.x, tile.y);
//then draw the shadow
Draw.colorl(!tile.isDarkened() || darkness <= 0f ? 1f : 1f - Math.min((darkness + 0.5f) / 4f, 1f));
Draw.colorl(darkness <= 0f ? 1f : 1f - Math.min((darkness + 0.5f) / 4f, 1f));
Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1);
});
@@ -186,7 +194,7 @@ public class BlockRenderer{
}
if(brokenFade > 0.001f){
for(BlockPlan block : state.teams.get(player.team()).blocks){
for(BlockPlan block : player.team().data().blocks){
Block b = content.block(block.block);
if(!camera.bounds(Tmp.r1).grow(tilesize * 2f).overlaps(Tmp.r2.setSize(b.size * tilesize).setCenter(block.x * tilesize + b.offset, block.y * tilesize + b.offset))) continue;

View File

@@ -97,7 +97,6 @@ public class FloorRenderer{
/** Queues up a cache change for a tile. Only runs in render loop. */
public void recacheTile(Tile tile){
//TODO will be faster it the position also specified the layer to be recached
//recaching all layers may not be necessary
recacheSet.add(Point2.pack(tile.x / chunksize, tile.y / chunksize));
}
@@ -168,7 +167,6 @@ public class FloorRenderer{
shader.setUniformi("u_texture", 0);
//only ever use the base environment texture
//TODO show error texture for anything else
texture.bind(0);
//enable all mesh attributes

View File

@@ -47,7 +47,6 @@ public class MenuRenderer implements Disposable{
Simplex s1 = new Simplex(offset);
Simplex s2 = new Simplex(offset + 1);
Simplex s3 = new Simplex(offset + 2);
RidgedPerlin rid = new RidgedPerlin(1 + offset, 1);
Block[] selected = Structs.select(
new Block[]{Blocks.sand, Blocks.sandWall},
new Block[]{Blocks.shale, Blocks.shaleWall},
@@ -141,7 +140,7 @@ public class MenuRenderer implements Disposable{
}
if(tendrils){
if(rid.getValue(x, y, 1f / 17f) > 0f){
if(RidgedPerlin.noise2d(1 + offset, x, y, 1f / 17f) > 0f){
floor = Mathf.chance(0.2) ? Blocks.sporeMoss : Blocks.moss;
if(wall != Blocks.air){

View File

@@ -108,7 +108,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
if(build == null || build.items == null) return;
build.items.set(item, amount);
}
@Remote(called = Loc.server, unreliable = true)
public static void clearItems(Building build){
if(build == null || build.items == null) return;
@@ -131,22 +131,24 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
@Remote(called = Loc.both, targets = Loc.both, forward = true, unreliable = true)
public static void deletePlans(Player player, int[] positions){
if(netServer.admins.allowAction(player, ActionType.removePlanned, a -> a.plans = positions)){
if(net.server() && !netServer.admins.allowAction(player, ActionType.removePlanned, a -> a.plans = positions)){
throw new ValidateException(player, "Player cannot remove plans.");
}
var it = state.teams.get(player.team()).blocks.iterator();
//O(n^2) search here; no way around it
outer:
while(it.hasNext()){
BlockPlan req = it.next();
if(player == null) return;
for(int pos : positions){
if(req.x == Point2.x(pos) && req.y == Point2.y(pos)){
it.remove();
continue outer;
}
var it = player.team().data().blocks.iterator();
//O(n^2) search here; no way around it
outer:
while(it.hasNext()){
BlockPlan req = it.next();
for(int pos : positions){
if(req.x == Point2.x(pos) && req.y == Point2.y(pos)){
it.remove();
continue outer;
}
}
}
}
@@ -885,7 +887,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
removed.clear();
//remove blocks to rebuild
Iterator<BlockPlan> broken = state.teams.get(player.team()).blocks.iterator();
Iterator<BlockPlan> broken = player.team().data().blocks.iterator();
while(broken.hasNext()){
BlockPlan req = broken.next();
Block block = content.block(req.block);
@@ -938,9 +940,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
//check if tapped block is configurable
if(build.block.configurable && build.interactable(player.team())){
consumed = true;
if(((!frag.config.isShown() && build.shouldShowConfigure(player)) //if the config fragment is hidden, show
if((!frag.config.isShown() && build.shouldShowConfigure(player)) //if the config fragment is hidden, show
//alternatively, the current selected block can 'agree' to switch config tiles
|| (frag.config.isShown() && frag.config.getSelectedTile().onConfigureTileTapped(build)))){
|| (frag.config.isShown() && frag.config.getSelectedTile().onConfigureTileTapped(build))){
Sounds.click.at(build);
frag.config.showConfig(build);
}

View File

@@ -588,6 +588,30 @@ public class TypeIO{
return new TraceInfo(readString(read), readString(read), read.b() == 1, read.b() == 1, read.i(), read.i());
}
public static void writeStrings(Writes write, String[][] strings){
write.b(strings.length);
for(String[] string : strings){
write.b(string.length);
for(String s : string){
writeString(write, s);
}
}
}
public static String[][] readStrings(Reads read){
int rows = read.ub();
String[][] strings = new String[rows][];
for(int i = 0; i < rows; i++){
int columns = read.ub();
strings[i] = new String[columns];
for(int j = 0; j < columns; j++){
strings[i][j] = readString(read);
}
}
return strings;
}
public static void writeStringData(DataOutput buffer, String string) throws IOException{
if(string != null){
byte[] bytes = string.getBytes(charset);

View File

@@ -1,8 +1,10 @@
package mindustry.logic;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.entities.units.*;
import mindustry.logic.LExecutor.*;
import mindustry.type.*;
import mindustry.world.*;
@@ -55,6 +57,10 @@ public class GlobalConstants{
for(LAccess sensor : LAccess.all){
put("@" + sensor.name(), sensor);
}
for(UnitCommand cmd : UnitCommand.all){
put("@command" + Strings.capitalize(cmd.name()), cmd);
}
}
/** @return a constant ID > 0 if there is a constant with this name, otherwise -1. */

View File

@@ -922,6 +922,7 @@ public class LExecutor{
v.objval instanceof Content ? "[content]" :
v.objval instanceof Building build ? build.block.name :
v.objval instanceof Unit unit ? unit.type.name :
v.objval instanceof Enum<?> e ? e.name() :
"[object]";
exec.textBuffer.append(strValue);

View File

@@ -1,6 +1,5 @@
package mindustry.maps.filters;
import arc.math.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.gen.*;
@@ -17,7 +16,7 @@ public class BlendFilter extends GenerateFilter{
return Structs.arr(
new SliderOption("radius", () -> radius, f -> radius = f, 1f, 10f),
new BlockOption("block", () -> block, b -> block = b, anyOptional),
new BlockOption("floor", () -> floor, b -> floor = b, floorsOnly),
new BlockOption("floor", () -> floor, b -> floor = b, anyOptional),
new BlockOption("ignore", () -> ignore, b -> ignore = b, floorsOptional)
);
}
@@ -34,7 +33,7 @@ public class BlendFilter extends GenerateFilter{
@Override
public void apply(){
if(in.floor == block || block == Blocks.air || in.floor == ignore) return;
if(in.floor == block || block == Blocks.air || in.floor == ignore || (!floor.isFloor() && (in.block == block || in.block == ignore))) return;
int rad = (int)radius;
boolean found = false;
@@ -42,7 +41,7 @@ public class BlendFilter extends GenerateFilter{
outer:
for(int x = -rad; x <= rad; x++){
for(int y = -rad; y <= rad; y++){
if(Mathf.within(x, y, rad)) continue;
if(x*x + y*y > rad*rad) continue;
Tile tile = in.tile(in.x + x, in.y + y);
if(tile.floor() == block || tile.block() == block || tile.overlay() == block){
@@ -53,7 +52,11 @@ public class BlendFilter extends GenerateFilter{
}
if(found){
in.floor = floor;
if(!floor.isFloor()){
in.block = floor;
}else{
in.floor = floor;
}
}
}
}

View File

@@ -9,7 +9,6 @@ import arc.scene.ui.layout.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.gen.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
@@ -105,6 +104,7 @@ public abstract class FilterOption{
if(++i % 10 == 0) dialog.cont.row();
}
dialog.closeOnBack();
dialog.show();
}).pad(4).margin(12f);

View File

@@ -13,7 +13,7 @@ import mindustry.world.*;
public abstract class GenerateFilter{
protected transient float o = (float)(Math.random() * 10000000.0);
protected transient long seed;
protected transient int seed;
protected transient GenerateInput in;
public void apply(Tiles tiles, GenerateInput in){
@@ -75,11 +75,16 @@ public abstract class GenerateFilter{
/** draw any additional guides */
public void draw(Image image){}
/** localized display name */
public String name(){
public String simpleName(){
Class c = getClass();
if(c.isAnonymousClass()) c = c.getSuperclass();
return Core.bundle.get("filter." + c.getSimpleName().toLowerCase().replace("filter", ""), c.getSimpleName().replace("Filter", ""));
return c.getSimpleName().toLowerCase().replace("filter", "");
}
/** localized display name */
public String name(){
var s = simpleName();
return Core.bundle.get("filter." + s);
}
public char icon(){
@@ -112,11 +117,15 @@ public abstract class GenerateFilter{
}
protected float rnoise(float x, float y, float scl, float mag){
return in.pnoise.getValue((int)(x + o), (int)(y + o), 1f / scl) * mag;
return RidgedPerlin.noise2d(seed + 1, (int)(x + o), (int)(y + o), 1f / scl) * mag;
}
protected float rnoise(float x, float y, int octaves, float scl, float falloff, float mag){
return RidgedPerlin.noise2d(seed + 1, (int)(x + o), (int)(y + o), octaves, falloff, 1f / scl) * mag;
}
protected float chance(){
return Mathf.randomSeed(Pack.longInt(in.x, in.y + (int)seed));
return Mathf.randomSeed(Pack.longInt(in.x, in.y + seed));
}
/** an input for generating at a certain coordinate. should only be instantiated once. */
@@ -129,7 +138,6 @@ public abstract class GenerateFilter{
public Block floor, block, overlay;
Simplex noise = new Simplex();
RidgedPerlin pnoise = new RidgedPerlin(0, 1);
TileProvider buffer;
public void apply(int x, int y, Block block, Block floor, Block overlay){
@@ -145,7 +153,6 @@ public abstract class GenerateFilter{
this.width = width;
this.height = height;
noise.setSeed(filter.seed);
pnoise.setSeed((int)(filter.seed + 1));
}
Tile tile(float x, float y){

View File

@@ -10,14 +10,15 @@ import mindustry.world.*;
import static mindustry.Vars.*;
public class MedianFilter extends GenerateFilter{
private static final IntSeq blocks = new IntSeq(), floors = new IntSeq();
float radius = 2;
float percentile = 0.5f;
IntSeq blocks = new IntSeq(), floors = new IntSeq();
@Override
public FilterOption[] options(){
return Structs.arr(
new SliderOption("radius", () -> radius, f -> radius = f, 1f, 12f),
new SliderOption("radius", () -> radius, f -> radius = f, 1f, 10f),
new SliderOption("percentile", () -> percentile, f -> percentile = f, 0f, 1f)
);
}

View File

@@ -8,15 +8,17 @@ import mindustry.world.*;
import static mindustry.maps.filters.FilterOption.*;
public class RiverNoiseFilter extends GenerateFilter{
float scl = 40, threshold = 0f, threshold2 = 0.1f;
float scl = 40, threshold = 0f, threshold2 = 0.1f, octaves = 1, falloff = 0.5f;
Block floor = Blocks.water, floor2 = Blocks.deepwater, block = Blocks.sandWall;
@Override
public FilterOption[] options(){
return Structs.arr(
new SliderOption("scale", () -> scl, f -> scl = f, 1f, 500f),
new SliderOption("threshold", () -> threshold, f -> threshold = f, -1f, 0.3f),
new SliderOption("threshold2", () -> threshold2, f -> threshold2 = f, -1f, 0.3f),
new SliderOption("threshold", () -> threshold, f -> threshold = f, -1f, 1f),
new SliderOption("threshold2", () -> threshold2, f -> threshold2 = f, -1f, 1f),
new SliderOption("octaves", () -> octaves, f -> octaves = f, 1f, 10f),
new SliderOption("falloff", () -> falloff, f -> falloff = f, 0f, 1f),
new BlockOption("block", () -> block, b -> block = b, wallsOnly),
new BlockOption("floor", () -> floor, b -> floor = b, floorsOnly),
new BlockOption("floor2", () -> floor2, b -> floor2 = b, floorsOnly)
@@ -30,7 +32,7 @@ public class RiverNoiseFilter extends GenerateFilter{
@Override
public void apply(){
float noise = rnoise(in.x, in.y, scl, 1f);
float noise = rnoise(in.x, in.y, (int)octaves, scl, falloff, 1f);
if(noise >= threshold){
in.floor = floor;

View File

@@ -18,7 +18,6 @@ import mindustry.world.*;
import static mindustry.Vars.*;
public class SerpuloPlanetGenerator extends PlanetGenerator{
RidgedPerlin rid = new RidgedPerlin(1, 2);
BaseGenerator basegen = new BaseGenerator();
float scl = 5f;
float waterOffset = 0.07f;
@@ -115,7 +114,7 @@ public class SerpuloPlanetGenerator extends PlanetGenerator{
tile.floor = getBlock(position);
tile.block = tile.floor.asFloor().wall;
if(rid.getValue(position.x, position.y, position.z, 22) > 0.31){
if(RidgedPerlin.noise3d(1, position.x, position.y, position.z, 2, 22) > 0.31){
tile.block = Blocks.air;
}
}

View File

@@ -121,6 +121,11 @@ public class ContentParser{
return sound;
});
put(Objectives.Objective.class, (type, data) -> {
if(data.isString()){
var cont = locateAny(data.asString());
if(cont == null) throw new IllegalArgumentException("Unknown objective content: " + data.asString());
return new Research((UnlockableContent)cont);
}
var oc = resolve(data.getString("type", ""), SectorComplete.class);
data.remove("type");
Objectives.Objective obj = make(oc);
@@ -228,6 +233,7 @@ public class ContentParser{
case "item" -> block.consumes.item(find(ContentType.item, child.asString()));
case "items" -> block.consumes.add((Consume)parser.readValue(ConsumeItems.class, child));
case "liquid" -> block.consumes.add((Consume)parser.readValue(ConsumeLiquid.class, child));
case "coolant" -> block.consumes.add((Consume)parser.readValue(ConsumeCoolant.class, child));
case "power" -> {
if(child.isNumber()){
block.consumes.power(child.asFloat());
@@ -543,6 +549,16 @@ public class ContentParser{
return first != null ? first : Vars.content.getByName(type, currentMod.name + "-" + name);
}
private <T extends MappableContent> T locateAny(String name){
for(ContentType t : ContentType.all){
var out = locate(t, name);
if(out != null){
return (T)out;
}
}
return null;
}
<T> T make(Class<T> type){
try{
Constructor<T> cons = type.getDeclaredConstructor();
@@ -679,18 +695,26 @@ public class ContentParser{
lastNode.remove();
}
TechNode node = new TechNode(null, unlock, customRequirements == null ? unlock.researchRequirements() : customRequirements);
TechNode node = new TechNode(null, unlock, customRequirements == null ? ItemStack.empty : customRequirements);
LoadedMod cur = currentMod;
postreads.add(() -> {
currentContent = unlock;
currentMod = cur;
//add custom objectives
if(research.has("objectives")){
node.objectives.addAll(parser.readValue(Objective[].class, research.get("objectives")));
}
//remove old node from parent
if(node.parent != null){
node.parent.children.remove(node);
}
if(customRequirements == null){
node.setupRequirements(unlock.researchRequirements());
}
//find parent node.
TechNode parent = TechTree.all.find(t -> t.content.name.equals(researchName) || t.content.name.equals(currentMod.name + "-" + researchName));

View File

@@ -11,25 +11,23 @@ public class ModClassLoader extends ClassLoader{
}
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//always try the superclass first
try{
return super.loadClass(name, resolve);
}catch(ClassNotFoundException error){
//a child may try to delegate class loading to its parent, which is *this class loader* - do not let that happen
if(inChild) throw error;
int size = children.size;
//if it doesn't exist in the main class loader, try all the children
for(int i = 0; i < size; i++){
try{
inChild = true;
var out = children.get(i).loadClass(name);
inChild = false;
return out;
}catch(ClassNotFoundException ignored){
}
protected Class<?> findClass(String name) throws ClassNotFoundException{
//a child may try to delegate class loading to its parent, which is *this class loader* - do not let that happen
if(inChild) throw new ClassNotFoundException(name);
ClassNotFoundException last = null;
int size = children.size;
//if it doesn't exist in the main class loader, try all the children
for(int i = 0; i < size; i++){
try{
inChild = true;
var out = children.get(i).loadClass(name);
inChild = false;
return out;
}catch(ClassNotFoundException e){
last = e;
}
throw error;
}
throw (last == null ? new ClassNotFoundException(name) : last);
}
}

View File

@@ -60,7 +60,7 @@ public class Scripts implements Disposable{
public String runConsole(String text){
try{
Object o = context.evaluateString(scope, text, "console.js", 1, null);
Object o = context.evaluateString(scope, text, "console.js", 1);
if(o instanceof NativeJavaObject n) o = n.unwrap();
if(o instanceof Undefined) o = "undefined";
return String.valueOf(o);
@@ -172,11 +172,11 @@ public class Scripts implements Disposable{
try{
if(currentMod != null){
//inject script info into file
context.evaluateString(scope, "modName = \"" + currentMod.name + "\"\nscriptName = \"" + file + "\"", "initscript.js", 1, null);
context.evaluateString(scope, "modName = \"" + currentMod.name + "\"\nscriptName = \"" + file + "\"", "initscript.js", 1);
}
context.evaluateString(scope,
wrap ? "(function(){'use strict';\n" + script + "\n})();" : script,
file, 0, null);
file, 0);
return true;
}catch(Throwable t){
if(currentMod != null){
@@ -224,7 +224,7 @@ public class Scripts implements Disposable{
if(!module.exists() || module.isDirectory()) return null;
return new ModuleSource(
new InputStreamReader(new ByteArrayInputStream((module.readString()).getBytes())),
null, new URI(moduleId), root.file().toURI(), validator);
new URI(moduleId), root.file().toURI(), validator);
}
}
}

View File

@@ -188,24 +188,22 @@ public class ArcNetProvider implements NetProvider{
@Override
public void pingHost(String address, int port, Cons<Host> valid, Cons<Exception> invalid){
executor.submit(() -> {
try{
DatagramSocket socket = new DatagramSocket();
long time = Time.millis();
socket.send(new DatagramPacket(new byte[]{-2, 1}, 2, InetAddress.getByName(address), port));
socket.setSoTimeout(2000);
try{
DatagramSocket socket = new DatagramSocket();
long time = Time.millis();
socket.send(new DatagramPacket(new byte[]{-2, 1}, 2, InetAddress.getByName(address), port));
socket.setSoTimeout(2000);
DatagramPacket packet = packetSupplier.get();
socket.receive(packet);
DatagramPacket packet = packetSupplier.get();
socket.receive(packet);
ByteBuffer buffer = ByteBuffer.wrap(packet.getData());
Host host = NetworkIO.readServerData((int)Time.timeSinceMillis(time), packet.getAddress().getHostAddress(), buffer);
ByteBuffer buffer = ByteBuffer.wrap(packet.getData());
Host host = NetworkIO.readServerData((int)Time.timeSinceMillis(time), packet.getAddress().getHostAddress(), buffer);
Core.app.post(() -> valid.get(host));
}catch(Exception e){
Core.app.post(() -> invalid.get(e));
}
});
Core.app.post(() -> valid.get(host));
}catch(Exception e){
Core.app.post(() -> invalid.get(e));
}
}
@Override

View File

@@ -5,6 +5,7 @@ import arc.func.*;
import arc.net.*;
import arc.struct.*;
import arc.util.*;
import arc.util.async.*;
import mindustry.gen.*;
import mindustry.net.Packets.*;
import mindustry.net.Streamable.*;
@@ -12,6 +13,7 @@ import mindustry.net.Streamable.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.concurrent.*;
import static arc.util.Log.*;
import static mindustry.Vars.*;
@@ -31,6 +33,7 @@ public class Net{
private final ObjectMap<Class<?>, Cons> clientListeners = new ObjectMap<>();
private final ObjectMap<Class<?>, Cons2<NetConnection, Object>> serverListeners = new ObjectMap<>();
private final IntMap<StreamBuilder> streams = new IntMap<>();
private final ExecutorService pingExecutor = Threads.executor(Math.max(Runtime.getRuntime().availableProcessors(), 6));
private final NetProvider provider;
@@ -316,10 +319,17 @@ public class Net{
}
/**
* Pings a host in an new thread. If an error occured, failed() should be called with the exception.
* Pings a host in a pooled thread. If an error occurred, failed() should be called with the exception.
*/
public void pingHost(String address, int port, Cons<Host> valid, Cons<Exception> failed){
provider.pingHost(address, port, valid, failed);
pingExecutor.submit(() -> provider.pingHost(address, port, valid, failed));
}
/**
* Pings a host in an new thread. If an error occurred, failed() should be called with the exception.
*/
public void pingHostThread(String address, int port, Cons<Host> valid, Cons<Exception> failed){
Threads.daemon(() -> provider.pingHost(address, port, valid, failed));
}
/**
@@ -367,7 +377,7 @@ public class Net{
*/
void discoverServers(Cons<Host> callback, Runnable done);
/** Ping a host. If an error occurred, failed() should be called with the exception. */
/** Ping a host. If an error occurred, failed() should be called with the exception. This method should block. */
void pingHost(String address, int port, Cons<Host> valid, Cons<Exception> failed);
/** Host a server at specified port. */

View File

@@ -71,6 +71,7 @@ public class UnitType extends UnlockableContent{
public boolean omniMovement = true;
public Effect fallEffect = Fx.fallSmoke;
public Effect fallThrusterEffect = Fx.fallSmoke;
public Effect deathExplosionEffect = Fx.dynamicExplosion;
public Seq<Ability> abilities = new Seq<>();
public BlockFlag targetFlag = BlockFlag.generator;

View File

@@ -0,0 +1,102 @@
package mindustry.ui;
import arc.*;
import arc.struct.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import static mindustry.Vars.*;
/** Class for handling menus and notifications across the network. Unstable API! */
public class Menus{
private static IntMap<MenuListener> menuListeners = new IntMap<>();
/** Register a *global* menu listener. If no option is chosen, the option is returned as -1. */
public static void registerMenu(int id, MenuListener listener){
menuListeners.put(id, listener);
}
//do not invoke any of the methods below directly, use Call
@Remote(variants = Variant.both)
public static void menu(int menuId, String title, String message, String[][] options){
if(title == null) title = "";
if(options == null) options = new String[0][0];
ui.showMenu(title, message, options, (option) -> Call.menuChoose(player, menuId, option));
}
@Remote(targets = Loc.both, called = Loc.both)
public static void menuChoose(@Nullable Player player, int menuId, int option){
if(player != null && menuListeners.containsKey(menuId)){
Events.fire(new MenuOptionChooseEvent(player, menuId, option));
menuListeners.get(menuId).get(player, option);
}
}
@Remote(variants = Variant.both, unreliable = true)
public static void setHudText(String message){
if(message == null) return;
ui.hudfrag.setHudText(message);
}
@Remote(variants = Variant.both)
public static void hideHudText(){
ui.hudfrag.toggleHudText(false);
}
/** TCP version */
@Remote(variants = Variant.both)
public static void setHudTextReliable(String message){
setHudText(message);
}
@Remote(variants = Variant.both)
public static void announce(String message){
if(message == null) return;
ui.announce(message);
}
@Remote(variants = Variant.both)
public static void infoMessage(String message){
if(message == null) return;
ui.showText("", message);
}
@Remote(variants = Variant.both)
public static void infoPopup(String message, float duration, int align, int top, int left, int bottom, int right){
if(message == null) return;
ui.showInfoPopup(message, duration, align, top, left, bottom, right);
}
@Remote(variants = Variant.both)
public static void label(String message, float duration, float worldx, float worldy){
if(message == null) return;
ui.showLabel(message, duration, worldx, worldy);
}
@Remote(variants = Variant.both)
public static void infoToast(String message, float duration){
if(message == null) return;
ui.showInfoToast(message, duration);
}
@Remote(variants = Variant.both)
public static void warningToast(int unicode, String text){
if(text == null || Fonts.icon.getData().getGlyph((char)unicode) == null) return;
ui.hudfrag.showToast(Fonts.getGlyph(Fonts.icon, (char)unicode), text);
}
public interface MenuListener{
void get(Player player, int option);
}
}

View File

@@ -67,7 +67,7 @@ public class ContentInfoDialog extends BaseDialog{
for(Stat stat : map.keys()){
table.table(inset -> {
inset.left();
inset.add("[lightgray]" + stat.localized() + ":[] ").left();
inset.add("[lightgray]" + stat.localized() + ":[] ").left().top();
Seq<StatValue> arr = map.get(stat);
for(StatValue value : arr){
value.display(inset);

View File

@@ -364,7 +364,7 @@ public class JoinDialog extends BaseDialog{
for(String address : group.addresses){
String resaddress = address.contains(":") ? address.split(":")[0] : address;
int resport = address.contains(":") ? Strings.parseInt(address.split(":")[1]) : port;
net.pingHost(resaddress, resport, res -> {
net.pingHostThread(resaddress, resport, res -> {
if(refreshes != cur) return;
res.port = resport;

View File

@@ -151,7 +151,7 @@ public class ModsDialog extends BaseDialog{
float w = Math.min(Core.graphics.getWidth() / 1.1f, 520f);
cont.clear();
cont.defaults().width(Math.min(Core.graphics.getWidth() / 1.2f, 520f)).pad(4);
cont.defaults().width(Math.min(Core.graphics.getWidth() / 1.2f, 556f)).pad(4);
cont.add("@mod.reloadrequired").visible(mods::requiresReload).center().get().setAlignment(Align.center);
cont.row();

View File

@@ -286,7 +286,7 @@ public class HudFragment extends Fragment{
.update(label -> label.color.set(Color.orange).lerp(Color.scarlet, Mathf.absin(Time.time, 2f, 1f))), true,
() -> {
if(!shown || state.isPaused()) return false;
if(state.isMenu() || !state.teams.get(player.team()).hasCore()){
if(state.isMenu() || !player.team().data().hasCore()){
coreAttackTime[0] = 0f;
return false;
}
@@ -322,7 +322,17 @@ public class HudFragment extends Fragment{
}
return max == 0f ? 0f : val / max;
}).blink(Color.white).outline(new Color(0, 0, 0, 0.6f), 7f)).grow())
.fillX().width(320f).height(60f).name("boss").visible(() -> state.rules.waves && state.boss() != null).padTop(7);
.fillX().width(320f).height(60f).name("boss").visible(() -> state.rules.waves && state.boss() != null).padTop(7).row();
t.table(Styles.black3, p -> p.margin(4).label(() -> hudText).style(Styles.outlineLabel)).touchable(Touchable.disabled).with(p -> p.visible(() -> {
p.color.a = Mathf.lerpDelta(p.color.a, Mathf.num(showHudText), 0.2f);
if(state.isMenu()){
p.color.a = 0f;
showHudText = false;
}
return p.color.a >= 0.001f;
}));
});
//spawner warning
@@ -342,20 +352,6 @@ public class HudFragment extends Fragment{
t.add("@saving").style(Styles.outlineLabel);
});
parent.fill(p -> {
p.name = "hudtext";
p.top().table(Styles.black3, t -> t.margin(4).label(() -> hudText)
.style(Styles.outlineLabel)).padTop(10).visible(p.color.a >= 0.001f);
p.update(() -> {
p.color.a = Mathf.lerpDelta(p.color.a, Mathf.num(showHudText), 0.2f);
if(state.isMenu()){
p.color.a = 0f;
showHudText = false;
}
});
p.touchable = Touchable.disabled;
});
//TODO DEBUG: rate table
if(false)
parent.fill(t -> {

View File

@@ -451,6 +451,10 @@ public class Block extends UnlockableContent{
}
public boolean configSenseable(){
return configurations.containsKey(Item.class) || configurations.containsKey(Liquid.class);
}
public Object nextConfig(){
if(saveConfig && lastConfig != null){
return lastConfig;

View File

@@ -119,11 +119,11 @@ public class Tile implements Position, QuadTreeObject, Displayable{
float result = 0f;
if(block.hasItems){
result += build.items.sum((item, amount) -> item.flammability * amount) / block.itemCapacity * Mathf.clamp(block.itemCapacity / 2.4f, 1f, 3f);
result += build.items.sum((item, amount) -> item.flammability * amount) / Math.max(block.itemCapacity, 1) * Mathf.clamp(block.itemCapacity / 2.4f, 1f, 3f);
}
if(block.hasLiquids){
result += build.liquids.sum((liquid, amount) -> liquid.flammability * amount / 1.6f) / block.liquidCapacity * Mathf.clamp(block.liquidCapacity / 30f, 1f, 2f);
result += build.liquids.sum((liquid, amount) -> liquid.flammability * amount / 1.6f) / Math.max(block.liquidCapacity, 1) * Mathf.clamp(block.liquidCapacity / 30f, 1f, 2f);
}
return result;

View File

@@ -53,7 +53,7 @@ public class ForceProjector extends Block{
hasItems = true;
ambientSound = Sounds.shield;
ambientSoundVolume = 0.08f;
consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, 0.1f)).boost().update(false);
consumes.add(new ConsumeCoolant(0.1f)).boost().update(false);
}
@Override

View File

@@ -37,7 +37,7 @@ public class BaseTurret extends Block{
public void init(){
if(acceptCoolant && !consumes.has(ConsumeType.liquid)){
hasLiquids = true;
consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, 0.2f)).update(false).boost();
consumes.add(new ConsumeCoolant(0.2f)).update(false).boost();
}
super.init();

View File

@@ -19,7 +19,7 @@ public class LaserTurret extends PowerTurret{
super(name);
canOverdrive = false;
consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, 0.01f)).update(false);
consumes.add(new ConsumeCoolant(0.01f)).update(false);
coolantMultiplier = 1f;
}

View File

@@ -27,6 +27,8 @@ public class LiquidTurret extends Turret{
hasLiquids = true;
loopSound = Sounds.spray;
shootSound = Sounds.none;
smokeEffect = Fx.none;
shootEffect = Fx.none;
outlinedIcon = 1;
}

View File

@@ -116,7 +116,7 @@ public class Turret extends ReloadTurret{
public void init(){
if(acceptCoolant && !consumes.has(ConsumeType.liquid)){
hasLiquids = true;
consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, coolantUsage)).update(false).boost();
consumes.add(new ConsumeCoolant(coolantUsage)).update(false).boost();
}
if(shootLength < 0) shootLength = size * tilesize / 2f;

View File

@@ -147,7 +147,7 @@ public class Duct extends Block implements Autotiler{
@Override
public boolean acceptItem(Building source, Item item){
return current == null && items.total() == 0 &&
(source.block instanceof Duct || Edges.getFacingEdge(source.tile(), tile).relativeTo(tile) == rotation);
((source.block.rotate && source.front() == this && source.block.hasItems) || Edges.getFacingEdge(source.tile(), tile).relativeTo(tile) == rotation);
}
@Override

View File

@@ -1,6 +1,7 @@
package mindustry.world.blocks.distribution;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
@@ -16,8 +17,10 @@ import mindustry.world.meta.*;
import static mindustry.Vars.*;
//TODO display range
public class DuctBridge extends Block{
private static BuildPlan otherReq;
private int otherDst = 0;
public @Load("@-bridge") TextureRegion bridgeRegion;
public @Load("@-bridge-bottom") TextureRegion bridgeBotRegion;
//public @Load("@-bridge-top") TextureRegion bridgeTopRegion;
@@ -46,6 +49,26 @@ public class DuctBridge extends Block{
Draw.rect(dirRegion, req.drawx(), req.drawy(), req.rotation * 90);
}
@Override
public void drawRequestConfigTop(BuildPlan req, Eachable<BuildPlan> list){
otherReq = null;
otherDst = range;
Point2 d = Geometry.d4(req.rotation);
list.each(other -> {
if(other.block == this && req != other && Mathf.clamp(other.x - req.x, -1, 1) == d.x && Mathf.clamp(other.y - req.y, -1, 1) == d.y){
int dst = Math.max(Math.abs(other.x - req.x), Math.abs(other.y - req.y));
if(dst <= otherDst){
otherReq = other;
otherDst = dst;
}
}
});
if(otherReq != null){
drawBridge(req.rotation, req.drawx(), req.drawy(), otherReq.drawx(), otherReq.drawy());
}
}
@Override
public TextureRegion[] icons(){
return new TextureRegion[]{region, dirRegion};
@@ -56,16 +79,14 @@ public class DuctBridge extends Block{
Placement.calculateNodes(points, this, rotation, (point, other) -> Math.max(Math.abs(point.x - other.x), Math.abs(point.y - other.y)) <= range);
}
@Override
public void drawPlace(int x, int y, int rotation, boolean valid){
super.drawPlace(x, y, rotation, valid);
public void drawPlace(int x, int y, int rotation, boolean valid, boolean line){
int length = range;
Building found = null;
int dx = Geometry.d4x(rotation), dy = Geometry.d4y(rotation);
//find the link
for(int i = 1; i <= range; i++){
Tile other = world.tile(x + Geometry.d4x(rotation) * i, y + Geometry.d4y(rotation) * i);
Tile other = world.tile(x + dx * i, y + dy * i);
if(other != null && other.build instanceof DuctBridgeBuild build && build.team == player.team()){
length = i;
@@ -74,17 +95,50 @@ public class DuctBridge extends Block{
}
}
Drawf.dashLine(Pal.placing,
x * tilesize + Geometry.d4[rotation].x * (tilesize / 2f + 2),
y * tilesize + Geometry.d4[rotation].y * (tilesize / 2f + 2),
x * tilesize + Geometry.d4[rotation].x * (length) * tilesize,
y * tilesize + Geometry.d4[rotation].y * (length) * tilesize
);
if(found != null){
Drawf.square(found.x, found.y, found.block.size * tilesize/2f + 2.5f, 0f);
if(line || found != null){
Drawf.dashLine(Pal.placing,
x * tilesize + dx * (tilesize / 2f + 2),
y * tilesize + dy * (tilesize / 2f + 2),
x * tilesize + dx * (length) * tilesize,
y * tilesize + dy * (length) * tilesize
);
}
if(found != null){
if(line){
Drawf.square(found.x, found.y, found.block.size * tilesize/2f + 2.5f, 0f);
}else{
Drawf.square(found.x, found.y, 2f);
}
}
}
@Override
public void drawPlace(int x, int y, int rotation, boolean valid){
super.drawPlace(x, y, rotation, valid);
drawPlace(x, y, rotation, valid, true);
}
public void drawBridge(int rotation, float x1, float y1, float x2, float y2){
Draw.alpha(Renderer.bridgeOpacity);
float
angle = Angles.angle(x1, y1, x2, y2),
cx = (x1 + x2)/2f,
cy = (y1 + y2)/2f,
len = Math.max(Math.abs(x1 - x2), Math.abs(y1 - y2)) - size * tilesize;
Draw.rect(bridgeRegion, cx, cy, len, tilesize, angle);
Draw.color(0.4f, 0.4f, 0.4f, 0.4f * Renderer.bridgeOpacity);
Draw.rect(bridgeBotRegion, cx, cy, len, tilesize, angle);
Draw.reset();
Draw.alpha(Renderer.bridgeOpacity);
for(float i = 6f; i <= len + size * tilesize - 5f; i += 5f){
Draw.rect(arrowRegion, x1 + Geometry.d4x(rotation) * i, y1 + Geometry.d4y(rotation) * i, angle);
}
Draw.reset();
}
public boolean positionsValid(int x1, int y1, int x2, int y2){
@@ -108,24 +162,42 @@ public class DuctBridge extends Block{
var link = findLink();
if(link != null){
Draw.z(Layer.power);
Draw.alpha(Renderer.bridgeOpacity);
float
angle = angleTo(link),
cx = (x + link.x)/2f,
cy = (y + link.y)/2f,
len = Math.max(Math.abs(x - link.x), Math.abs(y - link.y)) - size * tilesize;
drawBridge(rotation, x, y, link.x, link.y);
}
}
Draw.rect(bridgeRegion, cx, cy, len, tilesize, angle);
Draw.color(0.4f, 0.4f, 0.4f, 0.4f * Renderer.bridgeOpacity);
Draw.rect(bridgeBotRegion, cx, cy, len, tilesize, angle);
Draw.reset();
Draw.alpha(Renderer.bridgeOpacity);
@Override
public void drawSelect(){
drawPlace(tile.x, tile.y, rotation, true, false);
//draw incoming bridges
for(int dir = 0; dir < 4; dir++){
if(dir != rotation){
int dx = Geometry.d4x(dir), dy = Geometry.d4y(dir);
int length = range;
Building found = null;
for(float i = 6f; i <= len + size * tilesize - 5f; i += 5f){
Draw.rect(arrowRegion, x + Geometry.d4x(rotation) * i, y + Geometry.d4y(rotation) * i, angle);
//find the link
for(int i = 1; i <= range; i++){
Tile other = world.tile(tile.x + dx * i, tile.y + dy * i);
if(other != null && other.build instanceof DuctBridgeBuild build && build.team == player.team() && (build.rotation + 2) % 4 == dir){
length = i;
found = other.build;
break;
}
}
if(found != null){
Drawf.dashLine(Pal.place,
found.x - dx * (tilesize / 2f + 2),
found.y - dy * (tilesize / 2f + 2),
found.x - dx * (length) * tilesize,
found.y - dy * (length) * tilesize
);
Drawf.square(found.x, found.y, 2f, 45f, Pal.place);
}
}
Draw.reset();
}
}
@@ -173,7 +245,7 @@ public class DuctBridge extends Block{
@Override
public boolean acceptItem(Building source, Item item){
int rel = this.relativeTo(source);
int rel = this.relativeToEdge(source.tile);
return items.total() < itemCapacity && rel != rotation && occupied[(rel + 2) % 4] == null;
}
}

View File

@@ -0,0 +1,19 @@
package mindustry.world.blocks.environment;
import mindustry.world.meta.*;
/** Class for quickly defining a floor with no water and no variants. Offers no new functionality. */
public class MetalFloor extends Floor{
public MetalFloor(String name){
super(name);
variants = 0;
attributes.set(Attribute.water, -1);
}
public MetalFloor(String name, int variants){
super(name);
this.variants = variants;
attributes.set(Attribute.water, -1);
}
}

View File

@@ -16,6 +16,11 @@ public class BlockUnloader extends BlockLoader{
return true;
}
@Override
public boolean rotatedOutput(int x, int y){
return false;
}
public class BlockUnloaderBuild extends BlockLoaderBuild{
@Override

View File

@@ -66,6 +66,12 @@ public class PayloadMassDriver extends PayloadBlock{
config(Integer.class, (PayloadDriverBuild tile, Integer point) -> tile.link = point);
}
@Override
public void init(){
super.init();
clipSize = Math.max(clipSize, range*2f + tilesize*size);
}
@Override
public void setStats(){
super.setStats();

View File

@@ -0,0 +1,20 @@
package mindustry.world.blocks.power;
import arc.func.*;
import mindustry.gen.*;
import mindustry.world.consumers.*;
/** A power consumer that uses a dynamic amount of power. */
public class DynamicConsumePower extends ConsumePower{
private final Floatf<Building> usage;
public DynamicConsumePower(Floatf<Building> usage){
super(0, 0, false);
this.usage = usage;
}
@Override
public float requestedPower(Building entity){
return usage.get(entity);
}
}

View File

@@ -20,6 +20,8 @@ import mindustry.world.*;
import mindustry.world.meta.*;
public class CommandCenter extends Block{
public final int timerEffect = timers ++;
public TextureRegionDrawable[] commandRegions = new TextureRegionDrawable[UnitCommand.all.length];
public Color topColor = null, bottomColor = Color.valueOf("5e5e5e");
public Effect effect = Fx.commandSend;
@@ -33,11 +35,17 @@ public class CommandCenter extends Block{
solid = true;
configurable = true;
drawDisabled = false;
logicConfigurable = true;
config(UnitCommand.class, (CommandBuild build, UnitCommand command) -> {
build.team.data().command = command;
effect.at(build, effectSize);
Events.fire(new CommandIssueEvent(build, command));
if(build.team.data().command != command){
build.team.data().command = command;
//do not spam effect
if(build.timer(timerEffect, 60f)){
effect.at(build, effectSize);
}
Events.fire(new CommandIssueEvent(build, command));
}
});
}
@@ -52,6 +60,11 @@ public class CommandCenter extends Block{
}
}
@Override
public boolean configSenseable(){
return true;
}
public class CommandBuild extends Building{
@Override

View File

@@ -76,7 +76,7 @@ public class RepairPoint extends Block{
public void init(){
if(acceptCoolant){
hasLiquids = true;
consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, coolantUse)).optional(true, true);
consumes.add(new ConsumeCoolant(coolantUse)).optional(true, true);
}
consumes.powerCond(powerUse, (RepairPointBuild entity) -> entity.target != null);

View File

@@ -0,0 +1,16 @@
package mindustry.world.consumers;
/** A ConsumeLiquidFilter that consumes specific coolant, selected based on stats. */
public class ConsumeCoolant extends ConsumeLiquidFilter{
public float maxTemp = 0.5f, maxFlammability = 0.1f;
public ConsumeCoolant(float amount){
this.filter = liquid -> liquid.temperature <= maxTemp && liquid.flammability < maxFlammability;
this.amount = amount;
}
//mods
public ConsumeCoolant(){
this(0.1f);
}
}

View File

@@ -4,12 +4,14 @@ import mindustry.gen.*;
public abstract class ConsumeLiquidBase extends Consume{
/** amount used per frame */
public final float amount;
public float amount;
public ConsumeLiquidBase(float amount){
this.amount = amount;
}
public ConsumeLiquidBase(){}
@Override
public ConsumeType type(){
return ConsumeType.liquid;

View File

@@ -11,13 +11,17 @@ import mindustry.world.meta.*;
import static mindustry.Vars.*;
public class ConsumeLiquidFilter extends ConsumeLiquidBase{
public final Boolf<Liquid> filter;
public Boolf<Liquid> filter;
public ConsumeLiquidFilter(Boolf<Liquid> liquid, float amount){
super(amount);
this.filter = liquid;
}
public ConsumeLiquidFilter(){
this.filter = l -> false;
}
@Override
public void applyLiquidFilter(Bits arr){
content.liquids().each(filter, item -> arr.set(item.id));

View File

@@ -68,6 +68,11 @@ public class Consumers{
return add(new ConditionalConsumePower(usage, (Boolf<Building>)cons));
}
/** Creates a consumer that consumes a dynamic amount of power. */
public <T extends Building> ConsumePower powerDynamic(Floatf<T> usage){
return add(new DynamicConsumePower((Floatf<Building>)usage));
}
/**
* Creates a consumer which stores power.
* @param powerCapacity The maximum capacity in power units.

View File

@@ -8,10 +8,12 @@ import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.ctype.*;
import mindustry.entities.bullet.*;
import mindustry.gen.*;
import mindustry.maps.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.world.*;
@@ -117,6 +119,51 @@ public class StatValues{
);
}
public static StatValue floors(Attribute attr, boolean floating, float scale, boolean startZero){
return table -> table.table(c -> {
Runnable[] rebuild = {null};
Map[] lastMap = {null};
rebuild[0] = () -> {
c.clearChildren();
c.left();
if(state.isGame()){
var blocks = Vars.content.blocks()
.select(block -> block instanceof Floor f && indexer.isBlockPresent(block) && f.attributes.get(attr) != 0 && !(f.isLiquid && !floating))
.<Floor>as().with(s -> s.sort(f -> f.attributes.get(attr)));
if(blocks.any()){
int i = 0;
for(var block : blocks){
floorEfficiency(block, block.attributes.get(attr) * scale, startZero).display(c);
if(++i % 5 == 0){
c.row();
}
}
}else{
c.add("@none.found");
}
}else{
c.add("@stat.showinmap");
}
};
rebuild[0].run();
//rebuild when map changes.
c.update(() -> {
Map current = state.isGame() ? state.map : null;
if(current != lastMap[0]){
rebuild[0].run();
lastMap[0] = current;
}
});
});
}
public static StatValue blocks(Boolf<Block> pred){
return blocks(content.blocks().select(pred));
}
@@ -246,7 +293,7 @@ public class StatValues{
sep(bt, Core.bundle.format("bullet.splashdamage", (int)type.splashDamage, Strings.fixed(type.splashDamageRadius / tilesize, 1)));
}
if(!unit && !Mathf.equal(type.ammoMultiplier, 1f)){
if(!unit && !Mathf.equal(type.ammoMultiplier, 1f) && type.displayAmmoMultiplier){
sep(bt, Core.bundle.format("bullet.multiplier", (int)type.ammoMultiplier));
}

View File

@@ -3,9 +3,7 @@ package mindustry.world.meta;
import arc.struct.ObjectMap.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.type.*;
import mindustry.world.blocks.environment.*;
/** Hold and organizes a list of block stats. */
public class Stats{
@@ -68,11 +66,7 @@ public class Stats{
}
public void add(Stat stat, Attribute attr, boolean floating, float scale, boolean startZero){
for(var block : Vars.content.blocks()
.select(block -> block instanceof Floor f && f.attributes.get(attr) != 0 && !(f.isLiquid && !floating))
.<Floor>as().with(s -> s.sort(f -> f.attributes.get(attr)))){
add(stat, StatValues.floorEfficiency(block, block.attributes.get(attr) * scale, startZero));
}
add(stat, StatValues.floors(attr, floating, scale, startZero));
}
/** Adds a single string value with this stat. */