diff --git a/src/main/java/ru/windcorp/progressia/test/LayerDebug.java b/src/main/java/ru/windcorp/progressia/test/LayerDebug.java
new file mode 100755
index 0000000..f6509d5
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/test/LayerDebug.java
@@ -0,0 +1,260 @@
+/*
+ * Progressia
+ * Copyright (C) 2020-2021 Wind Corporation and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package ru.windcorp.progressia.test;
+
+import glm.vec._3.Vec3;
+import ru.windcorp.progressia.client.Client;
+import ru.windcorp.progressia.client.ClientState;
+import ru.windcorp.progressia.client.graphics.Colors;
+import ru.windcorp.progressia.client.graphics.backend.GraphicsBackend;
+import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface;
+import ru.windcorp.progressia.client.graphics.font.Font;
+import ru.windcorp.progressia.client.graphics.gui.DynamicLabel;
+import ru.windcorp.progressia.client.graphics.gui.GUILayer;
+import ru.windcorp.progressia.client.graphics.gui.Label;
+import ru.windcorp.progressia.client.graphics.gui.Group;
+import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign;
+import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical;
+import ru.windcorp.progressia.client.localization.Localizer;
+import ru.windcorp.progressia.client.localization.MutableString;
+import ru.windcorp.progressia.client.localization.MutableStringLocalized;
+import ru.windcorp.progressia.client.world.WorldRender;
+import ru.windcorp.progressia.common.Units;
+import ru.windcorp.progressia.common.util.dynstr.DynamicStrings;
+import ru.windcorp.progressia.server.Server;
+import ru.windcorp.progressia.server.ServerState;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+public class LayerDebug extends GUILayer {
+
+ private final List updateTriggers = new ArrayList<>();
+
+ public LayerDebug() {
+ super("LayerDebug", new LayoutAlign(0, 1, 5));
+ getRoot().addChild(new Group("Displays", new LayoutVertical(5)));
+
+ TestPlayerControls tpc = TestPlayerControls.getInstance();
+
+ addDynamicDisplay(
+ "FPSDisplay",
+ DynamicStrings.builder()
+ .addDyn(new MutableStringLocalized("LayerDebug.FPSDisplay"))
+ .addDyn(() -> FPS_RECORD.update(GraphicsInterface.getFPS()), 5, 1)
+ .addDyn(() -> GraphicsBackend.isFullscreen() ? " Fullscreen" : "")
+ .addDyn(() -> GraphicsBackend.isVSyncEnabled() ? " VSync" : "")
+ .buildSupplier()
+ );
+
+ addDynamicDisplay("TPSDisplay", LayerDebug::getTPS);
+
+ addDynamicDisplay(
+ "ChunkStatsDisplay",
+ DynamicStrings.builder()
+ .addDyn(new MutableStringLocalized("LayerDebug.ChunkStatsDisplay"))
+ .addDyn(() -> {
+ if (ClientState.getInstance() == null) {
+ return -1;
+ } else {
+ WorldRender world = ClientState.getInstance().getWorld();
+ return world.getChunks().size() - world.getPendingChunkUpdates();
+ }
+ }, 4)
+ .add('/')
+ .addDyn(() -> {
+ if (ClientState.getInstance() == null) {
+ return -1;
+ } else {
+ return ClientState.getInstance().getWorld().getPendingChunkUpdates();
+ }
+ }, 4)
+ .add('/')
+ .addDyn(() -> {
+ if (ServerState.getInstance() == null) {
+ return -1;
+ } else {
+ return ServerState.getInstance().getWorld().getChunks().size();
+ }
+ }, 4)
+ .buildSupplier()
+ );
+
+ addDynamicDisplay("PosDisplay", LayerDebug::getPos);
+
+ addDisplay("SelectedBlockDisplay", () -> tpc.isBlockSelected() ? ">" : " ", () -> tpc.getSelectedBlock().getId());
+ addDisplay("SelectedTileDisplay", () -> tpc.isBlockSelected() ? " " : ">", () -> tpc.getSelectedTile().getId());
+ addDisplay("PlacementModeHint", () -> "\u2B04");
+ }
+
+ private void addDisplay(String name, Supplier>... params) {
+ Font font = new Font().withColor(Colors.WHITE).deriveOutlined();
+ Label component = new Label(name, font, tmp_dynFormat("LayerDebug." + name, params));
+ getRoot().getChild(0).addChild(component);
+
+ for (Supplier> param : params) {
+ if (param == null) {
+ continue;
+ }
+
+ updateTriggers.add(new Runnable() {
+
+ private Object displayedValue;
+
+ @Override
+ public void run() {
+ Object newValue = param.get();
+ if (!Objects.equals(newValue, displayedValue)) {
+ component.update();
+ }
+ displayedValue = newValue;
+ }
+
+ });
+ }
+ }
+
+ private void addDynamicDisplay(String name, Supplier contents) {
+ Font font = new Font().withColor(Colors.WHITE).deriveOutlined();
+ DynamicLabel component = new DynamicLabel(name, font, contents, 128);
+ getRoot().getChild(0).addChild(component);
+ }
+
+ @Override
+ protected void doRender() {
+ updateTriggers.forEach(Runnable::run);
+ super.doRender();
+ }
+
+ private static class Averager {
+
+ private static final int DISPLAY_INERTIA = 32;
+ private static final double UPDATE_INTERVAL = Units.get(50.0, "ms");
+
+ private final double[] values = new double[DISPLAY_INERTIA];
+ private int size;
+ private int head;
+
+ private long lastUpdate;
+
+ public void add(double value) {
+ if (size == values.length) {
+ values[head] = value;
+ head++;
+ if (head == values.length)
+ head = 0;
+ } else {
+ values[size] = value;
+ size++;
+ }
+ }
+
+ public double average() {
+ double product = 1;
+
+ if (size == values.length) {
+ for (double d : values)
+ product *= d;
+ } else {
+ for (int i = 0; i < size; ++i)
+ product *= values[i];
+ }
+
+ return Math.pow(product, 1.0 / size);
+ }
+
+ public double update(double value) {
+ long now = (long) (GraphicsInterface.getTime() / UPDATE_INTERVAL);
+ if (lastUpdate != now) {
+ lastUpdate = now;
+ add(value);
+ }
+
+ return average();
+ }
+
+ }
+
+ private static final String[] CLOCK_CHARS = "\u2591\u2598\u259d\u2580\u2596\u258c\u259e\u259b\u2597\u259a\u2590\u259c\u2584\u2599\u259f\u2588"
+ .chars().mapToObj(c -> ((char) c) + "").toArray(String[]::new);
+
+ private static String getTPSClockChar() {
+ return CLOCK_CHARS[(int) (ServerState.getInstance().getUptimeTicks() % CLOCK_CHARS.length)];
+ }
+
+ private static final Averager FPS_RECORD = new Averager();
+ private static final Averager TPS_RECORD = new Averager();
+
+ private static final Supplier TPS_STRING = DynamicStrings.builder()
+ .addDyn(new MutableStringLocalized("LayerDebug.TPSDisplay"))
+ .addDyn(() -> TPS_RECORD.update(ServerState.getInstance().getTPS()), 5, 1)
+ .add(' ')
+ .addDyn(LayerDebug::getTPSClockChar)
+ .buildSupplier();
+
+ private static final Supplier POS_STRING = DynamicStrings.builder()
+ .addDyn(new MutableStringLocalized("LayerDebug.PosDisplay"))
+ .addDyn(() -> ClientState.getInstance().getCamera().getLastAnchorPosition().x, 7, 1)
+ .addDyn(() -> ClientState.getInstance().getCamera().getLastAnchorPosition().y, 7, 1)
+ .addDyn(() -> ClientState.getInstance().getCamera().getLastAnchorPosition().z, 7, 1)
+ .buildSupplier();
+
+ private static CharSequence getTPS() {
+ Server server = ServerState.getInstance();
+ if (server == null)
+ return Localizer.getInstance().getValue("LayerDebug.TPSDisplay.NA");
+
+ return TPS_STRING.get();
+ }
+
+ private static CharSequence getPos() {
+ Client client = ClientState.getInstance();
+ if (client == null)
+ return Localizer.getInstance().getValue("LayerDebug.PosDisplay.NA.Client");
+
+ Vec3 pos = client.getCamera().getLastAnchorPosition();
+ if (Float.isNaN(pos.x)) {
+ return Localizer.getInstance().getValue("LayerDebug.PosDisplay.NA.Entity");
+ } else {
+ return POS_STRING.get();
+ }
+ }
+
+ private static MutableString tmp_dynFormat(String formatKey, Supplier>... suppliers) {
+ return new MutableStringLocalized(formatKey).apply(s -> {
+ Object[] args = new Object[suppliers.length];
+
+ for (int i = 0; i < suppliers.length; ++i) {
+ Supplier> supplier = suppliers[i];
+
+ Object value = supplier != null ? supplier.get() : "null";
+ if (!(value instanceof Number)) {
+ value = Objects.toString(value);
+ }
+
+ args[i] = value;
+ }
+
+ return String.format(s, args);
+ });
+ }
+
+}
diff --git a/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java b/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java
deleted file mode 100755
index faf9851..0000000
--- a/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java
+++ /dev/null
@@ -1,406 +0,0 @@
-/*
- * Progressia
- * Copyright (C) 2020-2021 Wind Corporation and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package ru.windcorp.progressia.test;
-
-import glm.vec._3.Vec3;
-import glm.vec._4.Vec4;
-import ru.windcorp.progressia.client.Client;
-import ru.windcorp.progressia.client.ClientState;
-import ru.windcorp.progressia.client.graphics.Colors;
-import ru.windcorp.progressia.client.graphics.backend.GraphicsBackend;
-import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface;
-import ru.windcorp.progressia.client.graphics.font.Font;
-import ru.windcorp.progressia.client.graphics.gui.DynamicLabel;
-import ru.windcorp.progressia.client.graphics.gui.GUILayer;
-import ru.windcorp.progressia.client.graphics.gui.Label;
-import ru.windcorp.progressia.client.graphics.gui.Group;
-import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign;
-import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical;
-import ru.windcorp.progressia.client.localization.Localizer;
-import ru.windcorp.progressia.client.localization.MutableString;
-import ru.windcorp.progressia.client.localization.MutableStringLocalized;
-import ru.windcorp.progressia.client.world.WorldRender;
-import ru.windcorp.progressia.common.Units;
-import ru.windcorp.progressia.common.util.dynstr.DynamicStrings;
-import ru.windcorp.progressia.server.Server;
-import ru.windcorp.progressia.server.ServerState;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Objects;
-import java.util.function.Supplier;
-
-public class LayerTestGUI extends GUILayer {
-
- public LayerTestGUI() {
- super("LayerTestGui", new LayoutAlign(0, 1, 5));
-
- Group group = new Group("ControlDisplays", new LayoutVertical(5));
-
- Vec4 color = Colors.WHITE;
- Font font = new Font().withColor(color).deriveOutlined();
-
- TestPlayerControls tpc = TestPlayerControls.getInstance();
-
- group.addChild(
- new Label(
- "CameraModeDisplay",
- font,
- tmp_dynFormat(
- "LayerTestGUI.CameraModeDisplay",
- ClientState.getInstance().getCamera()::getCurrentModeIndex
- )
- )
- );
-
- group.addChild(
- new Label(
- "LanguageDisplay",
- font,
- tmp_dynFormat("LayerTestGUI.LanguageDisplay", Localizer.getInstance()::getLanguage)
- )
- );
-
- group.addChild(
- new Label(
- "FullscreenDisplay",
- font,
- tmp_dynFormat("LayerTestGUI.IsFullscreen", GraphicsBackend::isFullscreen)
- )
- );
-
- group.addChild(
- new Label(
- "VSyncDisplay",
- font,
- tmp_dynFormat("LayerTestGUI.IsVSync", GraphicsBackend::isVSyncEnabled)
- )
- );
-
- group.addChild(
- new DynamicLabel(
- "FPSDisplay",
- font,
- DynamicStrings.builder()
- .addDyn(new MutableStringLocalized("LayerTestGUI.FPSDisplay"))
- .addDyn(() -> FPS_RECORD.update(GraphicsInterface.getFPS()), 5, 1)
- .buildSupplier(),
- 128
- )
- );
-
- group.addChild(
- new DynamicLabel(
- "TPSDisplay",
- font,
- LayerTestGUI::getTPS,
- 128
- )
- );
-
- group.addChild(
- new DynamicLabel(
- "ChunkStatsDisplay",
- font,
- DynamicStrings.builder()
- .addDyn(new MutableStringLocalized("LayerTestGUI.ChunkStatsDisplay"))
- .addDyn(() -> {
- if (ClientState.getInstance() == null) {
- return -1;
- } else {
- WorldRender world = ClientState.getInstance().getWorld();
- return world.getChunks().size() - world.getPendingChunkUpdates();
- }
- }, 4)
- .add('/')
- .addDyn(() -> {
- if (ClientState.getInstance() == null) {
- return -1;
- } else {
- return ClientState.getInstance().getWorld().getPendingChunkUpdates();
- }
- }, 4)
- .add('/')
- .addDyn(() -> {
- if (ServerState.getInstance() == null) {
- return -1;
- } else {
- return ServerState.getInstance().getWorld().getChunks().size();
- }
- }, 4)
- .buildSupplier(),
- 128
- )
- );
-
- group.addChild(
- new DynamicLabel(
- "PosDisplay",
- font,
- LayerTestGUI::getPos,
- 128
- )
- );
-
- group.addChild(
- new Label(
- "SelectedBlockDisplay",
- font,
- tmp_dynFormat(
- "LayerTestGUI.SelectedBlockDisplay",
- () -> tpc.isBlockSelected() ? ">" : " ",
- () -> tpc.getSelectedBlock().getId()
- )
- )
- );
- group.addChild(
- new Label(
- "SelectedTileDisplay",
- font,
- tmp_dynFormat(
- "LayerTestGUI.SelectedTileDisplay",
- () -> tpc.isBlockSelected() ? " " : ">",
- () -> tpc.getSelectedTile().getId()
- )
- )
- );
- group.addChild(
- new Label(
- "PlacementModeHint",
- font,
- new MutableStringLocalized("LayerTestGUI.PlacementModeHint").format("\u2B04")
- )
- );
-
- getRoot().addChild(group);
- }
-
- public Runnable getUpdateCallback() {
- Collection