diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index e6e44ff8c1..ca9d3eeb2c 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -1317,10 +1317,10 @@ public class Blocks implements ContentList{ }}; launchPad = new LaunchPad("launch-pad"){{ - requirements(Category.effect, BuildVisibility.campaignOnly, ItemStack.with(Items.copper, 250, Items.silicon, 75, Items.lead, 100)); + requirements(Category.effect, BuildVisibility.campaignOnly, ItemStack.with(Items.copper, 350, Items.silicon, 140, Items.lead, 200, Items.titanium, 150)); size = 3; itemCapacity = 100; - launchTime = 60*3;//60f * 15; + launchTime = 60f * 15; hasPower = true; consumes.power(4f); }}; diff --git a/core/src/mindustry/content/Fx.java b/core/src/mindustry/content/Fx.java index c2266de60a..b0e92d4203 100644 --- a/core/src/mindustry/content/Fx.java +++ b/core/src/mindustry/content/Fx.java @@ -161,7 +161,7 @@ public class Fx{ rocketSmoke = new Effect(120, e -> { color(Color.gray); - alpha(Mathf.clamp(e.fout()*1.6f - e.rotation*0.8f)); + alpha(Mathf.clamp(e.fout()*1.6f - Interp.pow3In.apply(e.rotation)*1.2f)); Fill.circle(e.x, e.y, (1f + 6f * e.rotation) - e.fin()*2f); }), diff --git a/core/src/mindustry/entities/def/PlayerComp.java b/core/src/mindustry/entities/def/PlayerComp.java index fa2ec06dec..9371966112 100644 --- a/core/src/mindustry/entities/def/PlayerComp.java +++ b/core/src/mindustry/entities/def/PlayerComp.java @@ -144,6 +144,10 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra con.kick(reason); } + void kick(String reason, int duration){ + con.kick(reason, duration); + } + @Override public void draw(){ Draw.z(Layer.playerName); diff --git a/core/src/mindustry/entities/def/UnitComp.java b/core/src/mindustry/entities/def/UnitComp.java index 45e607455a..00104c457e 100644 --- a/core/src/mindustry/entities/def/UnitComp.java +++ b/core/src/mindustry/entities/def/UnitComp.java @@ -177,6 +177,10 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I return controller instanceof Playerc; } + public Playerc getPlayer(){ + return isPlayer() ? (Playerc)controller : null; + } + @Override public void killed(){ float explosiveness = 2f + item().explosiveness * stack().amount; diff --git a/core/src/mindustry/game/Saves.java b/core/src/mindustry/game/Saves.java index 4c3afd013c..41f722dea2 100644 --- a/core/src/mindustry/game/Saves.java +++ b/core/src/mindustry/game/Saves.java @@ -243,7 +243,7 @@ public class Saves{ } public ObjectFloatMap getProductionRates(){ - return meta.productionRates; + return meta.exportRates; } public String getPlayTime(){ diff --git a/core/src/mindustry/game/Stats.java b/core/src/mindustry/game/Stats.java index 2740428770..785479a080 100644 --- a/core/src/mindustry/game/Stats.java +++ b/core/src/mindustry/game/Stats.java @@ -4,8 +4,9 @@ import arc.math.*; import arc.struct.*; import arc.util.*; import mindustry.type.*; +import mindustry.world.blocks.storage.CoreBlock.*; -import static mindustry.Vars.content; +import static mindustry.Vars.*; public class Stats{ /** export window size in seconds */ @@ -13,7 +14,7 @@ public class Stats{ /** refresh period of export in ticks */ private static final float refreshPeriod = 60; - /** Items delivered to global resoure counter. Zones only. */ + /** Total items delivered to global resoure counter. Campaign only. */ public ObjectIntMap itemsDelivered = new ObjectIntMap<>(); /** Enemy (red team) units destroyed. */ public int enemyUnitsDestroyed; @@ -27,42 +28,64 @@ public class Stats{ public int buildingsDeconstructed; /** Friendly buildings destroyed. */ public int buildingsDestroyed; + /** Export statistics. */ + public ObjectMap export = new ObjectMap<>(); /** Counter refresh state. */ private transient Interval time = new Interval(); - /** Export statistics. */ - public ObjectMap production = new ObjectMap<>(); + /** Core item storage to prevent spoofing. */ + private transient int[] lastCoreItems; /** Updates export statistics. */ public void handleItemExport(ItemStack stack){ - production.getOr(stack.item, ProductionStat::new).counter += stack.amount; + export.getOr(stack.item, ExportStat::new).counter += stack.amount; } public float getExport(Item item){ - return production.getOr(item, ProductionStat::new).mean; + return export.getOr(item, ExportStat::new).mean; } public void update(){ + //create last stored core items + if(lastCoreItems == null){ + lastCoreItems = new int[content.items().size]; + updateCoreDeltas(); + } //refresh throughput if(time.get(refreshPeriod)){ - for(ProductionStat stat : production.values()){ + CoreEntity ent = state.rules.defaultTeam.core(); + + export.each((item, stat) -> { //initialize stat after loading if(!stat.loaded){ stat.means.fill(stat.mean); stat.loaded = true; } - stat.means.add(stat.counter); + //how the resources changed - only interested in negative deltas, since that's what happens during spoofing + int coreDelta = Math.min(ent == null ? 0 : ent.items.get(item) - lastCoreItems[item.id], 0); + + //add counter, subtract how many items were taken from the core during this time + stat.means.add(Math.max(stat.counter + coreDelta, 0)); stat.counter = 0; stat.mean = stat.means.rawMean(); - } + }); + + updateCoreDeltas(); } } - public ObjectFloatMap productionRates(){ + private void updateCoreDeltas(){ + CoreEntity ent = state.rules.defaultTeam.core(); + for(int i = 0; i < lastCoreItems.length; i++){ + lastCoreItems[i] = ent == null ? 0 : ent.items.get(i); + } + } + + public ObjectFloatMap exportRates(){ ObjectFloatMap map = new ObjectFloatMap<>(); - production.each((item, value) -> map.put(item, value.mean)); + export.each((item, value) -> map.put(item, value.mean)); return map; } @@ -117,9 +140,9 @@ public class Stats{ F, D, C, B, A, S, SS } - public static class ProductionStat{ + public static class ExportStat{ public transient float counter; - public transient WindowedMean means = new WindowedMean(content.items().size); + public transient WindowedMean means = new WindowedMean(exportWindow); public transient boolean loaded; public float mean; } diff --git a/core/src/mindustry/game/Team.java b/core/src/mindustry/game/Team.java index b3da5045ad..ba2565ea92 100644 --- a/core/src/mindustry/game/Team.java +++ b/core/src/mindustry/game/Team.java @@ -5,6 +5,7 @@ import arc.graphics.*; import arc.math.*; import arc.struct.*; import arc.util.*; +import arc.util.ArcAnnotate.*; import mindustry.game.Teams.*; import mindustry.graphics.*; import mindustry.world.blocks.storage.CoreBlock.*; @@ -72,7 +73,7 @@ public class Team implements Comparable{ return state.teams.get(this); } - public CoreEntity core(){ + public @Nullable CoreEntity core(){ return data().core(); } diff --git a/core/src/mindustry/game/Teams.java b/core/src/mindustry/game/Teams.java index 208d2c211e..04e6792dee 100644 --- a/core/src/mindustry/game/Teams.java +++ b/core/src/mindustry/game/Teams.java @@ -165,8 +165,8 @@ public class Teams{ return cores.isEmpty(); } - public CoreEntity core(){ - return cores.first(); + public @Nullable CoreEntity core(){ + return cores.isEmpty() ? null : cores.first(); } @Override diff --git a/core/src/mindustry/game/Universe.java b/core/src/mindustry/game/Universe.java index 8f7917cbb8..9d2fc1879f 100644 --- a/core/src/mindustry/game/Universe.java +++ b/core/src/mindustry/game/Universe.java @@ -76,10 +76,11 @@ public class Universe{ //calculate passive items for(Planet planet : content.planets()){ for(Sector sector : planet.sectors){ - if(sector.hasSave()){ + //make sure this is a different sector + if(sector.hasSave() && sector != state.rules.sector){ SaveMeta meta = sector.save.meta; - for(Entry entry : meta.productionRates){ + for(Entry entry : meta.exportRates){ //total is calculated by items/sec (value) * turn duration in seconds int total = (int)(entry.value * turnDuration / 60f); diff --git a/core/src/mindustry/io/SaveMeta.java b/core/src/mindustry/io/SaveMeta.java index ee114a0f5f..a28b1ac6ce 100644 --- a/core/src/mindustry/io/SaveMeta.java +++ b/core/src/mindustry/io/SaveMeta.java @@ -18,10 +18,10 @@ public class SaveMeta{ public StringMap tags; public String[] mods; /** These are in items/second. */ - public ObjectFloatMap productionRates; + public ObjectFloatMap exportRates; public boolean hasProduction; - public SaveMeta(int version, long timestamp, long timePlayed, int build, String map, int wave, Rules rules, ObjectFloatMap productionRates, StringMap tags){ + public SaveMeta(int version, long timestamp, long timePlayed, int build, String map, int wave, Rules rules, ObjectFloatMap exportRates, StringMap tags){ this.version = version; this.build = build; this.timestamp = timestamp; @@ -31,8 +31,8 @@ public class SaveMeta{ this.rules = rules; this.tags = tags; this.mods = JsonIO.read(String[].class, tags.get("mods", "[]")); - this.productionRates = productionRates; + this.exportRates = exportRates; - productionRates.each(e -> hasProduction |= e.value > 0.001f); + exportRates.each(e -> hasProduction |= e.value > 0.001f); } } diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index 1ecc35931b..ff20c77a6e 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -39,7 +39,7 @@ public abstract class SaveVersion extends SaveFileReader{ map.get("mapname"), map.getInt("wave"), JsonIO.read(Rules.class, map.get("rules", "{}")), - JsonIO.read(Stats.class, map.get("stats", "{}")).productionRates(), + JsonIO.read(Stats.class, map.get("stats", "{}")).exportRates(), map ); } diff --git a/core/src/mindustry/net/Administration.java b/core/src/mindustry/net/Administration.java index e1ce90f8fe..2610fe26f5 100644 --- a/core/src/mindustry/net/Administration.java +++ b/core/src/mindustry/net/Administration.java @@ -13,7 +13,7 @@ import mindustry.gen.*; import mindustry.type.*; import mindustry.world.*; -import static mindustry.Vars.headless; +import static mindustry.Vars.*; import static mindustry.game.EventType.*; public class Administration{ @@ -24,10 +24,21 @@ public class Administration{ private Array chatFilters = new Array<>(); private Array actionFilters = new Array<>(); private Array subnetBans = new Array<>(); + private IntIntMap lastPlaced = new IntIntMap(); public Administration(){ load(); + Events.on(ResetEvent.class, e -> lastPlaced = new IntIntMap()); + + //keep track of who placed what on the server + Events.on(BlockBuildEndEvent.class, e -> { + //players should be able to configure their own tiles + if(net.server() && e.unit != null && e.unit.isPlayer()){ + lastPlaced.put(e.tile.pos(), e.unit.getPlayer().id()); + } + }); + //anti-spam addChatFilter((player, message) -> { long resetTime = Config.messageRateLimit.num() * 1000; @@ -58,6 +69,31 @@ public class Administration{ return message; }); + + //block interaction rate limit + addActionFilter(action -> { + if(action.type != ActionType.breakBlock && + action.type != ActionType.placeBlock && + action.type != ActionType.tapTile && + Config.antiSpam.bool() && + //make sure players can configure their own stuff, e.g. in schematics + lastPlaced.get(action.tile.pos(), -1) != action.player.id()){ + + Ratekeeper rate = action.player.getInfo().rate; + if(rate.allow(Config.interactRateWindow.num() * 1000, Config.interactRateLimit.num())){ + return true; + }else{ + if(rate.occurences > Config.interactRateKick.num()){ + player.kick("You are interacting with too many blocks.", 1000 * 30); + }else{ + player.sendMessage("[scarlet]You are interacting with blocks too quickly."); + } + + return false; + } + } + return true; + }); } public Array getSubnetBans(){ @@ -420,6 +456,9 @@ public class Administration{ logging("Whether to log everything to files.", true), strict("Whether strict mode is on - corrects positions and prevents duplicate UUIDs.", true), antiSpam("Whether spammers are automatically kicked and rate-limited.", headless), + interactRateWindow("Block interaction rate limit window, in seconds.", 6), + interactRateLimit("Block interaction rate limit.", 25), + interactRateKick("How many times a player must interact inside the window to get kicked.", 60), messageRateLimit("Message rate limit in seconds. 0 to disable.", 0), messageSpamKick("How many times a player must send a message before the cooldown to get kicked. 0 to disable.", 3), socketInput("Allows a local application to control this server through a local TCP socket.", false, "socket", () -> Events.fire(Trigger.socketConfigChanged)), @@ -503,6 +542,7 @@ public class Administration{ public transient long lastMessageTime, lastSyncTime; public transient String lastSentMessage; public transient int messageInfractions; + public transient Ratekeeper rate = new Ratekeeper(); PlayerInfo(String id){ this.id = id; diff --git a/core/src/mindustry/ui/dialogs/PlanetDialog.java b/core/src/mindustry/ui/dialogs/PlanetDialog.java index 0640b569f6..fd1ba15134 100644 --- a/core/src/mindustry/ui/dialogs/PlanetDialog.java +++ b/core/src/mindustry/ui/dialogs/PlanetDialog.java @@ -361,7 +361,7 @@ public class PlanetDialog extends FloatingDialog{ stable.table(t -> { t.left(); - selected.save.meta.productionRates.each(entry -> { + selected.save.meta.exportRates.each(entry -> { int total = (int)(entry.value * turnDuration / 60f); if(total > 1){ t.image(entry.key.icon(Cicon.small)).padRight(3); diff --git a/core/src/mindustry/world/blocks/defense/Door.java b/core/src/mindustry/world/blocks/defense/Door.java index d239576917..54e6aed17c 100644 --- a/core/src/mindustry/world/blocks/defense/Door.java +++ b/core/src/mindustry/world/blocks/defense/Door.java @@ -27,7 +27,8 @@ public class Door extends Wall{ solid = false; solidifes = true; consumesTap = true; - config(Boolean.class, (entity, open) -> { + + config(Boolean.class, (entity, open) -> { DoorEntity door = (DoorEntity)entity; door.open = open; pathfinder.updateTile(door.tile()); diff --git a/core/src/mindustry/world/blocks/storage/LaunchPad.java b/core/src/mindustry/world/blocks/storage/LaunchPad.java index cc7dc31b33..105400727a 100644 --- a/core/src/mindustry/world/blocks/storage/LaunchPad.java +++ b/core/src/mindustry/world/blocks/storage/LaunchPad.java @@ -72,7 +72,7 @@ public class LaunchPad extends Block{ Draw.reset(); } - float cooldown = Mathf.clamp(timer.getTime(timerLaunch) / 90f); + float cooldown = Mathf.clamp(timer.getTime(timerLaunch) / 90f / timeScale); Draw.mixcol(lightColor, 1f - cooldown); @@ -90,7 +90,7 @@ public class LaunchPad extends Block{ public void updateTile(){ //launch when full and base conditions are met - if(items.total() >= itemCapacity && efficiency() >= 1f && timer(timerLaunch, launchTime)){ + if(items.total() >= itemCapacity && efficiency() >= 1f && timer(timerLaunch, launchTime / timeScale)){ LaunchPayloadc entity = LaunchPayloadEntity.create(); items.each((item, amount) -> entity.stacks().add(new ItemStack(item, amount))); entity.set(this);