mirror of
https://github.com/Anuken/Mindustry.git
synced 2025-07-24 22:57:50 +07:00
Bugfixes / Built-in rate limits
This commit is contained in:
@ -1317,10 +1317,10 @@ public class Blocks implements ContentList{
|
|||||||
}};
|
}};
|
||||||
|
|
||||||
launchPad = new LaunchPad("launch-pad"){{
|
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;
|
size = 3;
|
||||||
itemCapacity = 100;
|
itemCapacity = 100;
|
||||||
launchTime = 60*3;//60f * 15;
|
launchTime = 60f * 15;
|
||||||
hasPower = true;
|
hasPower = true;
|
||||||
consumes.power(4f);
|
consumes.power(4f);
|
||||||
}};
|
}};
|
||||||
|
@ -161,7 +161,7 @@ public class Fx{
|
|||||||
|
|
||||||
rocketSmoke = new Effect(120, e -> {
|
rocketSmoke = new Effect(120, e -> {
|
||||||
color(Color.gray);
|
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);
|
Fill.circle(e.x, e.y, (1f + 6f * e.rotation) - e.fin()*2f);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -144,6 +144,10 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
|
|||||||
con.kick(reason);
|
con.kick(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void kick(String reason, int duration){
|
||||||
|
con.kick(reason, duration);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void draw(){
|
public void draw(){
|
||||||
Draw.z(Layer.playerName);
|
Draw.z(Layer.playerName);
|
||||||
|
@ -177,6 +177,10 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
|||||||
return controller instanceof Playerc;
|
return controller instanceof Playerc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Playerc getPlayer(){
|
||||||
|
return isPlayer() ? (Playerc)controller : null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void killed(){
|
public void killed(){
|
||||||
float explosiveness = 2f + item().explosiveness * stack().amount;
|
float explosiveness = 2f + item().explosiveness * stack().amount;
|
||||||
|
@ -243,7 +243,7 @@ public class Saves{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ObjectFloatMap<Item> getProductionRates(){
|
public ObjectFloatMap<Item> getProductionRates(){
|
||||||
return meta.productionRates;
|
return meta.exportRates;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPlayTime(){
|
public String getPlayTime(){
|
||||||
|
@ -4,8 +4,9 @@ import arc.math.*;
|
|||||||
import arc.struct.*;
|
import arc.struct.*;
|
||||||
import arc.util.*;
|
import arc.util.*;
|
||||||
import mindustry.type.*;
|
import mindustry.type.*;
|
||||||
|
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||||
|
|
||||||
import static mindustry.Vars.content;
|
import static mindustry.Vars.*;
|
||||||
|
|
||||||
public class Stats{
|
public class Stats{
|
||||||
/** export window size in seconds */
|
/** export window size in seconds */
|
||||||
@ -13,7 +14,7 @@ public class Stats{
|
|||||||
/** refresh period of export in ticks */
|
/** refresh period of export in ticks */
|
||||||
private static final float refreshPeriod = 60;
|
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<Item> itemsDelivered = new ObjectIntMap<>();
|
public ObjectIntMap<Item> itemsDelivered = new ObjectIntMap<>();
|
||||||
/** Enemy (red team) units destroyed. */
|
/** Enemy (red team) units destroyed. */
|
||||||
public int enemyUnitsDestroyed;
|
public int enemyUnitsDestroyed;
|
||||||
@ -27,42 +28,64 @@ public class Stats{
|
|||||||
public int buildingsDeconstructed;
|
public int buildingsDeconstructed;
|
||||||
/** Friendly buildings destroyed. */
|
/** Friendly buildings destroyed. */
|
||||||
public int buildingsDestroyed;
|
public int buildingsDestroyed;
|
||||||
|
/** Export statistics. */
|
||||||
|
public ObjectMap<Item, ExportStat> export = new ObjectMap<>();
|
||||||
|
|
||||||
/** Counter refresh state. */
|
/** Counter refresh state. */
|
||||||
private transient Interval time = new Interval();
|
private transient Interval time = new Interval();
|
||||||
/** Export statistics. */
|
/** Core item storage to prevent spoofing. */
|
||||||
public ObjectMap<Item, ProductionStat> production = new ObjectMap<>();
|
private transient int[] lastCoreItems;
|
||||||
|
|
||||||
/** Updates export statistics. */
|
/** Updates export statistics. */
|
||||||
public void handleItemExport(ItemStack stack){
|
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){
|
public float getExport(Item item){
|
||||||
return production.getOr(item, ProductionStat::new).mean;
|
return export.getOr(item, ExportStat::new).mean;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update(){
|
public void update(){
|
||||||
|
//create last stored core items
|
||||||
|
if(lastCoreItems == null){
|
||||||
|
lastCoreItems = new int[content.items().size];
|
||||||
|
updateCoreDeltas();
|
||||||
|
}
|
||||||
|
|
||||||
//refresh throughput
|
//refresh throughput
|
||||||
if(time.get(refreshPeriod)){
|
if(time.get(refreshPeriod)){
|
||||||
for(ProductionStat stat : production.values()){
|
CoreEntity ent = state.rules.defaultTeam.core();
|
||||||
|
|
||||||
|
export.each((item, stat) -> {
|
||||||
//initialize stat after loading
|
//initialize stat after loading
|
||||||
if(!stat.loaded){
|
if(!stat.loaded){
|
||||||
stat.means.fill(stat.mean);
|
stat.means.fill(stat.mean);
|
||||||
stat.loaded = true;
|
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.counter = 0;
|
||||||
stat.mean = stat.means.rawMean();
|
stat.mean = stat.means.rawMean();
|
||||||
}
|
});
|
||||||
|
|
||||||
|
updateCoreDeltas();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectFloatMap<Item> 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<Item> exportRates(){
|
||||||
ObjectFloatMap<Item> map = new ObjectFloatMap<>();
|
ObjectFloatMap<Item> map = new ObjectFloatMap<>();
|
||||||
production.each((item, value) -> map.put(item, value.mean));
|
export.each((item, value) -> map.put(item, value.mean));
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,9 +140,9 @@ public class Stats{
|
|||||||
F, D, C, B, A, S, SS
|
F, D, C, B, A, S, SS
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ProductionStat{
|
public static class ExportStat{
|
||||||
public transient float counter;
|
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 transient boolean loaded;
|
||||||
public float mean;
|
public float mean;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import arc.graphics.*;
|
|||||||
import arc.math.*;
|
import arc.math.*;
|
||||||
import arc.struct.*;
|
import arc.struct.*;
|
||||||
import arc.util.*;
|
import arc.util.*;
|
||||||
|
import arc.util.ArcAnnotate.*;
|
||||||
import mindustry.game.Teams.*;
|
import mindustry.game.Teams.*;
|
||||||
import mindustry.graphics.*;
|
import mindustry.graphics.*;
|
||||||
import mindustry.world.blocks.storage.CoreBlock.*;
|
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||||
@ -72,7 +73,7 @@ public class Team implements Comparable<Team>{
|
|||||||
return state.teams.get(this);
|
return state.teams.get(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CoreEntity core(){
|
public @Nullable CoreEntity core(){
|
||||||
return data().core();
|
return data().core();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,8 +165,8 @@ public class Teams{
|
|||||||
return cores.isEmpty();
|
return cores.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CoreEntity core(){
|
public @Nullable CoreEntity core(){
|
||||||
return cores.first();
|
return cores.isEmpty() ? null : cores.first();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -76,10 +76,11 @@ public class Universe{
|
|||||||
//calculate passive items
|
//calculate passive items
|
||||||
for(Planet planet : content.planets()){
|
for(Planet planet : content.planets()){
|
||||||
for(Sector sector : planet.sectors){
|
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;
|
SaveMeta meta = sector.save.meta;
|
||||||
|
|
||||||
for(Entry<Item> entry : meta.productionRates){
|
for(Entry<Item> entry : meta.exportRates){
|
||||||
//total is calculated by items/sec (value) * turn duration in seconds
|
//total is calculated by items/sec (value) * turn duration in seconds
|
||||||
int total = (int)(entry.value * turnDuration / 60f);
|
int total = (int)(entry.value * turnDuration / 60f);
|
||||||
|
|
||||||
|
@ -18,10 +18,10 @@ public class SaveMeta{
|
|||||||
public StringMap tags;
|
public StringMap tags;
|
||||||
public String[] mods;
|
public String[] mods;
|
||||||
/** These are in items/second. */
|
/** These are in items/second. */
|
||||||
public ObjectFloatMap<Item> productionRates;
|
public ObjectFloatMap<Item> exportRates;
|
||||||
public boolean hasProduction;
|
public boolean hasProduction;
|
||||||
|
|
||||||
public SaveMeta(int version, long timestamp, long timePlayed, int build, String map, int wave, Rules rules, ObjectFloatMap<Item> productionRates, StringMap tags){
|
public SaveMeta(int version, long timestamp, long timePlayed, int build, String map, int wave, Rules rules, ObjectFloatMap<Item> exportRates, StringMap tags){
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.build = build;
|
this.build = build;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
@ -31,8 +31,8 @@ public class SaveMeta{
|
|||||||
this.rules = rules;
|
this.rules = rules;
|
||||||
this.tags = tags;
|
this.tags = tags;
|
||||||
this.mods = JsonIO.read(String[].class, tags.get("mods", "[]"));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ public abstract class SaveVersion extends SaveFileReader{
|
|||||||
map.get("mapname"),
|
map.get("mapname"),
|
||||||
map.getInt("wave"),
|
map.getInt("wave"),
|
||||||
JsonIO.read(Rules.class, map.get("rules", "{}")),
|
JsonIO.read(Rules.class, map.get("rules", "{}")),
|
||||||
JsonIO.read(Stats.class, map.get("stats", "{}")).productionRates(),
|
JsonIO.read(Stats.class, map.get("stats", "{}")).exportRates(),
|
||||||
map
|
map
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import mindustry.gen.*;
|
|||||||
import mindustry.type.*;
|
import mindustry.type.*;
|
||||||
import mindustry.world.*;
|
import mindustry.world.*;
|
||||||
|
|
||||||
import static mindustry.Vars.headless;
|
import static mindustry.Vars.*;
|
||||||
import static mindustry.game.EventType.*;
|
import static mindustry.game.EventType.*;
|
||||||
|
|
||||||
public class Administration{
|
public class Administration{
|
||||||
@ -24,10 +24,21 @@ public class Administration{
|
|||||||
private Array<ChatFilter> chatFilters = new Array<>();
|
private Array<ChatFilter> chatFilters = new Array<>();
|
||||||
private Array<ActionFilter> actionFilters = new Array<>();
|
private Array<ActionFilter> actionFilters = new Array<>();
|
||||||
private Array<String> subnetBans = new Array<>();
|
private Array<String> subnetBans = new Array<>();
|
||||||
|
private IntIntMap lastPlaced = new IntIntMap();
|
||||||
|
|
||||||
public Administration(){
|
public Administration(){
|
||||||
load();
|
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
|
//anti-spam
|
||||||
addChatFilter((player, message) -> {
|
addChatFilter((player, message) -> {
|
||||||
long resetTime = Config.messageRateLimit.num() * 1000;
|
long resetTime = Config.messageRateLimit.num() * 1000;
|
||||||
@ -58,6 +69,31 @@ public class Administration{
|
|||||||
|
|
||||||
return message;
|
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<String> getSubnetBans(){
|
public Array<String> getSubnetBans(){
|
||||||
@ -420,6 +456,9 @@ public class Administration{
|
|||||||
logging("Whether to log everything to files.", true),
|
logging("Whether to log everything to files.", true),
|
||||||
strict("Whether strict mode is on - corrects positions and prevents duplicate UUIDs.", true),
|
strict("Whether strict mode is on - corrects positions and prevents duplicate UUIDs.", true),
|
||||||
antiSpam("Whether spammers are automatically kicked and rate-limited.", headless),
|
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),
|
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),
|
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)),
|
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 long lastMessageTime, lastSyncTime;
|
||||||
public transient String lastSentMessage;
|
public transient String lastSentMessage;
|
||||||
public transient int messageInfractions;
|
public transient int messageInfractions;
|
||||||
|
public transient Ratekeeper rate = new Ratekeeper();
|
||||||
|
|
||||||
PlayerInfo(String id){
|
PlayerInfo(String id){
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
@ -361,7 +361,7 @@ public class PlanetDialog extends FloatingDialog{
|
|||||||
stable.table(t -> {
|
stable.table(t -> {
|
||||||
t.left();
|
t.left();
|
||||||
|
|
||||||
selected.save.meta.productionRates.each(entry -> {
|
selected.save.meta.exportRates.each(entry -> {
|
||||||
int total = (int)(entry.value * turnDuration / 60f);
|
int total = (int)(entry.value * turnDuration / 60f);
|
||||||
if(total > 1){
|
if(total > 1){
|
||||||
t.image(entry.key.icon(Cicon.small)).padRight(3);
|
t.image(entry.key.icon(Cicon.small)).padRight(3);
|
||||||
|
@ -27,7 +27,8 @@ public class Door extends Wall{
|
|||||||
solid = false;
|
solid = false;
|
||||||
solidifes = true;
|
solidifes = true;
|
||||||
consumesTap = true;
|
consumesTap = true;
|
||||||
config(Boolean.class, (entity, open) -> {
|
|
||||||
|
config(Boolean.class, (entity, open) -> {
|
||||||
DoorEntity door = (DoorEntity)entity;
|
DoorEntity door = (DoorEntity)entity;
|
||||||
door.open = open;
|
door.open = open;
|
||||||
pathfinder.updateTile(door.tile());
|
pathfinder.updateTile(door.tile());
|
||||||
|
@ -72,7 +72,7 @@ public class LaunchPad extends Block{
|
|||||||
Draw.reset();
|
Draw.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
float cooldown = Mathf.clamp(timer.getTime(timerLaunch) / 90f);
|
float cooldown = Mathf.clamp(timer.getTime(timerLaunch) / 90f / timeScale);
|
||||||
|
|
||||||
Draw.mixcol(lightColor, 1f - cooldown);
|
Draw.mixcol(lightColor, 1f - cooldown);
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ public class LaunchPad extends Block{
|
|||||||
public void updateTile(){
|
public void updateTile(){
|
||||||
|
|
||||||
//launch when full and base conditions are met
|
//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();
|
LaunchPayloadc entity = LaunchPayloadEntity.create();
|
||||||
items.each((item, amount) -> entity.stacks().add(new ItemStack(item, amount)));
|
items.each((item, amount) -> entity.stacks().add(new ItemStack(item, amount)));
|
||||||
entity.set(this);
|
entity.set(this);
|
||||||
|
Reference in New Issue
Block a user