soundMap = new ObjectMap<>();
- private Saves saves;
-
- private float respawntime;
- private InputHandler input;
-
- private InputProxy proxy;
- private float controlx, controly;
- private boolean controlling;
private Throwable error;
+ private Input gdxInput;
- public Control(){
- saves = new Saves();
+ public Control(){
- Inputs.useControllers(!gwt);
+ saves = new Saves();
+ db = new ContentDatabase();
- Gdx.input.setCatchBackKey(true);
+ Inputs.useControllers(!gwt);
- if(mobile){
- input = new AndroidInput();
- }else{
- input = new DesktopInput();
- }
+ Gdx.input.setCatchBackKey(true);
- proxy = new InputProxy(Gdx.input){
- @Override
- public int getY() {
- return controlling ? (int)controly : input.getY();
+ Effects.setShakeFalloff(10000f);
+
+ ContentLoader.initialize(Content::init);
+ Core.atlas = new Atlas("sprites.atlas");
+ Core.atlas.setErrorRegion("error");
+ ContentLoader.initialize(Content::load);
+
+ db.load();
+
+ gdxInput = Gdx.input;
+
+ Sounds.load("shoot.mp3", "place.mp3", "explosion.mp3", "enemyshoot.mp3",
+ "corexplode.mp3", "break.mp3", "spawn.mp3", "flame.mp3", "die.mp3",
+ "respawn.mp3", "purchase.mp3", "flame2.mp3", "bigshot.mp3", "laser.mp3", "lasershot.mp3",
+ "ping.mp3", "tesla.mp3", "waveend.mp3", "railgun.mp3", "blast.mp3", "bang2.mp3");
+
+ Sounds.setFalloff(9000f);
+ Sounds.setPlayer((sound, volume) -> {
+ long time = TimeUtils.millis();
+ long value = soundMap.get(sound, 0L);
+
+ if(TimeUtils.timeSinceMillis(value) >= minSoundPeriod){
+ threads.run(() -> sound.play(volume));
+ soundMap.put(sound, time);
}
-
- @Override
- public int getX() {
- return controlling ? (int)controlx : input.getX();
- }
-
- @Override
- public int getY(int pointer) {
- return pointer == 0 ? getY() : super.getY(pointer);
- }
-
- @Override
- public int getX(int pointer) {
- return pointer == 0 ? getX() : super.getX(pointer);
- }
- };
-
- Inputs.addProcessor(input);
-
- Effects.setShakeFalloff(10000f);
-
- Core.atlas = new Atlas("sprites.atlas");
-
- for(Item item : Item.getAllItems()){
- item.init();
- }
-
- Sounds.load("shoot.mp3", "place.mp3", "explosion.mp3", "enemyshoot.mp3",
- "corexplode.mp3", "break.mp3", "spawn.mp3", "flame.mp3", "die.mp3",
- "respawn.mp3", "purchase.mp3", "flame2.mp3", "bigshot.mp3", "laser.mp3", "lasershot.mp3",
- "ping.mp3", "tesla.mp3", "waveend.mp3", "railgun.mp3", "blast.mp3", "bang2.mp3");
-
- Sounds.setFalloff(9000f);
+ });
Musics.load("1.mp3", "2.mp3", "3.mp3", "4.mp3", "5.mp3", "6.mp3");
DefaultKeybinds.load();
- for(int i = 0; i < saveSlots; i ++){
- Settings.defaults("save-" + i + "-autosave", !gwt);
- Settings.defaults("save-" + i + "-name", "untitled");
- Settings.defaults("save-" + i + "-data", "empty");
- }
+ Settings.defaultList(
+ "ip", "localhost",
+ "port", port + "",
+ "color-0", Color.rgba8888(playerColors[8]),
+ "color-1", Color.rgba8888(playerColors[11]),
+ "color-2", Color.rgba8888(playerColors[13]),
+ "color-3", Color.rgba8888(playerColors[9]),
+ "name", "player",
+ "lastBuild", 0
+ );
- Settings.defaultList(
- "ip", "localhost",
- "port", port+"",
- "name", mobile || gwt ? "player" : UCore.getProperty("user.name"),
- "servers", "",
- "color", Color.rgba8888(playerColors[8]),
- "lastVersion", "3.2",
- "lastBuild", 0
- );
+ KeyBinds.load();
- KeyBinds.load();
+ addPlayer(0);
- for(Map map : world.maps().list()){
- Settings.defaults("hiscore" + map.name, 0);
- }
+ saves.load();
- player = new Player();
- player.name = Settings.getString("name");
- player.isAndroid = mobile;
- player.color.set(Settings.getInt("color"));
- player.isLocal = true;
+ Events.on(StateChangeEvent.class, (from, to) -> {
+ if((from == State.playing && to == State.menu) || (from == State.menu && to != State.menu)){
+ Timers.runTask(5f, Platform.instance::updateRPC);
+ }
+ });
- saves.load();
+ Events.on(PlayEvent.class, () -> {
+ for(Player player : players){
+ player.add();
+ }
- Events.on(StateChangeEvent.class, (from, to) -> {
- if((from == State.playing && to == State.menu) || (from == State.menu && to != State.menu)){
- Timers.runTask(5f, Platform.instance::updateRPC);
- }
- });
-
- Events.on(PlayEvent.class, () -> {
- renderer.clearTiles();
-
- player.set(world.getSpawnX(), world.getSpawnY());
-
- Core.camera.position.set(player.x, player.y, 0);
-
- ui.hudfrag.updateItems();
-
- state.set(State.playing);
- });
-
- Events.on(ResetEvent.class, () -> {
- upgrades.reset();
- player.weaponLeft = player.weaponRight = Weapon.blaster;
-
- player.add();
- player.heal();
-
- respawntime = -1;
- hiscore = false;
-
- ui.hudfrag.updateItems();
- ui.hudfrag.updateWeapons();
- ui.hudfrag.fadeRespawn(false);
- });
-
- Events.on(WaveEvent.class, () -> {
- Sounds.play("spawn");
-
- int last = Settings.getInt("hiscore" + world.getMap().name, 0);
-
- if(state.wave > last && !state.mode.infiniteResources && !state.mode.disableWaveTimer){
- Settings.putInt("hiscore" + world.getMap().name, state.wave);
- Settings.save();
- hiscore = true;
- }
-
- Platform.instance.updateRPC();
- });
-
- Events.on(GameOverEvent.class, () -> {
- Effects.shake(5, 6, Core.camera.position.x, Core.camera.position.y);
- Sounds.play("corexplode");
- for(int i = 0; i < 16; i ++){
- Timers.run(i*2, ()-> Effects.effect(Fx.explosion, world.getCore().worldx()+Mathf.range(40), world.getCore().worldy()+Mathf.range(40)));
- }
- Effects.effect(Fx.coreexplosion, world.getCore().worldx(), world.getCore().worldy());
-
- ui.restart.show();
-
- Timers.runTask(30f, () -> state.set(State.menu));
- });
- }
-
- //FIXME figure out what's causing this problem in the first place
- public void triggerInputUpdate(){
- Gdx.input = proxy;
- }
-
- public void setError(Throwable error){
- this.error = error;
- }
-
- public UpgradeInventory upgrades() {
- return upgrades;
- }
-
- public Saves getSaves(){
- return saves;
- }
-
- public boolean showCursor(){
- return controlling;
- }
-
- public InputHandler input(){
- return input;
- }
-
- public void playMap(Map map){
- ui.loadfrag.show();
- saves.resetSave();
-
- Timers.runTask(10, () -> {
- logic.reset();
- world.loadMap(map);
- logic.play();
- });
-
- Timers.runTask(18, () -> ui.loadfrag.hide());
- }
-
- public boolean isHighScore(){
- return hiscore;
- }
-
- public float getRespawnTime(){
- return respawntime;
- }
-
- public void setRespawnTime(float respawntime){
- this.respawntime = respawntime;
- }
-
- public Tutorial tutorial(){
- return tutorial;
- }
-
- private void checkOldUser(){
- boolean hasPlayed = false;
-
- for(Map map : world.maps().getAllMaps()){
- if(Settings.getInt("hiscore" + map.name) != 0){
- hasPlayed = true;
- break;
- }
- }
-
- if(hasPlayed && Settings.getString("lastVersion").equals("3.2")){
- Timers.runTask(1f, () -> ui.showInfo("$text.changes"));
- Settings.putString("lastVersion", "3.3");
- Settings.save();
- }
- }
-
- @Override
- public void dispose(){
- Platform.instance.onGameExit();
- Net.dispose();
- }
-
- @Override
- public void pause(){
- wasPaused = state.is(State.paused);
- if(state.is(State.playing)) state.set(State.paused);
- }
-
- @Override
- public void resume(){
- if(state.is(State.paused) && !wasPaused){
state.set(State.playing);
- }
- }
+ });
- @Override
- public void init(){
- Timers.run(1f, Musics::shuffleAll);
+ Events.on(WorldLoadGraphicsEvent.class, () -> {
+ if(mobile){
+ Core.camera.position.set(players[0].x, players[0].y, 0);
+ }
+ });
- Entities.initPhysics();
- Entities.collisions().setCollider(tilesize, world::solid);
-
- Platform.instance.updateRPC();
-
- checkOldUser();
- }
-
- @Override
- public void update(){
-
- if(error != null){
- throw new RuntimeException(error);
- }
-
- Gdx.input = proxy;
-
- if(Inputs.keyTap("console")){
- console = !console;
- }
-
- if(KeyBinds.getSection("default").device.type == DeviceType.controller){
- if(Inputs.keyTap("select")){
- Inputs.getProcessor().touchDown(Gdx.input.getX(), Gdx.input.getY(), 0, Buttons.LEFT);
+ Events.on(ResetEvent.class, () -> {
+ for(Player player : players){
+ player.reset();
}
- if(Inputs.keyRelease("select")){
- Inputs.getProcessor().touchUp(Gdx.input.getX(), Gdx.input.getY(), 0, Buttons.LEFT);
+ hiscore = false;
+
+ saves.resetSave();
+ });
+
+ Events.on(WaveEvent.class, () -> {
+
+ int last = Settings.getInt("hiscore" + world.getMap().name, 0);
+
+ if(state.wave > last && !state.mode.infiniteResources && !state.mode.disableWaveTimer){
+ Settings.putInt("hiscore" + world.getMap().name, state.wave);
+ Settings.save();
+ hiscore = true;
}
- float xa = Inputs.getAxis("cursor_x");
- float ya = Inputs.getAxis("cursor_y");
+ Platform.instance.updateRPC();
+ });
- if(Math.abs(xa) > controllerMin || Math.abs(ya) > controllerMin) {
- float scl = Settings.getInt("sensitivity")/100f * Unit.dp.scl(1f);
- controlx += xa*baseControllerSpeed*scl;
- controly -= ya*baseControllerSpeed*scl;
- controlling = true;
+ Events.on(GameOverEvent.class, () -> {
+ Effects.shake(5, 6, Core.camera.position.x, Core.camera.position.y);
- Gdx.input.setCursorCatched(true);
+ //TODO game over effect
+ ui.restart.show();
- Inputs.getProcessor().touchDragged(Gdx.input.getX(), Gdx.input.getY(), 0);
- }
+ Timers.runTask(30f, () -> state.set(State.menu));
+ });
- controlx = Mathf.clamp(controlx, 0, Gdx.graphics.getWidth());
- controly = Mathf.clamp(controly, 0, Gdx.graphics.getHeight());
+ Events.on(WorldLoadEvent.class, () -> threads.runGraphics(() -> Events.fire(WorldLoadGraphicsEvent.class)));
+ }
- if(Gdx.input.getDeltaX() > 1 || Gdx.input.getDeltaY() > 1) {
- controlling = false;
- Gdx.input.setCursorCatched(false);
- }
- }else{
- controlling = false;
- Gdx.input.setCursorCatched(false);
+ public void addPlayer(int index){
+ if(players.length != index + 1){
+ Player[] old = players;
+ players = new Player[index + 1];
+ System.arraycopy(old, 0, players, 0, old.length);
}
- if(!controlling){
- controlx = Gdx.input.getX();
- controly = Gdx.input.getY();
+ if(inputs.length != index + 1){
+ InputHandler[] oldi = inputs;
+ inputs = new InputHandler[index + 1];
+ System.arraycopy(oldi, 0, inputs, 0, oldi.length);
+ }
+
+ Player setTo = (index == 0 ? null : players[0]);
+
+ Player player = new Player();
+ player.name = Settings.getString("name");
+ player.mech = mobile ? Mechs.starterMobile : Mechs.starterDesktop;
+ player.color.set(Settings.getInt("color-" + index));
+ player.isLocal = true;
+ player.playerIndex = index;
+ player.isMobile = mobile;
+ players[index] = player;
+
+ if(setTo != null){
+ player.set(setTo.x, setTo.y);
+ }
+
+ if(!state.is(State.menu)){
+ player.add();
+ }
+
+ InputHandler input;
+
+ if(mobile){
+ input = new MobileInput(player);
+ }else{
+ input = new DesktopInput(player);
+ }
+
+ inputs[index] = input;
+ Inputs.addProcessor(input);
+ }
+
+ public void removePlayer(){
+ players[players.length - 1].remove();
+ inputs[inputs.length - 1].remove();
+
+ Player[] old = players;
+ players = new Player[players.length - 1];
+ System.arraycopy(old, 0, players, 0, players.length);
+
+ InputHandler[] oldi = inputs;
+ inputs = new InputHandler[inputs.length - 1];
+ System.arraycopy(oldi, 0, inputs, 0, inputs.length);
+ }
+
+ public ContentDatabase database(){
+ return db;
+ }
+
+ public Input gdxInput(){
+ return gdxInput;
+ }
+
+ public void setError(Throwable error){
+ this.error = error;
+ }
+
+ public Saves getSaves(){
+ return saves;
+ }
+
+ public InputHandler input(int index){
+ return inputs[index];
+ }
+
+ public void triggerUpdateInput(){
+ //Gdx.input = proxy;
+ }
+
+ public void playMap(Map map){
+ ui.loadfrag.show();
+
+ Timers.run(5f, () ->
+ threads.run(() -> {
+ logic.reset();
+ world.loadMap(map);
+ logic.play();
+
+ Gdx.app.postRunnable(ui.loadfrag::hide);
+ }));
+ }
+
+ public boolean isHighScore(){
+ return hiscore;
+ }
+
+ private void checkUnlockableBlocks(){
+ TileEntity entity = players[0].getClosestCore();
+
+ if(entity == null) return;
+
+ entity.items.forEach((item, amount) -> control.database().unlockContent(item));
+
+ if(players[0].inventory.hasItem()){
+ control.database().unlockContent(players[0].inventory.getItem().item);
+ }
+
+ for(int i = 0; i < Recipe.all().size; i++){
+ Recipe recipe = Recipe.all().get(i);
+ if(!recipe.debugOnly && entity.items.has(recipe.requirements, 1.4f)){
+ if(control.database().unlockContent(recipe)){
+ ui.hudfrag.showUnlock(recipe);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void dispose(){
+ Platform.instance.onGameExit();
+ ContentLoader.dispose();
+ Net.dispose();
+ ui.editor.dispose();
+ inputs = new InputHandler[]{};
+ players = new Player[]{};
+ }
+
+ @Override
+ public void pause(){
+ wasPaused = state.is(State.paused);
+ if(state.is(State.playing)) state.set(State.paused);
+ }
+
+ @Override
+ public void resume(){
+ if(state.is(State.paused) && !wasPaused){
+ state.set(State.playing);
+ }
+ }
+
+ @Override
+ public void init(){
+ EntityPhysics.initPhysics();
+
+ Platform.instance.updateRPC();
+
+ if(!Settings.has("4.0-warning")){
+ Settings.putBool("4.0-warning", true);
+
+ Timers.run(5f, () -> {
+ FloatingDialog dialog = new FloatingDialog("[orange]WARNING![]");
+ dialog.buttons().addButton("$text.ok", dialog::hide).size(100f, 60f);
+ dialog.content().add("The beta version you are about to play should be considered very unstable, and is [accent]not representative of the final 4.0 release.[]\n\n " +
+ "A large portion of content is still unimplemented. \nAll current art and UI is temporary, and will be re-drawn before release. " +
+ "\n\n[accent]Saves and maps may be corrupted without warning between updates.[] You have been warned!").wrap().width(500f);
+ dialog.show();
+
+ });
+ }
+
+ if(!Settings.has("4.0-no-sound")){
+ Settings.putBool("4.0-no-sound", true);
+
+ Timers.run(4f, () -> {
+ FloatingDialog dialog = new FloatingDialog("[orange]Attention![]");
+ dialog.buttons().addButton("$text.ok", dialog::hide).size(100f, 60f);
+ dialog.content().add("You might have noticed that 4.0 does not have any sound.\nThis is [orange]intentional![] Sound will be added in a later update.\n\n[LIGHT_GRAY](now stop reporting this as a bug)").wrap().width(500f);
+ dialog.show();
+
+ });
+ }
+ }
+
+ /** Called from main logic thread.*/
+ public void runUpdateLogic(){
+ if(!state.is(State.menu)){
+ renderer.minimap().updateUnitArray();
+ }
+ }
+
+ @Override
+ public void update(){
+
+ if(error != null){
+ throw new RuntimeException(error);
+ }
+
+ if(Inputs.keyTap("console")){
+ console = !console;
}
saves.update();
- if(state.inventory.isUpdated() && (Timers.get("updateItems", 8) || state.is(State.paused))){
- ui.hudfrag.updateItems();
- state.inventory.setUpdated(false);
- }
+ triggerUpdateInput();
- if(!state.is(State.menu)){
- input.update();
+ for(InputHandler inputHandler : inputs){
+ inputHandler.updateController();
+ }
- if(Inputs.keyTap("pause") && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
+ if(!state.is(State.menu)){
+ for(InputHandler input : inputs){
+ input.update();
+ }
+
+ //check unlocks every 2 seconds
+ if(!state.mode.infiniteResources && Timers.get("timerCheckUnlock", 120)){
+ checkUnlockableBlocks();
+
+ //save if the db changed, but don't save unlocks
+ if(db.isDirty() && !debug){
+ db.save();
+ }
+ }
+
+ if(Inputs.keyTap("pause") && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
state.set(state.is(State.playing) ? State.paused : State.playing);
- }
+ }
- if(Inputs.keyTap("menu")){
- if(state.is(State.paused)){
- ui.paused.hide();
+ if(Inputs.keyTap("menu")){
+ if(state.is(State.paused)){
+ ui.paused.hide();
state.set(State.playing);
- }else if (!ui.restart.isShown()){
- if(ui.chatfrag.chatOpen()) {
- ui.chatfrag.hide();
- }else{
- ui.paused.show();
+ }else if(!ui.restart.isShown()){
+ if(ui.chatfrag.chatOpen()){
+ ui.chatfrag.hide();
+ }else{
+ ui.paused.show();
state.set(State.paused);
- }
- }
- }
+ }
+ }
+ }
- if(!state.is(State.paused) || Net.active()){
- Entities.update(effectGroup);
-
- if(respawntime > 0){
-
- respawntime -= Timers.delta();
-
- if(respawntime <= 0){
- player.set(world.getSpawnX(), world.getSpawnY());
- player.heal();
- player.add();
- Effects.sound("respawn");
- ui.hudfrag.fadeRespawn(false);
- }
- }
-
- if(tutorial.active()){
- tutorial.update();
- }
- }
- }else{
- if(!state.is(State.paused) || Net.active()){
- Timers.update();
- }
- }
- }
+ if(!state.is(State.paused) || Net.active()){
+ Entities.update(effectGroup);
+ Entities.update(groundEffectGroup);
+ }
+ }else{
+ if(!state.is(State.paused) || Net.active()){
+ Timers.update();
+ }
+ }
+ }
}
diff --git a/core/src/io/anuke/mindustry/core/GameState.java b/core/src/io/anuke/mindustry/core/GameState.java
index 20acc304e3..92eee9a4d3 100644
--- a/core/src/io/anuke/mindustry/core/GameState.java
+++ b/core/src/io/anuke/mindustry/core/GameState.java
@@ -1,40 +1,37 @@
package io.anuke.mindustry.core;
+import io.anuke.mindustry.ai.WaveSpawner;
import io.anuke.mindustry.game.Difficulty;
import io.anuke.mindustry.game.EventType.StateChangeEvent;
import io.anuke.mindustry.game.GameMode;
-import io.anuke.mindustry.game.Inventory;
+import io.anuke.mindustry.game.TeamInfo;
import io.anuke.ucore.core.Events;
public class GameState{
- private State state = State.menu;
+ public int wave = 1;
+ public float wavetime;
+ public boolean gameOver = false;
+ public GameMode mode = GameMode.waves;
+ public Difficulty difficulty = Difficulty.normal;
+ public boolean friendlyFire;
+ public WaveSpawner spawner = new WaveSpawner();
+ public TeamInfo teams = new TeamInfo();
+ private State state = State.menu;
- public final Inventory inventory = new Inventory();
+ public void set(State astate){
+ Events.fire(StateChangeEvent.class, state, astate);
+ state = astate;
+ }
- public int wave = 1;
- public int lastUpdated = -1;
- public float wavetime;
- public float extrawavetime;
- public int enemies = 0;
- public boolean gameOver = false;
- public GameMode mode = GameMode.waves;
- public Difficulty difficulty = Difficulty.normal;
- public boolean friendlyFire;
-
- public void set(State astate){
- Events.fire(StateChangeEvent.class, state, astate);
- state = astate;
- }
-
- public boolean is(State astate){
- return state == astate;
- }
+ public boolean is(State astate){
+ return state == astate;
+ }
- public State getState(){
- return state;
- }
-
- public enum State{
- paused, playing, menu
- }
+ public State getState(){
+ return state;
+ }
+
+ public enum State{
+ paused, playing, menu
+ }
}
diff --git a/core/src/io/anuke/mindustry/core/Logic.java b/core/src/io/anuke/mindustry/core/Logic.java
index cb4f484c73..2e07b0a2e6 100644
--- a/core/src/io/anuke/mindustry/core/Logic.java
+++ b/core/src/io/anuke/mindustry/core/Logic.java
@@ -1,160 +1,173 @@
package io.anuke.mindustry.core;
-import com.badlogic.gdx.utils.Array;
+import io.anuke.mindustry.Vars;
+import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.core.GameState.State;
-import io.anuke.mindustry.entities.enemies.Enemy;
-import io.anuke.mindustry.game.EnemySpawn;
+import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.game.EventType.GameOverEvent;
import io.anuke.mindustry.game.EventType.PlayEvent;
import io.anuke.mindustry.game.EventType.ResetEvent;
import io.anuke.mindustry.game.EventType.WaveEvent;
-import io.anuke.mindustry.game.SpawnPoint;
-import io.anuke.mindustry.game.WaveCreator;
-import io.anuke.mindustry.graphics.Fx;
+import io.anuke.mindustry.game.Team;
+import io.anuke.mindustry.game.TeamInfo;
+import io.anuke.mindustry.game.TeamInfo.TeamData;
import io.anuke.mindustry.net.Net;
-import io.anuke.mindustry.net.NetEvents;
+import io.anuke.mindustry.type.Item;
+import io.anuke.mindustry.type.ItemType;
import io.anuke.mindustry.world.Tile;
-import io.anuke.mindustry.world.blocks.ProductionBlocks;
-import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Events;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.Entities;
+import io.anuke.ucore.entities.EntityGroup;
+import io.anuke.ucore.entities.EntityPhysics;
import io.anuke.ucore.modules.Module;
-import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.*;
-/**Logic module.
+/**
+ * Logic module.
* Handles all logic for entities and waves.
* Handles game state events.
* Does not store any game state itself.
- *
+ *
* This class should not call any outside methods to change state of modules, but instead fire events.
*/
-public class Logic extends Module {
- private final Array spawns = WaveCreator.getSpawns();
+public class Logic extends Module{
+ public boolean doUpdate = true;
+
+ public Logic(){
+ state = new GameState();
+ }
@Override
public void init(){
- Entities.initPhysics();
- Entities.collisions().setCollider(tilesize, world::solid);
+ EntityPhysics.initPhysics();
+ EntityPhysics.collisions().setCollider(tilesize, world::solid);
}
public void play(){
+ state.set(State.playing);
state.wavetime = wavespace * state.difficulty.timeScaling * 2;
- if(state.mode.infiniteResources){
- state.inventory.fill();
+ //fill inventory with items for debugging
+
+ for(TeamData team : state.teams.getTeams()){
+ for(Tile tile : team.cores){
+ if(debug){
+ for(Item item : Item.all()){
+ if(item.type == ItemType.material){
+ tile.entity.items.add(item, 1000);
+ }
+ }
+ }else{
+ tile.entity.items.add(Items.tungsten, 50);
+ tile.entity.items.add(Items.lead, 20);
+ }
+ }
}
+
Events.fire(PlayEvent.class);
}
public void reset(){
state.wave = 1;
- state.extrawavetime = maxwavespace * state.difficulty.maxTimeScaling;
state.wavetime = wavespace * state.difficulty.timeScaling;
- state.enemies = 0;
- state.lastUpdated = -1;
state.gameOver = false;
- state.inventory.clearItems();
+ state.teams = new TeamInfo();
+ state.teams.add(Team.blue, true);
+ state.teams.add(Team.red, false);
Timers.clear();
Entities.clear();
+ TileEntity.sleepingEntities = 0;
Events.fire(ResetEvent.class);
}
public void runWave(){
-
- if(state.lastUpdated < state.wave + 1){
- world.pathfinder().resetPaths();
- state.lastUpdated = state.wave + 1;
- }
-
- for(EnemySpawn spawn : spawns){
- Array spawns = world.getSpawns();
-
- for(int lane = 0; lane < spawns.size; lane ++){
- int fl = lane;
- Tile tile = spawns.get(lane).start;
- int spawnamount = spawn.evaluate(state.wave, lane);
-
- for(int i = 0; i < spawnamount; i ++){
- float range = 12f;
-
- Timers.runTask(i*5f, () -> {
-
- Enemy enemy = new Enemy(spawn.type);
- enemy.set(tile.worldx() + Mathf.range(range), tile.worldy() + Mathf.range(range));
- enemy.lane = fl;
- enemy.tier = spawn.tier(state.wave, fl);
- enemy.add();
-
- Effects.effect(Fx.spawn, enemy);
-
- state.enemies ++;
- });
- }
- }
- }
-
- state.wave ++;
+ state.spawner.spawnEnemies();
+ state.wave++;
state.wavetime = wavespace * state.difficulty.timeScaling;
- state.extrawavetime = maxwavespace * state.difficulty.maxTimeScaling;
Events.fire(WaveEvent.class);
}
+ private void checkGameOver(){
+ boolean gameOver = true;
+
+ for(TeamData data : state.teams.getTeams(true)){
+ if(data.cores.size > 0){
+ gameOver = false;
+ break;
+ }
+ }
+
+ if(gameOver && !state.gameOver){
+ state.gameOver = true;
+ Events.fire(GameOverEvent.class);
+ }
+ }
+
@Override
public void update(){
+ if(threads.isEnabled() && !threads.isOnThread()) return;
+
+ if(Vars.control != null){
+ control.runUpdateLogic();
+ }
if(!state.is(State.menu)){
- if(control != null) control.triggerInputUpdate();
+ if(control != null) control.triggerUpdateInput();
if(!state.is(State.paused) || Net.active()){
Timers.update();
}
- if(!Net.client())
- world.pathfinder().update();
-
- if(world.getCore() != null && world.getCore().block() != ProductionBlocks.core && !state.gameOver){
- state.gameOver = true;
- if(Net.server()) NetEvents.handleGameOver();
- Events.fire(GameOverEvent.class);
+ if(!world.isInvalidMap()){
+ checkGameOver();
}
if(!state.is(State.paused) || Net.active()){
if(!state.mode.disableWaveTimer){
-
- if(state.enemies <= 0){
- if(!world.getMap().name.equals("tutorial")) state.wavetime -= Timers.delta();
-
- if(state.lastUpdated < state.wave + 1 && state.wavetime < aheadPathfinding){ //start updating beforehand
- world.pathfinder().resetPaths();
- state.lastUpdated = state.wave + 1;
- }
- }else if(!world.getMap().name.equals("tutorial")){
- state.extrawavetime -= Timers.delta();
- }
+ state.wavetime -= Timers.delta();
}
- if(!Net.client() && (state.wavetime <= 0 || state.extrawavetime <= 0)){
+ if(!Net.client() && state.wavetime <= 0){
runWave();
}
- Entities.update(Entities.defaultGroup());
+ if(!Entities.defaultGroup().isEmpty())
+ throw new RuntimeException("Do not add anything to the default group!");
+
Entities.update(bulletGroup);
- Entities.update(enemyGroup);
+ for(EntityGroup group : unitGroups){
+ Entities.update(group);
+ }
+ Entities.update(puddleGroup);
Entities.update(tileGroup);
+ Entities.update(fireGroup);
Entities.update(shieldGroup);
Entities.update(playerGroup);
+ Entities.update(itemGroup);
- Entities.collideGroups(bulletGroup, enemyGroup);
- Entities.collideGroups(bulletGroup, playerGroup);
+ //effect group only contains item drops in the headless version, update it!
+ if(headless){
+ Entities.update(effectGroup);
+ }
+
+ for(EntityGroup group : unitGroups){
+ if(!group.isEmpty()){
+ EntityPhysics.collideGroups(bulletGroup, group);
+ }
+ }
+
+ EntityPhysics.collideGroups(bulletGroup, playerGroup);
+ EntityPhysics.collideGroups(itemGroup, playerGroup);
+
+ world.pathfinder().update();
}
}
}
diff --git a/core/src/io/anuke/mindustry/core/NetClient.java b/core/src/io/anuke/mindustry/core/NetClient.java
index 85fc3b3ebc..4ba984c6d3 100644
--- a/core/src/io/anuke/mindustry/core/NetClient.java
+++ b/core/src/io/anuke/mindustry/core/NetClient.java
@@ -1,78 +1,138 @@
package io.anuke.mindustry.core;
+import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.utils.IntMap;
+import com.badlogic.gdx.utils.Base64Coder;
import com.badlogic.gdx.utils.IntSet;
-import com.badlogic.gdx.utils.TimeUtils;
-import io.anuke.mindustry.Vars;
+import io.anuke.annotations.Annotations.PacketPriority;
+import io.anuke.annotations.Annotations.Remote;
+import io.anuke.annotations.Annotations.Variant;
import io.anuke.mindustry.core.GameState.State;
-import io.anuke.mindustry.entities.Bullet;
-import io.anuke.mindustry.entities.BulletType;
import io.anuke.mindustry.entities.Player;
-import io.anuke.mindustry.entities.SyncEntity;
-import io.anuke.mindustry.entities.enemies.Enemy;
+import io.anuke.mindustry.entities.traits.SyncTrait;
+import io.anuke.mindustry.entities.traits.TypeTrait;
+import io.anuke.mindustry.gen.Call;
+import io.anuke.mindustry.gen.RemoteReadClient;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.net.Net.SendMode;
import io.anuke.mindustry.net.NetworkIO;
import io.anuke.mindustry.net.Packets.*;
-import io.anuke.mindustry.resource.Item;
-import io.anuke.mindustry.resource.Upgrade;
-import io.anuke.mindustry.resource.UpgradeRecipes;
-import io.anuke.mindustry.resource.Weapon;
-import io.anuke.mindustry.world.Block;
-import io.anuke.mindustry.world.Map;
-import io.anuke.mindustry.world.Placement;
-import io.anuke.mindustry.world.Tile;
-import io.anuke.mindustry.world.blocks.ProductionBlocks;
-import io.anuke.ucore.core.Effects;
+import io.anuke.mindustry.net.TraceInfo;
+import io.anuke.ucore.core.Settings;
import io.anuke.ucore.core.Timers;
-import io.anuke.ucore.entities.BaseBulletType;
import io.anuke.ucore.entities.Entities;
-import io.anuke.ucore.entities.Entity;
import io.anuke.ucore.entities.EntityGroup;
+import io.anuke.ucore.io.ReusableByteArrayInputStream;
+import io.anuke.ucore.io.delta.DEZDecoder;
import io.anuke.ucore.modules.Module;
import io.anuke.ucore.util.Log;
+import io.anuke.ucore.util.Mathf;
+import io.anuke.ucore.util.Pooling;
import io.anuke.ucore.util.Timer;
-import java.nio.ByteBuffer;
+import java.io.DataInputStream;
+import java.util.Arrays;
+import java.util.Random;
import static io.anuke.mindustry.Vars.*;
-public class NetClient extends Module {
- private final static float dataTimeout = 60*18; //18 seconds timeout
+public class NetClient extends Module{
+ private final static float dataTimeout = 60 * 18;
private final static float playerSyncTime = 2;
- private final static int maxRequests = 50;
private Timer timer = new Timer(5);
+ /**
+ * Whether the client is currently connecting.
+ */
private boolean connecting = false;
- private boolean kicked = false;
- private IntSet recieved = new IntSet();
- private IntMap recent = new IntMap<>();
- private int requests = 0;
- private float timeoutTime = 0f; //data timeout counter
+ /**
+ * If true, no message will be shown on disconnect.
+ */
+ private boolean quiet = false;
+ /**
+ * Counter for data timeout.
+ */
+ private float timeoutTime = 0f;
+ /**
+ * Last sent client snapshot ID.
+ */
+ private int lastSent;
+
+ /**
+ * Last snapshot ID recieved.
+ */
+ private int lastSnapshotBaseID = -1;
+ /**
+ * Last snapshot recieved.
+ */
+ private byte[] lastSnapshotBase;
+ /**
+ * Current snapshot that is being built from chinks.
+ */
+ private byte[] currentSnapshot;
+ /**
+ * Array of recieved chunk statuses.
+ */
+ private boolean[] recievedChunks;
+ /**
+ * Counter of how many chunks have been recieved.
+ */
+ private int recievedChunkCounter;
+ /**
+ * ID of snapshot that is currently being constructed.
+ */
+ private int currentSnapshotID = -1;
+
+ /**
+ * Decoder for uncompressing snapshots.
+ */
+ private DEZDecoder decoder = new DEZDecoder();
+ /**
+ * List of entities that were removed, and need not be added while syncing.
+ */
+ private IntSet removed = new IntSet();
+ /**
+ * Byte stream for reading in snapshots.
+ */
+ private ReusableByteArrayInputStream byteStream = new ReusableByteArrayInputStream();
+ private DataInputStream dataStream = new DataInputStream(byteStream);
public NetClient(){
Net.handleClient(Connect.class, packet -> {
+ Player player = players[0];
+
player.isAdmin = false;
Net.setClientLoaded(false);
- recieved.clear();
- recent.clear();
+ removed.clear();
timeoutTime = 0f;
connecting = true;
- kicked = false;
+ quiet = false;
+ lastSent = 0;
+ lastSnapshotBase = null;
+ currentSnapshot = null;
+ currentSnapshotID = -1;
+ lastSnapshotBaseID = -1;
ui.chatfrag.clearMessages();
ui.loadfrag.hide();
ui.loadfrag.show("$text.connecting.data");
+ ui.loadfrag.setButton(() -> {
+ ui.loadfrag.hide();
+ connecting = false;
+ quiet = true;
+ Net.disconnect();
+ });
+
Entities.clear();
ConnectPacket c = new ConnectPacket();
c.name = player.name;
- c.android = mobile;
+ c.mobile = mobile;
c.color = Color.rgba8888(player.color);
+ c.usid = getUsid(packet.addressTCP);
c.uuid = Platform.instance.getUUID();
if(c.uuid == null){
@@ -86,7 +146,7 @@ public class NetClient extends Module {
});
Net.handleClient(Disconnect.class, packet -> {
- if (kicked) return;
+ if(quiet) return;
Timers.runTask(3f, ui.loadfrag::hide);
@@ -98,226 +158,177 @@ public class NetClient extends Module {
Platform.instance.updateRPC();
});
- Net.handleClient(WorldData.class, data -> {
+ Net.handleClient(WorldStream.class, data -> {
Log.info("Recieved world data: {0} bytes.", data.stream.available());
NetworkIO.loadWorld(data.stream);
- player.set(world.getSpawnX(), world.getSpawnY());
finishConnecting();
});
- Net.handleClient(CustomMapPacket.class, packet -> {
- Log.info("Recieved custom map: {0} bytes.", packet.stream.available());
-
- //custom map is always sent before world data
- Map map = NetworkIO.loadMap(packet.stream);
-
- world.maps().setNetworkMap(map);
-
- MapAckPacket ack = new MapAckPacket();
- Net.send(ack, SendMode.tcp);
+ Net.handleClient(InvokePacket.class, packet -> {
+ packet.writeBuffer.position(0);
+ RemoteReadClient.readPacket(packet.writeBuffer, packet.type);
});
+ }
- Net.handleClient(SyncPacket.class, packet -> {
- if (connecting) return;
- int players = 0;
- int enemies = 0;
+ @Remote(variants = Variant.one, priority = PacketPriority.high)
+ public static void onKick(KickReason reason){
+ netClient.disconnectQuietly();
+ state.set(State.menu);
+ if(!reason.quiet) ui.showError("$text.server.kicked." + reason.name());
+ ui.loadfrag.hide();
+ }
- ByteBuffer data = ByteBuffer.wrap(packet.data);
- long time = data.getLong();
+ @Remote(variants = Variant.one)
+ public static void onPositionSet(float x, float y){
+ players[0].x = x;
+ players[0].y = y;
+ }
- byte groupid = data.get();
+ @Remote(variants = Variant.one)
+ public static void onTraceInfo(TraceInfo info){
+ Player player = playerGroup.getByID(info.playerid);
+ ui.traces.show(player, info);
+ }
- EntityGroup> group = Entities.getGroup(groupid);
+ @Remote
+ public static void onPlayerDisconnect(int playerid){
+ playerGroup.removeByID(playerid);
+ }
- while (data.position() < data.capacity()) {
- int id = data.getInt();
+ @Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
+ public static void onSnapshot(byte[] chunk, int snapshotID, short chunkID, int totalLength, int base){
+ if(NetServer.showSnapshotSize)
+ Log.info("Recieved snapshot: len {0} ID {1} chunkID {2} totalLength {3} base {4} client-base {5}", chunk.length, snapshotID, chunkID, totalLength, base, netClient.lastSnapshotBaseID);
- SyncEntity entity = (SyncEntity) group.getByID(id);
+ //skip snapshot IDs that have already been recieved OR snapshots that are too far in front
+ if(snapshotID < netClient.lastSnapshotBaseID || base != netClient.lastSnapshotBaseID){
+ if(NetServer.showSnapshotSize) Log.info("//SKIP SNAPSHOT");
+ return;
+ }
- if(entity instanceof Player) players ++;
- if(entity instanceof Enemy) enemies ++;
+ try{
+ byte[] snapshot;
- if (entity == null || id == player.id) {
- if (id != player.id && requests < maxRequests) {
- EntityRequestPacket req = new EntityRequestPacket();
- req.id = id;
- req.group = groupid;
- Net.send(req, SendMode.udp);
- requests ++;
+ //total length exceeds that needed to hold one snapshot, therefore, it is split into chunks
+ if(totalLength > NetServer.maxSnapshotSize){
+ //total amount of chunks to recieve
+ int totalChunks = Mathf.ceil((float) totalLength / NetServer.maxSnapshotSize);
+
+ //reset status when a new snapshot sending begins
+ if(netClient.currentSnapshotID != snapshotID){
+ netClient.currentSnapshotID = snapshotID;
+ netClient.currentSnapshot = new byte[totalLength];
+ netClient.recievedChunkCounter = 0;
+ netClient.recievedChunks = new boolean[totalChunks];
+ }
+
+ //if this chunk hasn't been recieved yet...
+ if(!netClient.recievedChunks[chunkID]){
+ netClient.recievedChunks[chunkID] = true;
+ netClient.recievedChunkCounter++; //update recieved status
+ //copy the recieved bytes into the holding array
+ System.arraycopy(chunk, 0, netClient.currentSnapshot, chunkID * NetServer.maxSnapshotSize,
+ Math.min(NetServer.maxSnapshotSize, totalLength - chunkID * NetServer.maxSnapshotSize));
+ }
+
+ //when all chunks have been recieved, begin
+ if(netClient.recievedChunkCounter >= totalChunks){
+ snapshot = netClient.currentSnapshot;
+ }else{
+ return;
+ }
+ }else{
+ snapshot = chunk;
+ }
+
+ if(NetServer.showSnapshotSize)
+ Log.info("Finished recieving snapshot ID {0} length {1}", snapshotID, chunk.length);
+
+ byte[] result;
+ int length;
+ if(base == -1){ //fresh snapshot
+ result = snapshot;
+ length = snapshot.length;
+ netClient.lastSnapshotBase = Arrays.copyOf(snapshot, snapshot.length);
+ }else{ //otherwise, last snapshot must not be null, decode it
+ if(NetServer.showSnapshotSize)
+ Log.info("Base size: {0} Patch size: {1}", netClient.lastSnapshotBase.length, snapshot.length);
+ netClient.decoder.init(netClient.lastSnapshotBase, snapshot);
+ result = netClient.decoder.decode();
+ length = netClient.decoder.getDecodedLength();
+ //set last snapshot to a copy to prevent issues
+ netClient.lastSnapshotBase = Arrays.copyOf(result, length);
+ }
+
+ netClient.lastSnapshotBaseID = snapshotID;
+
+ //set stream bytes to begin snapshot reaeding
+ netClient.byteStream.setBytes(result, 0, length);
+
+ //get data input for reading from the stream
+ DataInputStream input = netClient.dataStream;
+
+ //read wave info
+ state.wavetime = input.readFloat();
+ state.wave = input.readInt();
+
+ byte cores = input.readByte();
+ for(int i = 0; i < cores; i++){
+ int pos = input.readInt();
+ world.tile(pos).entity.items.read(input);
+ }
+
+ long timestamp = input.readLong();
+
+ byte totalGroups = input.readByte();
+ //for each group...
+ for(int i = 0; i < totalGroups; i++){
+ //read group info
+ byte groupID = input.readByte();
+ short amount = input.readShort();
+
+ EntityGroup group = Entities.getGroup(groupID);
+
+ //go through each entity
+ for(int j = 0; j < amount; j++){
+ int position = netClient.byteStream.position(); //save position to check read/write correctness
+ int id = input.readInt();
+ byte typeID = input.readByte();
+
+ SyncTrait entity = (SyncTrait) group.getByID(id);
+ boolean add = false;
+
+ //entity must not be added yet, so create it
+ if(entity == null){
+ entity = (SyncTrait) TypeTrait.getTypeByID(typeID).get(); //create entity from supplier
+ entity.resetID(id);
+ if(!netClient.isEntityUsed(entity.getID())){
+ add = true;
+ }
+ }
+
+ //read the entity
+ entity.read(input, timestamp);
+
+ byte readLength = input.readByte();
+ if(netClient.byteStream.position() - position - 1 != readLength){
+ throw new RuntimeException("Error reading entity of type '" + group.getType() + "': Read length mismatch [write=" + readLength + ", read=" + (netClient.byteStream.position() - position - 1) + "]");
+ }
+
+ if(add){
+ entity.add();
+ netClient.addRemovedEntity(entity.getID());
}
- data.position(data.position() + SyncEntity.getWriteSize((Class extends SyncEntity>) group.getType()));
- } else {
- entity.read(data, time);
}
}
- if(debugNet){
- clientDebug.setSyncDebug(players, enemies);
- }
- });
+ //confirm that snapshot has been recieved
+ netClient.lastSnapshotBaseID = snapshotID;
- Net.handleClient(StateSyncPacket.class, packet -> {
-
- System.arraycopy(packet.items, 0, state.inventory.getItems(), 0, packet.items.length);
-
- state.enemies = packet.enemies;
- state.wavetime = packet.countdown;
- state.wave = packet.wave;
-
- ui.hudfrag.updateItems();
- });
-
- Net.handleClient(BlockLogRequestPacket.class, packet -> {
- currentEditLogs = packet.editlogs;
- });
-
- Net.handleClient(PlacePacket.class, (packet) -> {
- Placement.placeBlock(packet.x, packet.y, Block.getByID(packet.block), packet.rotation, true, Timers.get("placeblocksound", 10));
-
- if(packet.playerid == player.id){
- Tile tile = world.tile(packet.x, packet.y);
- if(tile != null) Block.getByID(packet.block).placed(tile);
- }
- });
-
- Net.handleClient(BreakPacket.class, (packet) ->
- Placement.breakBlock(packet.x, packet.y, true, Timers.get("breakblocksound", 10)));
-
- Net.handleClient(EntitySpawnPacket.class, packet -> {
- EntityGroup group = packet.group;
-
- //duplicates.
- if (group.getByID(packet.entity.id) != null ||
- recieved.contains(packet.entity.id)) return;
-
- recieved.add(packet.entity.id);
- recent.put(packet.entity.id, packet.entity);
-
- packet.entity.add();
-
- Log.info("Recieved entity {0}", packet.entity.id);
- });
-
- Net.handleClient(EnemyDeathPacket.class, packet -> {
- Enemy enemy = enemyGroup.getByID(packet.id);
- if (enemy != null){
- enemy.type.onDeath(enemy, true);
- }else if(recent.get(packet.id) != null){
- recent.get(packet.id).remove();
- }else{
- Log.err("Got remove for null entity! {0}", packet.id);
- }
- recieved.add(packet.id);
- });
-
- Net.handleClient(BulletPacket.class, packet -> {
- //TODO shoot effects for enemies, clientside as well as serverside
- BulletType type = (BulletType) BaseBulletType.getByID(packet.type);
- Entity owner = enemyGroup.getByID(packet.owner);
- new Bullet(type, owner, packet.x, packet.y, packet.angle).add();
- });
-
- Net.handleClient(BlockDestroyPacket.class, packet -> {
- Tile tile = world.tile(packet.position % world.width(), packet.position / world.width());
- if (tile != null && tile.entity != null) {
- tile.entity.onDeath(true);
- }
- });
-
- Net.handleClient(BlockUpdatePacket.class, packet -> {
- Tile tile = world.tile(packet.position % world.width(), packet.position / world.width());
- if (tile != null && tile.entity != null) {
- tile.entity.health = packet.health;
- }
- });
-
- Net.handleClient(DisconnectPacket.class, packet -> {
- Player player = playerGroup.getByID(packet.playerid);
-
- if (player != null) {
- player.remove();
- }
-
- Platform.instance.updateRPC();
- });
-
- Net.handleClient(KickPacket.class, packet -> {
- kicked = true;
- Net.disconnect();
- state.set(State.menu);
- if(!packet.reason.quiet) ui.showError("$text.server.kicked." + packet.reason.name());
- ui.loadfrag.hide();
- });
-
- Net.handleClient(GameOverPacket.class, packet -> {
- if(world.getCore().block() != ProductionBlocks.core &&
- world.getCore().entity != null){
- world.getCore().entity.onDeath(true);
- }
- kicked = true;
- ui.restart.show();
- });
-
- Net.handleClient(FriendlyFireChangePacket.class, packet -> state.friendlyFire = packet.enabled);
-
- Net.handleClient(ItemTransferPacket.class, packet -> {
- Runnable r = () -> {
- Tile tile = world.tile(packet.position);
- if (tile == null || tile.entity == null) return;
- Tile next = tile.getNearby(packet.rotation);
- tile.entity.items[packet.itemid] --;
- next.block().handleItem(Item.getByID(packet.itemid), next, tile);
- };
-
- threads.run(r);
- });
-
- Net.handleClient(ItemSetPacket.class, packet -> {
- Runnable r = () -> {
- Tile tile = world.tile(packet.position);
- if (tile == null || tile.entity == null) return;
- tile.entity.items[packet.itemid] = packet.amount;
- };
-
- threads.run(r);
- });
-
- Net.handleClient(ItemOffloadPacket.class, packet -> {
- Runnable r = () -> {
- Tile tile = world.tile(packet.position);
- if (tile == null || tile.entity == null) return;
- Tile next = tile.getNearby(tile.getRotation());
- next.block().handleItem(Item.getByID(packet.itemid), next, tile);
- };
-
- threads.run(r);
- });
-
- Net.handleClient(NetErrorPacket.class, packet -> {
- ui.showError(packet.message);
- disconnectQuietly();
- });
-
- Net.handleClient(PlayerAdminPacket.class, packet -> {
- Player player = playerGroup.getByID(packet.id);
- player.isAdmin = packet.admin;
- ui.listfrag.rebuild();
- });
-
- Net.handleClient(TracePacket.class, packet -> {
- Player player = playerGroup.getByID(packet.info.playerid);
- ui.traces.show(player, packet.info);
- });
-
- Net.handleClient(UpgradePacket.class, packet -> {
- Weapon weapon = (Weapon) Upgrade.getByID(packet.id);
-
- state.inventory.removeItems(UpgradeRecipes.get(weapon));
- control.upgrades().addWeapon(weapon);
- ui.hudfrag.updateWeapons();
- Effects.sound("purchase");
- });
+ }catch(Exception e){
+ throw new RuntimeException(e);
+ }
}
@Override
@@ -333,7 +344,7 @@ public class NetClient extends Module {
if(timeoutTime > dataTimeout){
Log.err("Failed to load data!");
ui.loadfrag.hide();
- kicked = true;
+ quiet = true;
ui.showError("$text.disconnect.data");
Net.disconnect();
timeoutTime = 0f;
@@ -351,7 +362,7 @@ public class NetClient extends Module {
ui.loadfrag.hide();
ui.join.hide();
Net.setClientLoaded(true);
- Timers.runTask(1f, () -> Net.send(new ConnectConfirmPacket(), SendMode.tcp));
+ Gdx.app.postRunnable(Call::connectConfirm);
Timers.runTask(40f, Platform.instance::updateRPC);
}
@@ -360,26 +371,25 @@ public class NetClient extends Module {
}
public void disconnectQuietly(){
- kicked = true;
+ quiet = true;
Net.disconnect();
}
- public void clearRecieved(){
- recieved.clear();
+ public synchronized void addRemovedEntity(int id){
+ removed.add(id);
+ }
+
+ public synchronized boolean isEntityUsed(int id){
+ return removed.contains(id);
}
void sync(){
- requests = 0;
if(timer.get(0, playerSyncTime)){
- byte[] bytes = new byte[player.getWriteSize() + 8];
- ByteBuffer buffer = ByteBuffer.wrap(bytes);
- buffer.putLong(TimeUtils.millis());
- player.write(buffer);
-
- PositionPacket packet = new PositionPacket();
- packet.data = bytes;
+ ClientSnapshotPacket packet = Pooling.obtain(ClientSnapshotPacket.class);
+ packet.lastSnapshot = lastSnapshotBaseID;
+ packet.snapid = lastSent++;
Net.send(packet, SendMode.udp);
}
@@ -387,4 +397,17 @@ public class NetClient extends Module {
Net.updatePing();
}
}
-}
+
+ String getUsid(String ip){
+ if(Settings.getString("usid-" + ip, null) != null){
+ return Settings.getString("usid-" + ip, null);
+ }else{
+ byte[] bytes = new byte[8];
+ new Random().nextBytes(bytes);
+ String result = new String(Base64Coder.encode(bytes));
+ Settings.putString("usid-" + ip, result);
+ Settings.save();
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/src/io/anuke/mindustry/core/NetCommon.java b/core/src/io/anuke/mindustry/core/NetCommon.java
deleted file mode 100644
index 9b8a9435da..0000000000
--- a/core/src/io/anuke/mindustry/core/NetCommon.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package io.anuke.mindustry.core;
-
-import io.anuke.mindustry.entities.Player;
-import io.anuke.mindustry.net.Net;
-import io.anuke.mindustry.net.Net.SendMode;
-import io.anuke.mindustry.net.Packets.*;
-import io.anuke.mindustry.resource.Upgrade;
-import io.anuke.mindustry.resource.Weapon;
-import io.anuke.mindustry.world.Tile;
-import io.anuke.ucore.modules.Module;
-
-import static io.anuke.mindustry.Vars.*;
-
-public class NetCommon extends Module {
-
- public NetCommon(){
-
- Net.handle(ShootPacket.class, (packet) -> {
- Player player = playerGroup.getByID(packet.playerid);
-
- Weapon weapon = (Weapon) Upgrade.getByID(packet.weaponid);
- weapon.shoot(player, packet.x, packet.y, packet.rotation);
- });
-
- Net.handle(ChatPacket.class, (packet) -> {
- ui.chatfrag.addMessage(packet.text, colorizeName(packet.id, packet.name));
- });
-
- Net.handle(WeaponSwitchPacket.class, (packet) -> {
- Player player = playerGroup.getByID(packet.playerid);
-
- if (player == null) return;
-
- player.weaponLeft = (Weapon) Upgrade.getByID(packet.left);
- player.weaponRight = (Weapon) Upgrade.getByID(packet.right);
- });
-
- Net.handle(BlockTapPacket.class, (packet) -> {
- Tile tile = world.tile(packet.position);
- tile.block().tapped(tile);
- });
-
- Net.handle(BlockConfigPacket.class, (packet) -> {
- Tile tile = world.tile(packet.position);
- if (tile != null) tile.block().configure(tile, packet.data);
- });
-
- Net.handle(PlayerDeathPacket.class, (packet) -> {
- Player player = playerGroup.getByID(packet.id);
- if(player == null) return;
-
- player.doRespawn();
- });
- }
-
- public void sendMessage(String message){
- ChatPacket packet = new ChatPacket();
- packet.name = null;
- packet.text = message;
- Net.send(packet, SendMode.tcp);
- if(!headless) ui.chatfrag.addMessage(message, null);
- }
-
- public String colorizeName(int id, String name){
- Player player = playerGroup.getByID(id);
- if(name == null || player == null) return null;
- return "[#" + player.color.toString().toUpperCase() + "]" + name;
- }
-}
diff --git a/core/src/io/anuke/mindustry/core/NetServer.java b/core/src/io/anuke/mindustry/core/NetServer.java
index 0559a441c6..fa2c5ac999 100644
--- a/core/src/io/anuke/mindustry/core/NetServer.java
+++ b/core/src/io/anuke/mindustry/core/NetServer.java
@@ -1,69 +1,102 @@
package io.anuke.mindustry.core;
-import com.badlogic.gdx.utils.*;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.Colors;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.IntMap;
+import com.badlogic.gdx.utils.TimeUtils;
+import io.anuke.annotations.Annotations.Loc;
+import io.anuke.annotations.Annotations.Remote;
+import io.anuke.mindustry.content.Mechs;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.Player;
-import io.anuke.mindustry.entities.SyncEntity;
-import io.anuke.mindustry.game.EventType.GameOverEvent;
+import io.anuke.mindustry.entities.traits.SyncTrait;
+import io.anuke.mindustry.gen.Call;
+import io.anuke.mindustry.gen.RemoteReadServer;
import io.anuke.mindustry.io.Version;
import io.anuke.mindustry.net.*;
import io.anuke.mindustry.net.Administration.PlayerInfo;
-import io.anuke.mindustry.net.Net.SendMode;
import io.anuke.mindustry.net.Packets.*;
-import io.anuke.mindustry.resource.*;
-import io.anuke.mindustry.world.Block;
-import io.anuke.mindustry.world.Placement;
import io.anuke.mindustry.world.Tile;
-import io.anuke.ucore.core.Events;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.EntityGroup;
+import io.anuke.ucore.entities.trait.Entity;
+import io.anuke.ucore.io.CountableByteArrayOutputStream;
+import io.anuke.ucore.io.delta.ByteDeltaEncoder;
+import io.anuke.ucore.io.delta.ByteMatcherHash;
+import io.anuke.ucore.io.delta.DEZEncoder;
import io.anuke.ucore.modules.Module;
import io.anuke.ucore.util.Log;
-import io.anuke.ucore.util.Timer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
-import java.nio.ByteBuffer;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
import static io.anuke.mindustry.Vars.*;
public class NetServer extends Module{
- private final static float serverSyncTime = 4, itemSyncTime = 10, kickDuration = 30 * 1000;
+ public final static int maxSnapshotSize = 2047;
+ public final static boolean showSnapshotSize = false;
- private final static int timerEntitySync = 0;
- private final static int timerStateSync = 1;
+ private final static byte[] reusableSnapArray = new byte[maxSnapshotSize];
+ private final static float serverSyncTime = 4, kickDuration = 30 * 1000;
+ private final static Vector2 vector = new Vector2();
+ /**
+ * If a play goes away of their server-side coordinates by this distance, they get teleported back.
+ */
+ private final static float correctDist = 16f;
public final Administration admins = new Administration();
- /**Maps connection IDs to players.*/
+ /**
+ * Maps connection IDs to players.
+ */
private IntMap connections = new IntMap<>();
- private ObjectMap weapons = new ObjectMap<>();
private boolean closing = false;
- private Timer timer = new Timer(5);
+
+ /**
+ * Stream for writing player sync data to.
+ */
+ private CountableByteArrayOutputStream syncStream = new CountableByteArrayOutputStream();
+ /**
+ * Data stream for writing player sync data to.
+ */
+ private DataOutputStream dataStream = new DataOutputStream(syncStream);
+ /**
+ * Encoder for computing snapshot deltas.
+ */
+ private DEZEncoder encoder = new DEZEncoder();
public NetServer(){
- Events.on(GameOverEvent.class, () -> {
- weapons.clear();
- admins.getEditLogs().clear();
- });
-
Net.handleServer(Connect.class, (id, connect) -> {
if(admins.isIPBanned(connect.addressTCP)){
kick(id, KickReason.banned);
}
});
+ Net.handleServer(Disconnect.class, (id, packet) -> {
+ Player player = connections.get(id);
+ if(player != null){
+ onDisconnect(player);
+ }
+ connections.remove(id);
+ });
+
Net.handleServer(ConnectPacket.class, (id, packet) -> {
- String uuid = new String(Base64Coder.encode(packet.uuid));
+ String uuid = packet.uuid;
if(Net.getConnection(id) == null ||
admins.isIPBanned(Net.getConnection(id).address)) return;
- TraceInfo trace = admins.getTrace(Net.getConnection(id).address);
+ TraceInfo trace = admins.getTraceByID(uuid);
PlayerInfo info = admins.getInfo(uuid);
trace.uuid = uuid;
- trace.android = packet.android;
+ trace.android = packet.mobile;
if(admins.isIDBanned(uuid)){
kick(id, KickReason.banned);
@@ -75,6 +108,34 @@ public class NetServer extends Module{
return;
}
+ if(packet.version == -1 && Version.build != -1 && !admins.allowsCustomClients()){
+ kick(id, KickReason.customClient);
+ return;
+ }
+
+ boolean preventDuplicates = headless;
+
+ if(preventDuplicates){
+ for(Player player : playerGroup.all()){
+ if(player.name.equalsIgnoreCase(packet.name)){
+ kick(id, KickReason.nameInUse);
+ return;
+ }
+
+ if(player.uuid.equals(packet.uuid)){
+ kick(id, KickReason.idInUse);
+ return;
+ }
+ }
+ }
+
+ packet.name = fixName(packet.name);
+
+ if(packet.name.trim().length() <= 0){
+ kick(id, KickReason.nameEmpty);
+ return;
+ }
+
Log.info("Recieved connect packet for player '{0}' / UUID {1} / IP {2}", packet.name, uuid, trace.ip);
String ip = Net.getConnection(id).address;
@@ -91,282 +152,180 @@ public class NetServer extends Module{
}
Player player = new Player();
- player.isAdmin = admins.isAdmin(uuid, ip);
- player.clientid = id;
+ player.isAdmin = admins.isAdmin(uuid, packet.usid);
+ player.con = Net.getConnection(id);
+ player.usid = packet.usid;
player.name = packet.name;
- player.isAndroid = packet.android;
- player.set(world.getSpawnX(), world.getSpawnY());
- player.setNet(player.x, player.y);
+ player.uuid = uuid;
+ player.isMobile = packet.mobile;
+ player.mech = packet.mobile ? Mechs.starterMobile : Mechs.starterDesktop;
+ player.dead = true;
player.setNet(player.x, player.y);
player.color.set(packet.color);
+ player.color.a = 1f;
connections.put(id, player);
trace.playerid = player.id;
- if(world.getMap().custom){
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- NetworkIO.writeMap(world.getMap(), stream);
- CustomMapPacket data = new CustomMapPacket();
- data.stream = new ByteArrayInputStream(stream.toByteArray());
- Net.sendStream(id, data);
-
- Log.info("Sending custom map: Packed {0} uncompressed bytes of MAP data.", stream.size());
- }else{
- //hack-- simulate the map ack packet recieved to send the world data to the client.
- Net.handleServerReceived(id, new MapAckPacket());
- }
-
- Platform.instance.updateRPC();
- });
-
- Net.handleServer(MapAckPacket.class, (id, packet) -> {
- Player player = connections.get(id);
-
+ //TODO try DeflaterOutputStream
ByteArrayOutputStream stream = new ByteArrayOutputStream();
- NetworkIO.writeWorld(player, weapons.get(admins.getTrace(Net.getConnection(id).address).uuid, new ByteArray()), stream);
- WorldData data = new WorldData();
+ NetworkIO.writeWorld(player, stream);
+ WorldStream data = new WorldStream();
data.stream = new ByteArrayInputStream(stream.toByteArray());
Net.sendStream(id, data);
Log.info("Packed {0} uncompressed bytes of WORLD data.", stream.size());
- });
-
- Net.handleServer(ConnectConfirmPacket.class, (id, packet) -> {
- Player player = connections.get(id);
-
- if (player == null) return;
-
- player.add();
- Log.info("&y{0} has connected.", player.name);
- netCommon.sendMessage("[accent]" + player.name + " has connected.");
- });
-
- Net.handleServer(Disconnect.class, (id, packet) -> {
- Player player = connections.get(packet.id);
-
- if (player == null) {
- Log.err("Unknown client has disconnected (ID={0})", id);
- return;
- }
-
- Log.info("&y{0} has disconnected.", player.name);
- netCommon.sendMessage("[accent]" + player.name + " has disconnected.");
- player.remove();
-
- DisconnectPacket dc = new DisconnectPacket();
- dc.playerid = player.id;
-
- Net.send(dc, SendMode.tcp);
Platform.instance.updateRPC();
- admins.save();
});
- Net.handleServer(PositionPacket.class, (id, packet) -> {
- ByteBuffer buffer = ByteBuffer.wrap(packet.data);
- long time = buffer.getLong();
-
+ //update last recieved snapshot based on client snapshot
+ Net.handleServer(ClientSnapshotPacket.class, (id, packet) -> {
Player player = connections.get(id);
- player.read(buffer, time);
+ NetConnection connection = Net.getConnection(id);
+ if(player == null || connection == null || packet.snapid < connection.lastRecievedClientSnapshot) return;
+
+ boolean verifyPosition = !player.isDead() && !debug && headless && !player.mech.flying && player.getCarrier() == null;
+
+ if(connection.lastRecievedClientTime == 0) connection.lastRecievedClientTime = TimeUtils.millis() - 16;
+
+ long elapsed = TimeUtils.timeSinceMillis(connection.lastRecievedClientTime);
+
+ float maxSpeed = (packet.boosting && !player.mech.flying ? player.mech.boostSpeed : player.mech.speed) * 2.5f;
+
+ //extra 1.1x multiplicaton is added just in case
+ float maxMove = elapsed / 1000f * 60f * maxSpeed * 1.1f;
+
+ player.pointerX = packet.pointerX;
+ player.pointerY = packet.pointerY;
+ player.setMineTile(packet.mining);
+ player.isBoosting = packet.boosting;
+ player.isShooting = packet.shooting;
+ player.getPlaceQueue().clear();
+ if(packet.currentRequest != null){
+ player.getPlaceQueue().addLast(packet.currentRequest);
+ }
+
+ vector.set(packet.x - player.getInterpolator().target.x, packet.y - player.getInterpolator().target.y);
+
+ vector.limit(maxMove);
+
+ float prevx = player.x, prevy = player.y;
+ player.set(player.getInterpolator().target.x, player.getInterpolator().target.y);
+ player.move(vector.x, vector.y);
+ float newx = player.x, newy = player.y;
+
+ if(!verifyPosition){
+ player.x = prevx;
+ player.y = prevy;
+ newx = packet.x;
+ newy = packet.y;
+ }else if(Vector2.dst(packet.x, packet.y, newx, newy) > correctDist){
+ Call.onPositionSet(id, newx, newy); //teleport and correct position when necessary
+ }
+ //reset player to previous synced position so it gets interpolated
+ player.x = prevx;
+ player.y = prevy;
+
+ //set interpolator target to *new* position so it moves toward it
+ player.getInterpolator().read(player.x, player.y, newx, newy, packet.timeSent, packet.rotation, packet.baseRotation);
+ player.getVelocity().set(packet.xv, packet.yv); //only for visual calculation purposes, doesn't actually update the player
+
+ //when the client confirms recieveing a snapshot, update base and clear map
+ if(packet.lastSnapshot > connection.currentBaseID){
+ connection.currentBaseID = packet.lastSnapshot;
+ connection.currentBaseSnapshot = connection.lastSentRawSnapshot;
+ }
+
+ connection.lastRecievedClientSnapshot = packet.snapid;
+ connection.lastRecievedClientTime = TimeUtils.millis();
});
- Net.handleServer(ShootPacket.class, (id, packet) -> {
- TraceInfo info = admins.getTrace(Net.getConnection(id).address);
- Weapon weapon = (Weapon)Upgrade.getByID(packet.weaponid);
+ Net.handleServer(InvokePacket.class, (id, packet) -> {
+ Player player = connections.get(id);
+ if(player == null) return;
+ RemoteReadServer.readPacket(packet.writeBuffer, packet.type, player);
+ });
+ }
- float wtrc = 80;
-
- if(!Timers.get("fastshoot-" + id + "-" + weapon.id, wtrc)){
- info.fastShots.getAndIncrement(weapon.id, 0, 1);
-
- if(info.fastShots.get(weapon.id, 0) > (int)(wtrc / (weapon.getReload() / 2f)) + 30){
- kick(id, KickReason.fastShoot);
+ /**
+ * Sends a raw byte[] snapshot to a client, splitting up into chunks when needed.
+ */
+ private static void sendSplitSnapshot(int userid, byte[] bytes, int snapshotID, int base){
+ if(bytes.length < maxSnapshotSize){
+ Call.onSnapshot(userid, bytes, snapshotID, (short) 0, bytes.length, base);
+ }else{
+ int remaining = bytes.length;
+ int offset = 0;
+ int chunkid = 0;
+ while(remaining > 0){
+ int used = Math.min(remaining, maxSnapshotSize);
+ byte[] toSend;
+ //re-use sent byte arrays when possible
+ if(used == maxSnapshotSize){
+ toSend = reusableSnapArray;
+ System.arraycopy(bytes, offset, toSend, 0, Math.min(offset + maxSnapshotSize, bytes.length) - offset);
+ }else{
+ toSend = Arrays.copyOfRange(bytes, offset, Math.min(offset + maxSnapshotSize, bytes.length));
}
+ Call.onSnapshot(userid, toSend, snapshotID, (short) chunkid, bytes.length, base);
+
+ remaining -= used;
+ offset += used;
+ chunkid++;
+ }
+ }
+ }
+
+ public static void onDisconnect(Player player){
+ Call.sendMessage("[accent]" + player.name + " has disconnected.");
+ Call.onPlayerDisconnect(player.id);
+ player.remove();
+ netServer.connections.remove(player.con.id);
+ }
+
+ @Remote(targets = Loc.client, called = Loc.server)
+ public static void onAdminRequest(Player player, Player other, AdminAction action){
+
+ if(!player.isAdmin){
+ Log.err("ACCESS DENIED: Player {0} / {1} attempted to perform admin action without proper security access.",
+ player.name, player.con.address);
+ return;
+ }
+
+ if(other == null || (other.isAdmin && other != player)){ //fun fact: this means you can ban yourself
+ Log.err("{0} attempted to perform admin action on nonexistant or admin player.", player.name);
+ return;
+ }
+
+ if(action == AdminAction.wave){
+ //no verification is done, so admins can hypothetically spam waves
+ //not a real issue, because server owners may want to do just that
+ state.wavetime = 0f;
+ }else if(action == AdminAction.ban){
+ netServer.admins.banPlayerIP(other.con.address);
+ netServer.kick(other.con.id, KickReason.banned);
+ Log.info("&lc{0} has banned {1}.", player.name, other.name);
+ }else if(action == AdminAction.kick){
+ netServer.kick(other.con.id, KickReason.kick);
+ Log.info("&lc{0} has kicked {1}.", player.name, other.name);
+ }else if(action == AdminAction.trace){
+ //TODO
+ if(player.con != null){
+ Call.onTraceInfo(player.con.id, netServer.admins.getTraceByID(other.uuid));
}else{
- info.fastShots.put(weapon.id, 0);
+ NetClient.onTraceInfo(netServer.admins.getTraceByID(other.uuid));
}
+ Log.info("&lc{0} has requested trace info of {1}.", player.name, other.name);
+ }
+ }
- packet.playerid = connections.get(id).id;
- Net.sendExcept(id, packet, SendMode.udp);
- });
-
- Net.handleServer(PlacePacket.class, (id, packet) -> {
- packet.playerid = connections.get(id).id;
-
- Block block = Block.getByID(packet.block);
-
- if(!Placement.validPlace(packet.x, packet.y, block)) return;
-
- Recipe recipe = Recipes.getByResult(block);
-
- if(recipe == null) return;
-
- Tile tile = world.tile(packet.x, packet.y);
- if(tile.synthetic() && admins.isValidateReplace() && !admins.validateBreak(admins.getTrace(Net.getConnection(id).address).uuid, Net.getConnection(id).address)){
- if(Timers.get("break-message-" + id, 120)){
- sendMessageTo(id, "[scarlet]Anti-grief: you are replacing blocks too quickly. wait until replacing again.");
- }
- return;
- }
-
- state.inventory.removeItems(recipe.requirements);
-
- Placement.placeBlock(packet.x, packet.y, block, packet.rotation, true, false);
-
- admins.logEdit(packet.x, packet.y, connections.get(id), block, packet.rotation, EditLog.EditAction.PLACE);
- admins.getTrace(Net.getConnection(id).address).lastBlockPlaced = block;
- admins.getTrace(Net.getConnection(id).address).totalBlocksPlaced ++;
- admins.getInfo(admins.getTrace(Net.getConnection(id).address).uuid).totalBlockPlaced ++;
-
- Net.send(packet, SendMode.tcp);
- });
-
- Net.handleServer(BreakPacket.class, (id, packet) -> {
- packet.playerid = connections.get(id).id;
-
- if(!Placement.validBreak(packet.x, packet.y)) return;
-
- Tile tile = world.tile(packet.x, packet.y);
-
- if(tile.synthetic() && !admins.validateBreak(admins.getTrace(Net.getConnection(id).address).uuid, Net.getConnection(id).address)){
- if(Timers.get("break-message-" + id, 120)){
- sendMessageTo(id, "[scarlet]Anti-grief: you are breaking blocks too quickly. wait until breaking again.");
- }
- return;
- }
-
- Block block = Placement.breakBlock(packet.x, packet.y, true, false);
-
- if(block != null) {
- admins.logEdit(packet.x, packet.y, connections.get(id), block, tile.getRotation(), EditLog.EditAction.BREAK);
- admins.getTrace(Net.getConnection(id).address).lastBlockBroken = block;
- admins.getTrace(Net.getConnection(id).address).totalBlocksBroken++;
- admins.getInfo(admins.getTrace(Net.getConnection(id).address).uuid).totalBlocksBroken ++;
- if (block.update || block.destructible)
- admins.getTrace(Net.getConnection(id).address).structureBlocksBroken++;
- }
-
- Net.send(packet, SendMode.tcp);
- });
-
- Net.handleServer(ChatPacket.class, (id, packet) -> {
- if(!Timers.get("chatFlood" + id, 20)){
- ChatPacket warn = new ChatPacket();
- warn.text = "[scarlet]You are sending messages too quickly.";
- Net.sendTo(id, warn, SendMode.tcp);
- return;
- }
- Player player = connections.get(id);
- packet.name = player.name;
- packet.id = player.id;
- Net.send(packet, SendMode.tcp);
- });
-
- Net.handleServer(UpgradePacket.class, (id, packet) -> {
- Player player = connections.get(id);
-
- Weapon weapon = (Weapon) Upgrade.getByID(packet.id);
- String uuid = admins.getTrace(Net.getConnection(id).address).uuid;
-
- if(!state.inventory.hasItems(UpgradeRecipes.get(weapon))){
- return;
- }
-
- if (!weapons.containsKey(uuid)) weapons.put(uuid, new ByteArray());
-
- if (!weapons.get(uuid).contains(weapon.id)){
- weapons.get(uuid).add(weapon.id);
- }else{
- return;
- }
-
- state.inventory.removeItems(UpgradeRecipes.get(weapon));
- Net.sendTo(id, packet, SendMode.tcp);
- });
-
- Net.handleServer(WeaponSwitchPacket.class, (id, packet) -> {
- TraceInfo info = admins.getTrace(Net.getConnection(id).address);
-
- packet.playerid = connections.get(id).id;
- Net.sendExcept(id, packet, SendMode.tcp);
- });
-
- Net.handleServer(BlockTapPacket.class, (id, packet) -> {
- Net.sendExcept(id, packet, SendMode.tcp);
- });
-
- Net.handleServer(BlockConfigPacket.class, (id, packet) -> {
- Net.sendExcept(id, packet, SendMode.tcp);
- });
-
- Net.handleServer(EntityRequestPacket.class, (cid, packet) -> {
-
- int id = packet.id;
- int dest = cid;
- EntityGroup group = Entities.getGroup(packet.group);
- if(group.getByID(id) != null){
- EntitySpawnPacket p = new EntitySpawnPacket();
- p.entity = (SyncEntity)group.getByID(id);
- p.group = group;
- Net.sendTo(dest, p, SendMode.tcp);
- }
- });
-
- Net.handleServer(PlayerDeathPacket.class, (id, packet) -> {
- packet.id = connections.get(id).id;
- Net.sendExcept(id, packet, SendMode.tcp);
- });
-
- Net.handleServer(AdministerRequestPacket.class, (id, packet) -> {
- Player player = connections.get(id);
-
- if(!player.isAdmin){
- Log.err("ACCESS DENIED: Player {0} / {1} attempted to perform admin action without proper security access.",
- player.name, Net.getConnection(player.clientid).address);
- return;
- }
-
- Player other = playerGroup.getByID(packet.id);
-
- if(other == null || other.isAdmin){
- Log.err("{0} attempted to perform admin action on nonexistant or admin player.", player.name);
- return;
- }
-
- String ip = Net.getConnection(other.clientid).address;
-
- if(packet.action == AdminAction.ban){
- admins.banPlayerIP(ip);
- kick(other.clientid, KickReason.banned);
- Log.info("&lc{0} has banned {1}.", player.name, other.name);
- }else if(packet.action == AdminAction.kick){
- kick(other.clientid, KickReason.kick);
- Log.info("&lc{0} has kicked {1}.", player.name, other.name);
- }else if(packet.action == AdminAction.trace){
- TracePacket trace = new TracePacket();
- trace.info = admins.getTrace(ip);
- Net.sendTo(id, trace, SendMode.tcp);
- Log.info("&lc{0} has requested trace info of {1}.", player.name, other.name);
- }
- });
-
- Net.handleServer(BlockLogRequestPacket.class, (id, packet) -> {
- packet.editlogs = admins.getEditLogs().get(packet.x + packet.y * world.width(), new Array<>());
- Net.sendTo(id, packet, SendMode.udp);
- });
-
- Net.handleServer(RollbackRequestPacket.class, (id, packet) -> {
- Player player = connections.get(id);
-
- if(!player.isAdmin){
- Log.err("ACCESS DENIED: Player {0} / {1} attempted to perform a rollback without proper security access.",
- player.name, Net.getConnection(player.clientid).address);
- return;
- }
-
- admins.rollbackWorld(packet.rollbackTimes);
- Log.info("&lc{0} has rolled back the world {1} times.", player.name, packet.rollbackTimes);
- });
+ @Remote(targets = Loc.client)
+ public static void connectConfirm(Player player){
+ player.add();
+ player.con.hasConnected = true;
+ Call.sendMessage("[accent]" + player.name + " has connected.");
+ Log.info("&y{0} has connected.", player.name);
}
public void update(){
@@ -387,7 +346,6 @@ public class NetServer extends Module{
}
public void reset(){
- weapons.clear();
admins.clearTraces();
}
@@ -400,104 +358,184 @@ public class NetServer extends Module{
Log.info("Kicking connection #{0} / IP: {1}. Reason: {2}", connection, con.address, reason);
}
- if((reason == KickReason.kick || reason == KickReason.banned) && admins.getTrace(con.address).uuid != null){
- PlayerInfo info = admins.getInfo(admins.getTrace(con.address).uuid);
- info.timesKicked ++;
+ if((reason == KickReason.kick || reason == KickReason.banned) && admins.getTraceByID(getUUID(con.id)).uuid != null){
+ PlayerInfo info = admins.getInfo(admins.getTraceByID(getUUID(con.id)).uuid);
+ info.timesKicked++;
info.lastKicked = TimeUtils.millis();
}
- KickPacket p = new KickPacket();
- p.reason = reason;
+ //TODO kick player, send kick packet
+ Call.onKick(connection, reason);
- con.send(p, SendMode.tcp);
Timers.runTask(2f, con::close);
admins.save();
}
- void sendMessageTo(int id, String message){
- ChatPacket packet = new ChatPacket();
- packet.text = message;
- Net.sendTo(id, packet, SendMode.tcp);
+ String getUUID(int connectionID){
+ return connections.get(connectionID).uuid;
}
- void sync(){
+ String fixName(String name){
- if(timer.get(timerEntitySync, serverSyncTime)){
- //scan through all groups with syncable entities
- for(EntityGroup> group : Entities.getAllGroups()) {
- if(group.size() == 0 || !(group.all().iterator().next() instanceof SyncEntity)) continue;
+ for(int i = 0; i < name.length(); i++){
+ if(name.charAt(i) == '[' && i != name.length() - 1 && name.charAt(i + 1) != '[' && (i == 0 || name.charAt(i - 1) != '[')){
+ String prev = name.substring(0, i);
+ String next = name.substring(i);
+ String result = checkColor(next);
- //get write size for one entity (adding 4, as you need to write the ID as well)
- int writesize = SyncEntity.getWriteSize((Class extends SyncEntity>)group.getType()) + 4;
- //amount of entities
- int amount = group.size();
- //maximum amount of entities per packet
- int maxsize = 64;
-
- //current buffer you're writing to
- ByteBuffer current = null;
- //number of entities written to this packet/buffer
- int written = 0;
-
- //for all the entities...
- for (int i = 0; i < amount; i++) {
- //if the buffer is null, create a new one
- if(current == null){
- //calculate amount of entities to go into this packet
- int csize = Math.min(amount-i, maxsize);
- //create a byte array to write to
- byte[] bytes = new byte[csize*writesize + 1 + 8];
- //wrap it for easy writing
- current = ByteBuffer.wrap(bytes);
- current.putLong(TimeUtils.millis());
- //write the group ID so the client knows which group this is
- current.put((byte)group.getID());
- }
-
- SyncEntity entity = (SyncEntity) group.all().get(i);
-
- //write ID to the buffer
- current.putInt(entity.id);
-
- int previous = current.position();
- //write extra data to the buffer
- entity.write(current);
-
- written ++;
-
- //if the packet is too big now...
- if(written >= maxsize){
- //send the packet.
- SyncPacket packet = new SyncPacket();
- packet.data = current.array();
- Net.send(packet, SendMode.udp);
-
- //reset data, send the next packet
- current = null;
- written = 0;
- }
- }
-
- //make sure to send incomplete packets too
- if(current != null){
- SyncPacket packet = new SyncPacket();
- packet.data = current.array();
- Net.send(packet, SendMode.udp);
- }
+ name = prev + result;
}
}
- if(timer.get(timerStateSync, itemSyncTime)){
- StateSyncPacket packet = new StateSyncPacket();
- packet.items = state.inventory.getItems();
- packet.countdown = state.wavetime;
- packet.enemies = state.enemies;
- packet.wave = state.wave;
- packet.time = Timers.time();
- packet.timestamp = TimeUtils.millis();
-
- Net.send(packet, SendMode.udp);
+ return name.substring(0, Math.min(name.length(), maxNameLength));
+ }
+
+ String checkColor(String str){
+
+ for(int i = 1; i < str.length(); i++){
+ if(str.charAt(i) == ']'){
+ String color = str.substring(1, i);
+
+ if(Colors.get(color.toUpperCase()) != null || Colors.get(color.toLowerCase()) != null){
+ Color result = (Colors.get(color.toLowerCase()) == null ? Colors.get(color.toUpperCase()) : Colors.get(color.toLowerCase()));
+ if(result.a <= 0.8f){
+ return str.substring(i + 1);
+ }
+ }else{
+ try{
+ Color result = Color.valueOf(color);
+ if(result.a <= 0.8f){
+ return str.substring(i + 1);
+ }
+ }catch(Exception e){
+ return str;
+ }
+ }
+ }
+ }
+ return str;
+ }
+
+ void sync(){
+ try{
+
+ //iterate through each player
+ for(Player player : connections.values()){
+ NetConnection connection = player.con;
+
+ if(!connection.isConnected()){
+ //player disconnected, ignore them
+ onDisconnect(player);
+ return;
+ }
+
+ if(!player.timer.get(Player.timerSync, serverSyncTime) || !connection.hasConnected) continue;
+
+ //if the player hasn't acknowledged that it has recieved the packet, send the same thing again
+ if(connection.currentBaseID < connection.lastSentSnapshotID){
+ if(showSnapshotSize)
+ Log.info("Re-sending snapshot: {0} bytes, ID {1} base {2} baselength {3}", connection.lastSentSnapshot.length, connection.lastSentSnapshotID, connection.lastSentBase, connection.currentBaseSnapshot.length);
+ sendSplitSnapshot(connection.id, connection.lastSentSnapshot, connection.lastSentSnapshotID, connection.lastSentBase);
+ return;
+ }
+
+ //reset stream to begin writing
+ syncStream.reset();
+
+ //write wave datas
+ dataStream.writeFloat(state.wavetime);
+ dataStream.writeInt(state.wave);
+
+ Array cores = state.teams.get(player.getTeam()).cores;
+
+ dataStream.writeByte(cores.size);
+
+ //write all core inventory data
+ for(Tile tile : cores){
+ dataStream.writeInt(tile.packedPosition());
+ tile.entity.items.write(dataStream);
+ }
+
+ //write timestamp
+ dataStream.writeLong(TimeUtils.millis());
+
+ int totalGroups = 0;
+
+ for(EntityGroup> group : Entities.getAllGroups()){
+ if(!group.isEmpty() && (group.all().get(0) instanceof SyncTrait)) totalGroups++;
+ }
+
+ //write total amount of serializable groups
+ dataStream.writeByte(totalGroups);
+
+ //check for syncable groups
+ for(EntityGroup> group : Entities.getAllGroups()){
+ //TODO range-check sync positions to optimize?
+ if(group.isEmpty() || !(group.all().get(0) instanceof SyncTrait)) continue;
+
+ //make sure mapping is enabled for this group
+ if(!group.mappingEnabled()){
+ throw new RuntimeException("Entity group '" + group.getType() + "' contains SyncTrait entities, yet mapping is not enabled. In order for syncing to work, you must enable mapping for this group.");
+ }
+
+ int amount = 0;
+
+ for(Entity entity : group.all()){
+ if(((SyncTrait) entity).isSyncing()){
+ amount++;
+ }
+ }
+
+ //write group ID + group size
+ dataStream.writeByte(group.getID());
+ dataStream.writeShort(amount);
+
+ for(Entity entity : group.all()){
+ if(!((SyncTrait) entity).isSyncing()) continue;
+
+ int position = syncStream.position();
+ //write all entities now
+ dataStream.writeInt(entity.getID()); //write id
+ dataStream.writeByte(((SyncTrait) entity).getTypeID()); //write type ID
+ ((SyncTrait) entity).write(dataStream); //write entity
+ int length = syncStream.position() - position; //length must always be less than 127 bytes
+ if(length > 127)
+ throw new RuntimeException("Write size for entity of type " + group.getType() + " must not exceed 127!");
+ dataStream.writeByte(length);
+ }
+ }
+
+ byte[] bytes = syncStream.toByteArray();
+
+ if(connection.currentBaseID == -1){
+ //assign to last sent snapshot so that there is only ever one unique snapshot with ID 0
+ if(connection.lastSentSnapshot != null){
+ bytes = connection.lastSentSnapshot;
+ }else{
+ connection.lastSentRawSnapshot = bytes;
+ connection.lastSentSnapshot = bytes;
+ }
+
+ if(showSnapshotSize) Log.info("Sent raw snapshot: {0} bytes.", bytes.length);
+ ///Nothing to diff off of in this case, send the whole thing
+ sendSplitSnapshot(connection.id, bytes, 0, -1);
+ }else{
+ connection.lastSentRawSnapshot = bytes;
+
+ //send diff, otherwise
+ byte[] diff = ByteDeltaEncoder.toDiff(new ByteMatcherHash(connection.currentBaseSnapshot, bytes), encoder);
+ if(showSnapshotSize)
+ Log.info("Shrank snapshot: {0} -> {1}, Base {2} ID {3} base length = {4}", bytes.length, diff.length, connection.currentBaseID, connection.currentBaseID + 1, connection.currentBaseSnapshot.length);
+ sendSplitSnapshot(connection.id, diff, connection.currentBaseID + 1, connection.currentBaseID);
+ connection.lastSentSnapshot = diff;
+ connection.lastSentSnapshotID = connection.currentBaseID + 1;
+ connection.lastSentBase = connection.currentBaseID;
+ }
+ }
+
+ }catch(IOException e){
+ e.printStackTrace();
}
}
}
diff --git a/core/src/io/anuke/mindustry/core/Platform.java b/core/src/io/anuke/mindustry/core/Platform.java
index 1c3ab23003..7462ba3eb2 100644
--- a/core/src/io/anuke/mindustry/core/Platform.java
+++ b/core/src/io/anuke/mindustry/core/Platform.java
@@ -4,8 +4,6 @@ import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Base64Coder;
import io.anuke.mindustry.core.ThreadHandler.ThreadProvider;
import io.anuke.ucore.core.Settings;
-import io.anuke.ucore.entities.Entity;
-import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.scene.ui.TextField;
@@ -13,77 +11,184 @@ import java.util.Date;
import java.util.Locale;
import java.util.Random;
-public abstract class Platform {
- /**Each separate game platform should set this instance to their own implementation.*/
- public static Platform instance = new Platform() {};
+public abstract class Platform{
+ /**
+ * Each separate game platform should set this instance to their own implementation.
+ */
+ public static Platform instance = new Platform(){
+ };
- /**Format the date using the default date formatter.*/
- public String format(Date date){return "invalid";}
- /**Format a number by adding in commas or periods where needed.*/
- public String format(int number){return "invalid";}
- /**Show a native error dialog.*/
- public void showError(String text){}
- /**Add a text input dialog that should show up after the field is tapped.*/
- public void addDialog(TextField field){
- addDialog(field, 16);
- }
- /**See addDialog().*/
- public void addDialog(TextField field, int maxLength){}
- /**Update discord RPC.*/
- public void updateRPC(){}
- /**Called when the game is exited.*/
- public void onGameExit(){}
- /**Open donation dialog. Currently android only.*/
- public void openDonations(){}
- /**Whether discord RPC is supported.*/
- public boolean hasDiscord(){return true;}
- /**Request Android permissions for writing files.*/
- public void requestWritePerms(){}
- /**Return the localized name for the locale. This is basically a workaround for GWT not supporting getName().*/
- public String getLocaleName(Locale locale){
- return locale.toString();
- }
- /**Whether joining games is supported.*/
- public boolean canJoinGame(){
- return true;
- }
- /**Whether debug mode is enabled.*/
- public boolean isDebug(){return false;}
- /**Must be 8 bytes in length.*/
- public byte[] getUUID(){
- String uuid = Settings.getString("uuid", "");
- if(uuid.isEmpty()){
- byte[] result = new byte[8];
- new Random().nextBytes(result);
- uuid = new String(Base64Coder.encode(result));
- Settings.putString("uuid", uuid);
- Settings.save();
- return result;
- }
- return Base64Coder.decode(uuid);
- }
- /**Only used for iOS or android: open the share menu for a map or save.*/
- public void shareFile(FileHandle file){}
+ /**
+ * Format the date using the default date formatter.
+ */
+ public String format(Date date){
+ return "invalid";
+ }
- /**Show a file chooser. Desktop only.
+ /**
+ * Format a number by adding in commas or periods where needed.
+ */
+ public String format(int number){
+ return "invalid";
+ }
+
+ /**
+ * Show a native error dialog.
+ */
+ public void showError(String text){
+ }
+
+ /**
+ * Add a text input dialog that should show up after the field is tapped.
+ */
+ public void addDialog(TextField field){
+ addDialog(field, 16);
+ }
+
+ /**
+ * See addDialog().
+ */
+ public void addDialog(TextField field, int maxLength){
+ }
+
+ /**
+ * Update discord RPC.
+ */
+ public void updateRPC(){
+ }
+
+ /**
+ * Called when the game is exited.
+ */
+ public void onGameExit(){
+ }
+
+ /**
+ * Open donation dialog. Currently android only.
+ */
+ public void openDonations(){
+ }
+
+ /**
+ * Whether donating is supported.
+ */
+ public boolean canDonate(){
+ return false;
+ }
+
+ /**
+ * Whether discord RPC is supported.
+ */
+ public boolean hasDiscord(){
+ return true;
+ }
+
+ /**
+ * Return the localized name for the locale. This is basically a workaround for GWT not supporting getName().
+ */
+ public String getLocaleName(Locale locale){
+ return locale.toString();
+ }
+
+ /**
+ * Whether joining games is supported.
+ */
+ public boolean canJoinGame(){
+ return true;
+ }
+
+ /**
+ * Whether debug mode is enabled.
+ */
+ public boolean isDebug(){
+ return false;
+ }
+
+ /**
+ * Must be a base64 string 8 bytes in length.
+ */
+ public String getUUID(){
+ String uuid = Settings.getString("uuid", "");
+ if(uuid.isEmpty()){
+ byte[] result = new byte[8];
+ new Random().nextBytes(result);
+ uuid = new String(Base64Coder.encode(result));
+ Settings.putString("uuid", uuid);
+ Settings.save();
+ return uuid;
+ }
+ return uuid;
+ }
+
+ /**
+ * Only used for iOS or android: open the share menu for a map or save.
+ */
+ public void shareFile(FileHandle file){
+ }
+
+ /**
+ * Download a file. Only used on GWT backend.
+ */
+ public void downloadFile(String name, byte[] bytes){
+ }
+
+ /**
+ * Show a file chooser. Desktop only.
*
* @param text File chooser title text
- * @param content Type of files to be loaded
+ * @param content Description of the type of files to be loaded
* @param cons Selection listener
- * @param open Whether to open or save files.
- * @param filetype File extensions to filter.
+ * @param open Whether to open or save files
+ * @param filetype File extension to filter
*/
- public void showFileChooser(String text, String content, Consumer cons, boolean open, String filetype){}
- /**Use the default thread provider from the kryonet module for this.*/
- public ThreadProvider getThreadProvider(){
- return new ThreadProvider() {
- @Override public boolean isOnThread() {return true;}
- @Override public void sleep(long ms) {}
- @Override public void start(Runnable run) {}
- @Override public void stop() {}
- @Override public void notify(Object object) {}
- @Override public void wait(Object object) {}
- @Override public void switchContainer(EntityGroup group) {}
- };
- }
+ public void showFileChooser(String text, String content, Consumer cons, boolean open, String filetype){
+ }
+
+ /**
+ * Use the default thread provider from the kryonet module for this.
+ */
+ public ThreadProvider getThreadProvider(){
+ return new ThreadProvider(){
+ @Override
+ public boolean isOnThread(){
+ return true;
+ }
+
+ @Override
+ public void sleep(long ms){
+ }
+
+ @Override
+ public void start(Runnable run){
+ }
+
+ @Override
+ public void stop(){
+ }
+
+ @Override
+ public void notify(Object object){
+ }
+
+ @Override
+ public void wait(Object object){
+ }
+ };
+ }
+
+ //TODO iOS implementation
+
+ /**
+ * Forces the app into landscape mode. Currently Android only.
+ */
+ public void beginForceLandscape(){
+ }
+
+ //TODO iOS implementation
+
+ /**
+ * Stops forcing the app into landscape orientation. Currently Android only.
+ */
+ public void endForceLandscape(){
+ }
}
diff --git a/core/src/io/anuke/mindustry/core/Renderer.java b/core/src/io/anuke/mindustry/core/Renderer.java
index 214b5785c0..65aa2f3eda 100644
--- a/core/src/io/anuke/mindustry/core/Renderer.java
+++ b/core/src/io/anuke/mindustry/core/Renderer.java
@@ -2,592 +2,475 @@ package io.anuke.mindustry.core;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.Colors;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureWrap;
-import com.badlogic.gdx.graphics.g2d.GlyphLayout;
-import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.Pools;
+import com.badlogic.gdx.utils.ObjectIntMap;
+import com.badlogic.gdx.utils.TimeUtils;
+import io.anuke.mindustry.content.fx.Fx;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.Player;
-import io.anuke.mindustry.entities.SyncEntity;
-import io.anuke.mindustry.entities.enemies.Enemy;
-import io.anuke.mindustry.game.SpawnPoint;
-import io.anuke.mindustry.graphics.BlockRenderer;
-import io.anuke.mindustry.graphics.Shaders;
-import io.anuke.mindustry.input.InputHandler;
-import io.anuke.mindustry.input.PlaceMode;
-import io.anuke.mindustry.ui.fragments.ToolFragment;
-import io.anuke.mindustry.world.BlockBar;
+import io.anuke.mindustry.entities.Unit;
+import io.anuke.mindustry.entities.effect.GroundEffectEntity;
+import io.anuke.mindustry.entities.effect.GroundEffectEntity.GroundEffect;
+import io.anuke.mindustry.entities.traits.BelowLiquidTrait;
+import io.anuke.mindustry.entities.units.BaseUnit;
+import io.anuke.mindustry.game.Team;
+import io.anuke.mindustry.graphics.*;
import io.anuke.mindustry.world.Tile;
-import io.anuke.mindustry.world.blocks.Blocks;
-import io.anuke.mindustry.world.blocks.ProductionBlocks;
-import io.anuke.ucore.core.*;
-import io.anuke.ucore.entities.EffectEntity;
-import io.anuke.ucore.entities.Entities;
-import io.anuke.ucore.function.Callable;
-import io.anuke.ucore.graphics.*;
+import io.anuke.mindustry.world.meta.BlockFlag;
+import io.anuke.ucore.core.Core;
+import io.anuke.ucore.core.Effects;
+import io.anuke.ucore.core.Graphics;
+import io.anuke.ucore.core.Settings;
+import io.anuke.ucore.entities.EntityDraw;
+import io.anuke.ucore.entities.EntityGroup;
+import io.anuke.ucore.entities.impl.BaseEntity;
+import io.anuke.ucore.entities.impl.EffectEntity;
+import io.anuke.ucore.entities.trait.DrawTrait;
+import io.anuke.ucore.entities.trait.SolidTrait;
+import io.anuke.ucore.function.Consumer;
+import io.anuke.ucore.function.Predicate;
+import io.anuke.ucore.graphics.Draw;
+import io.anuke.ucore.graphics.Hue;
+import io.anuke.ucore.graphics.Lines;
+import io.anuke.ucore.graphics.Surface;
import io.anuke.ucore.modules.RendererModule;
-import io.anuke.ucore.scene.ui.layout.Unit;
import io.anuke.ucore.scene.utils.Cursors;
-import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
-import io.anuke.ucore.util.Tmp;
+import io.anuke.ucore.util.Pooling;
+import io.anuke.ucore.util.Translator;
import static io.anuke.mindustry.Vars.*;
import static io.anuke.ucore.core.Core.batch;
import static io.anuke.ucore.core.Core.camera;
public class Renderer extends RendererModule{
- private final static float shieldHitDuration = 18f;
-
- public Surface shadowSurface, shieldSurface, indicatorSurface;
-
- private int targetscale = baseCameraScale;
- private Texture background = new Texture("sprites/background.png");
- private FloatArray shieldHits = new FloatArray();
- private Array shieldDraws = new Array<>();
- private Rectangle rect = new Rectangle(), rect2 = new Rectangle();
- private BlockRenderer blocks = new BlockRenderer();
+ public Surface effectSurface;
- public Renderer() {
- Lines.setCircleVertices(14);
+ private int targetscale = baseCameraScale;
+ private Texture background = new Texture("sprites/background.png");
- Core.cameraScale = baseCameraScale;
- Effects.setEffectProvider((name, color, x, y, rotation) -> {
- if(Settings.getBool("effects")){
- Rectangle view = rect.setSize(camera.viewportWidth, camera.viewportHeight)
- .setCenter(camera.position.x, camera.position.y);
- Rectangle pos = rect2.setSize(name.size).setCenter(x, y);
- if(view.overlaps(pos)){
- new EffectEntity(name, color, rotation).set(x, y).add(effectGroup);
- }
- }
- });
+ private Rectangle rect = new Rectangle(), rect2 = new Rectangle();
+ private Vector2 avgPosition = new Translator();
+ private Vector2 tmpVector1 = new Translator();
+ private Vector2 tmpVector2 = new Translator();
- Cursors.cursorScaling = 3;
- Cursors.outlineColor = Color.valueOf("444444");
- Cursors.arrow = Cursors.loadCursor("cursor");
- Cursors.hand = Cursors.loadCursor("hand");
- Cursors.ibeam = Cursors.loadCursor("ibar");
+ private BlockRenderer blocks = new BlockRenderer();
+ private MinimapRenderer minimap = new MinimapRenderer();
+ private OverlayRenderer overlays = new OverlayRenderer();
+ private FogRenderer fog = new FogRenderer();
- clearColor = Hue.lightness(0.4f);
- clearColor.a = 1f;
+ public Renderer(){
+ pixelate = true;
+ Lines.setCircleVertices(14);
- background.setWrap(TextureWrap.Repeat, TextureWrap.Repeat);
- }
+ Shaders.init();
- @Override
- public void init(){
- pixelate = Settings.getBool("pixelate");
- int scale = Settings.getBool("pixelate") ? Core.cameraScale : 1;
-
- shadowSurface = Graphics.createSurface(scale);
- shieldSurface = Graphics.createSurface(scale);
- indicatorSurface = Graphics.createSurface(scale);
- pixelSurface = Graphics.createSurface(scale);
- }
+ Core.cameraScale = baseCameraScale;
+ Effects.setEffectProvider((effect, color, x, y, rotation, data) -> {
+ if(effect == Fx.none) return;
+ if(Settings.getBool("effects")){
+ Rectangle view = rect.setSize(camera.viewportWidth, camera.viewportHeight)
+ .setCenter(camera.position.x, camera.position.y);
+ Rectangle pos = rect2.setSize(effect.size).setCenter(x, y);
- public void setPixelate(boolean pixelate){
- this.pixelate = pixelate;
- }
+ if(view.overlaps(pos)){
- @Override
- public void update(){
+ if(!(effect instanceof GroundEffect)){
+ EffectEntity entity = Pooling.obtain(EffectEntity.class);
+ entity.effect = effect;
+ entity.color = color;
+ entity.rotation = rotation;
+ entity.data = data;
+ entity.id++;
+ entity.set(x, y);
+ if(data instanceof BaseEntity){
+ entity.setParent((BaseEntity) data);
+ }
+ threads.runGraphics(() -> effectGroup.add(entity));
+ }else{
+ GroundEffectEntity entity = Pooling.obtain(GroundEffectEntity.class);
+ entity.effect = effect;
+ entity.color = color;
+ entity.rotation = rotation;
+ entity.id++;
+ entity.data = data;
+ entity.set(x, y);
+ threads.runGraphics(() -> groundEffectGroup.add(entity));
+ }
+ }
+ }
+ });
- if(Core.cameraScale != targetscale){
- float targetzoom = (float) Core.cameraScale / targetscale;
- camera.zoom = Mathf.lerpDelta(camera.zoom, targetzoom, 0.2f);
+ Cursors.cursorScaling = 3;
+ Cursors.outlineColor = Color.valueOf("444444");
- if(Mathf.in(camera.zoom, targetzoom, 0.005f)){
- camera.zoom = 1f;
- Graphics.setCameraScale(targetscale);
- control.input().resetCursor();
- }
- }else{
- camera.zoom = Mathf.lerpDelta(camera.zoom, 1f, 0.2f);
- }
+ Cursors.arrow = Cursors.loadCursor("cursor");
+ Cursors.hand = Cursors.loadCursor("hand");
+ Cursors.ibeam = Cursors.loadCursor("ibar");
+ Cursors.restoreCursor();
+ Cursors.loadCustom("drill");
+ Cursors.loadCustom("unload");
- if(state.is(State.menu)){
- clearScreen();
- }else{
- boolean smoothcam = Settings.getBool("smoothcam");
+ clearColor = Hue.lightness(0.4f);
+ clearColor.a = 1f;
- if(world.getCore() == null || world.getCore().block() == ProductionBlocks.core){
- if(!smoothcam){
- setCamera(player.x, player.y);
- }else{
- smoothCamera(player.x, player.y, mobile ? 0.3f : 0.14f);
- }
- }else{
- smoothCamera(world.getCore().worldx(), world.getCore().worldy(), 0.4f);
- }
-
- if(Settings.getBool("pixelate"))
- limitCamera(4f, player.x, player.y);
-
- float prex = camera.position.x, prey = camera.position.y;
- updateShake(0.75f);
- float prevx = camera.position.x, prevy = camera.position.y;
- clampCamera(-tilesize / 2f, -tilesize / 2f + 1, world.width() * tilesize - tilesize / 2f, world.height() * tilesize - tilesize / 2f);
-
- float deltax = camera.position.x - prex, deltay = camera.position.y - prey;
-
- if(mobile){
- player.x += camera.position.x - prevx;
- player.y += camera.position.y - prevy;
- }
-
- float lastx = camera.position.x, lasty = camera.position.y;
-
- if(snapCamera && smoothcam && Settings.getBool("pixelate")){
- camera.position.set((int) camera.position.x, (int) camera.position.y, 0);
- }
-
- if(Gdx.graphics.getHeight() / Core.cameraScale % 2 == 1){
- camera.position.add(0, -0.5f, 0);
- }
-
- if(Gdx.graphics.getWidth() / Core.cameraScale % 2 == 1){
- camera.position.add(-0.5f, 0, 0);
- }
-
- draw();
-
- camera.position.set(lastx - deltax, lasty - deltay, 0);
-
- if(debug && !ui.chatfrag.chatOpen())
- record(); //this only does something if GdxGifRecorder is on the class path, which it usually isn't
- }
- }
-
- @Override
- public void draw(){
- camera.update();
-
- clearScreen(clearColor);
-
- batch.setProjectionMatrix(camera.combined);
-
- if(pixelate)
- Graphics.surface(pixelSurface, false);
- else
- batch.begin();
-
- //clears shield surface
- Graphics.surface(shieldSurface);
- Graphics.surface();
-
- drawPadding();
-
- blocks.drawFloor();
- blocks.processBlocks();
- blocks.drawBlocks(false);
-
- Graphics.shader(Shaders.outline, false);
- Entities.draw(enemyGroup);
- Entities.draw(playerGroup, p -> !p.isAndroid);
- Graphics.shader();
-
- Entities.draw(Entities.defaultGroup());
-
- blocks.drawBlocks(true);
-
- Graphics.shader(Shaders.outline, false);
- Entities.draw(playerGroup, p -> p.isAndroid);
- Graphics.shader();
-
- Entities.draw(bulletGroup);
- Entities.draw(effectGroup);
-
- drawShield();
-
- drawOverlay();
-
- if(Settings.getBool("indicators") && showUI){
- drawEnemyMarkers();
- }
-
- if(pixelate)
- Graphics.flushSurface();
-
- drawPlayerNames();
-
- batch.end();
- }
-
- @Override
- public void resize(int width, int height){
- super.resize(width, height);
- control.input().resetCursor();
- camera.position.set(player.x, player.y, 0);
- }
-
- @Override
- public void dispose() {
- background.dispose();
- }
-
- public void clearTiles(){
- blocks.clearTiles();
- }
-
- void drawPadding(){
- float vw = world.width() * tilesize;
- float cw = camera.viewportWidth * camera.zoom;
- float ch = camera.viewportHeight * camera.zoom;
- if(vw < cw){
- batch.draw(background,
- camera.position.x + vw/2,
- Mathf.round(camera.position.y - ch/2, tilesize),
- (cw - vw) /2,
- ch + tilesize,
- 0, 0,
- ((cw - vw) / 2 / tilesize), -ch / tilesize + 1);
-
- batch.draw(background,
- camera.position.x - vw/2,
- Mathf.round(camera.position.y - ch/2, tilesize),
- -(cw - vw) /2,
- ch + tilesize,
- 0, 0,
- -((cw - vw) / 2 / tilesize), -ch / tilesize + 1);
- }
- }
-
- void drawPlayerNames(){
- GlyphLayout layout = Pools.obtain(GlyphLayout.class);
-
- Draw.tscl(0.25f/2);
- for(Player player : playerGroup.all()){
- if(!player.isLocal && !player.isDead()){
- layout.setText(Core.font, player.name);
- Draw.color(0f, 0f, 0f, 0.3f);
- Draw.rect("blank", player.getDrawPosition().x, player.getDrawPosition().y + 8 - layout.height/2, layout.width + 2, layout.height + 2);
- Draw.color();
- Draw.tcolor(player.getColor());
- Draw.text(player.name, player.getDrawPosition().x, player.getDrawPosition().y + 8);
-
- if(player.isAdmin){
- Draw.color(player.getColor());
- float s = 3f;
- Draw.rect("icon-admin-small", player.getDrawPosition().x + layout.width/2f + 2 + 1, player.getDrawPosition().y + 7f, s, s);
- }
- Draw.reset();
- }
- }
- Pools.free(layout);
- Draw.tscl(fontscale);
+ background.setWrap(TextureWrap.Repeat, TextureWrap.Repeat);
}
- void drawEnemyMarkers(){
- Graphics.surface(indicatorSurface);
- Draw.color(Color.RED);
+ @Override
+ public void init(){
+ int scale = Core.cameraScale;
- for(Enemy enemy : enemyGroup.all()) {
+ effectSurface = Graphics.createSurface(scale);
+ pixelSurface = Graphics.createSurface(scale);
+ }
- if (rect.setSize(camera.viewportWidth, camera.viewportHeight).setCenter(camera.position.x, camera.position.y)
- .overlaps(enemy.hitbox.getRect(enemy.x, enemy.y))) {
- continue;
- }
+ @Override
+ public void update(){
- float angle = Angles.angle(camera.position.x, camera.position.y, enemy.x, enemy.y);
- float tx = Angles.trnsx(angle, Unit.dp.scl(20f));
- float ty = Angles.trnsy(angle, Unit.dp.scl(20f));
- Draw.rect("enemyarrow", camera.position.x + tx, camera.position.y + ty, angle);
- }
+ if(Core.cameraScale != targetscale){
+ float targetzoom = (float) Core.cameraScale / targetscale;
+ camera.zoom = Mathf.lerpDelta(camera.zoom, targetzoom, 0.2f);
- Draw.color();
- Draw.alpha(0.4f);
- Graphics.flushSurface();
- Draw.color();
- }
+ if(Mathf.in(camera.zoom, targetzoom, 0.005f)){
+ camera.zoom = 1f;
+ Graphics.setCameraScale(targetscale);
+ for(Player player : players){
+ control.input(player.playerIndex).resetCursor();
+ }
+ }
+ }else{
+ camera.zoom = Mathf.lerpDelta(camera.zoom, 1f, 0.2f);
+ }
- void drawShield(){
- if(shieldGroup.size() == 0 && shieldDraws.size == 0) return;
-
- Graphics.surface(renderer.shieldSurface, false);
- Draw.color(Color.ROYAL);
- Entities.draw(shieldGroup);
- for(Callable c : shieldDraws){
- c.run();
- }
- Draw.reset();
- Graphics.surface();
-
- for(int i = 0; i < shieldHits.size / 3; i++){
- float time = shieldHits.get(i * 3 + 2);
+ if(state.is(State.menu)){
+ Graphics.clear(Color.BLACK);
+ }else{
+ Vector2 position = averagePosition();
- time += Timers.delta() / shieldHitDuration;
- shieldHits.set(i * 3 + 2, time);
+ if(!mobile){
+ setCamera(position.x + 0.0001f, position.y + 0.0001f);
+ }
- if(time >= 1f){
- shieldHits.removeRange(i * 3, i * 3 + 2);
- i--;
- }
- }
+ clampCamera(-tilesize / 2f, -tilesize / 2f + 1, world.width() * tilesize - tilesize / 2f, world.height() * tilesize - tilesize / 2f);
- Texture texture = shieldSurface.texture();
- Shaders.shield.color.set(Color.SKY);
+ float prex = camera.position.x, prey = camera.position.y;
+ updateShake(0.75f);
- Tmp.tr2.setRegion(texture);
- Shaders.shield.region = Tmp.tr2;
- Shaders.shield.hits = shieldHits;
-
- if(Shaders.shield.isFallback){
- Draw.color(1f, 1f, 1f, 0.3f);
- Shaders.outline.color = Color.SKY;
- Shaders.outline.region = Tmp.tr2;
- }
+ float deltax = camera.position.x - prex, deltay = camera.position.y - prey;
+ float lastx = camera.position.x, lasty = camera.position.y;
- Graphics.end();
- Graphics.shader(Shaders.shield.isFallback ? Shaders.outline : Shaders.shield);
- Graphics.setScreen();
+ if(snapCamera){
+ camera.position.set((int) camera.position.x, (int) camera.position.y, 0);
+ }
- Core.batch.draw(texture, 0, Gdx.graphics.getHeight(), Gdx.graphics.getWidth(), -Gdx.graphics.getHeight());
+ if(Gdx.graphics.getHeight() / Core.cameraScale % 2 == 1){
+ camera.position.add(0, -0.5f, 0);
+ }
- Graphics.shader();
- Graphics.end();
- Graphics.beginCam();
-
- Draw.color();
- shieldDraws.clear();
- }
+ if(Gdx.graphics.getWidth() / Core.cameraScale % 2 == 1){
+ camera.position.add(-0.5f, 0, 0);
+ }
- public BlockRenderer getBlocks() {
- return blocks;
- }
+ draw();
- public void addShieldHit(float x, float y){
- shieldHits.addAll(x, y, 0f);
- }
+ camera.position.set(lastx - deltax, lasty - deltay, 0);
+ }
- public void addShield(Callable call){
- shieldDraws.add(call);
- }
+ if(debug && !ui.chatfrag.chatOpen()){
+ renderer.record(); //this only does something if GdxGifRecorder is on the class path, which it usually isn't
+ }
+ }
- void drawOverlay(){
+ @Override
+ public void draw(){
+ camera.update();
- //draw tutorial placement point
- if(world.getMap().name.equals("tutorial") && control.tutorial().showBlock()){
- int x = world.getCore().x + control.tutorial().getPlacePoint().x;
- int y = world.getCore().y + control.tutorial().getPlacePoint().y;
- int rot = control.tutorial().getPlaceRotation();
+ Graphics.clear(clearColor);
- Lines.stroke(1f);
- Draw.color(Color.YELLOW);
- Lines.square(x * tilesize, y * tilesize, tilesize / 2f + Mathf.sin(Timers.time(), 4f, 1f));
+ batch.setProjectionMatrix(camera.combined);
- Draw.color(Color.ORANGE);
- Lines.stroke(2f);
- if(rot != -1){
- Lines.lineAngle(x * tilesize, y * tilesize, rot * 90, 6);
- }
- Draw.reset();
- }
+ Graphics.surface(pixelSurface, false);
- //draw config selected block
- if(ui.configfrag.isShown()){
- Tile tile = ui.configfrag.getSelectedTile();
- Draw.color(Colors.get("accent"));
- Lines.stroke(1f);
- Lines.square(tile.drawx(), tile.drawy(),
- tile.block().width * tilesize / 2f + 1f);
- Draw.reset();
- }
-
- int tilex = control.input().getBlockX();
- int tiley = control.input().getBlockY();
-
- if(mobile){
- Vector2 vec = Graphics.world(Gdx.input.getX(0), Gdx.input.getY(0));
- tilex = Mathf.scl2(vec.x, tilesize);
- tiley = Mathf.scl2(vec.y, tilesize);
- }
+ drawPadding();
- InputHandler input = control.input();
+ blocks.drawFloor();
- //draw placement box
- if((input.recipe != null && state.inventory.hasItems(input.recipe.requirements) && (!ui.hasMouse() || mobile)
- && control.input().drawPlace())){
+ drawAndInterpolate(groundEffectGroup, e -> e instanceof BelowLiquidTrait);
+ drawAndInterpolate(puddleGroup);
+ drawAndInterpolate(groundEffectGroup, e -> !(e instanceof BelowLiquidTrait));
- input.placeMode.draw(control.input().getBlockX(), control.input().getBlockY(),
- control.input().getBlockEndX(), control.input().getBlockEndY());
+ blocks.processBlocks();
+ blocks.drawBlocks(Layer.block);
- Lines.stroke(1f);
- Draw.color(Color.SCARLET);
- for(SpawnPoint spawn : world.getSpawns()){
- Lines.dashCircle(spawn.start.worldx(), spawn.start.worldy(), enemyspawnspace);
- }
+ Graphics.shader(Shaders.blockbuild, false);
+ blocks.drawBlocks(Layer.placement);
+ Graphics.shader();
- if(world.getCore() != null) {
- Draw.color(Color.LIME);
- Lines.poly(world.getSpawnX(), world.getSpawnY(), 4, 6f, Timers.time() * 2f);
- }
-
- if(input.breakMode == PlaceMode.holdDelete)
- input.breakMode.draw(tilex, tiley, 0, 0);
-
- }else if(input.breakMode.delete && control.input().drawPlace()
- && (input.recipe == null || !state.inventory.hasItems(input.recipe.requirements))
- && (input.placeMode.delete || input.breakMode.both || !mobile)){
+ blocks.drawBlocks(Layer.overlay);
- if(input.breakMode == PlaceMode.holdDelete)
- input.breakMode.draw(tilex, tiley, 0, 0);
- else
- input.breakMode.draw(control.input().getBlockX(), control.input().getBlockY(),
- control.input().getBlockEndX(), control.input().getBlockEndY());
- }
+ if(itemGroup.size() > 0){
+ Shaders.outline.color.set(Team.none.color);
- if(ui.toolfrag.confirming){
- ToolFragment t = ui.toolfrag;
- PlaceMode.areaDelete.draw(t.px, t.py, t.px2, t.py2);
- }
-
- Draw.reset();
+ Graphics.beginShaders(Shaders.outline);
+ drawAndInterpolate(itemGroup);
+ Graphics.endShaders();
+ }
- //draw selected block bars and info
- if(input.recipe == null && !ui.hasMouse()){
- Tile tile = world.tileWorld(Graphics.mouseWorld().x, Graphics.mouseWorld().y);
+ drawAllTeams(false);
- if(tile != null && tile.block() != Blocks.air){
- Tile target = tile;
- if(tile.isLinked())
- target = tile.getLinked();
+ blocks.skipLayer(Layer.turret);
+ blocks.drawBlocks(Layer.laser);
- if(showBlockDebug && target.entity != null){
- Draw.color(Color.RED);
- Lines.crect(target.drawx(), target.drawy(), target.block().width * tilesize, target.block().height * tilesize);
- Vector2 v = new Vector2();
+ drawFlyerShadows();
+ drawAllTeams(true);
+ drawAndInterpolate(bulletGroup);
+ drawAndInterpolate(effectGroup);
- Draw.tcolor(Color.YELLOW);
- Draw.tscl(0.25f);
- Array