diff --git a/README.md b/README.md index 4c6cbc5..23727aa 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ temperature mechanics and a parallelism-capable server. - GNU/Linux (x64, arm32 or arm64), Windows XP or later (x64 or x86) or MacOS (x64) - Java 8 or later - OpenGL 2.1 or later -- Probably at least 4 GiB RAM +- Probably about 0.5 GiB RAM - Less than 1 GiB of storage space See [Build Guide](docs/building/BuildGuide.md) for compilation requirements. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..7424966 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing Guidelines + +This document lists conventions adopted by Progressia developers. + +## git + +### Branches +Progressia repository contains a `master` branch and several "feature" branches. + +`master` is expected to contain a version of the game suitable for demonstration and forking/branching. Do not commit directly to `master` without OLEGSHA's approval. +- `master` must always correctly build without compiler warnings (see below). +- `master` must always pass all unit tests. +- `master` must always be able to launch successfully. +- `master` must always only contain working features. +- `master` should not contain excessive debug code. +- `master` must always have its code and filenames formatted (see below). + +"Feature" branches are branches dedicated to the development of a single feature. When the feature reaches completion the branch is merged into `master` and removed. Intermediate merges into `master` may occur when some fitting milestone is reached. Intermediate merges from `master` may be done as necessary. Merges between "feature" branches should generally be avoided. + +When beginning work on a new feature, create a new branch based on `master` (or on another "feature" branch if absolutely necessary). Use `all-small-with-dashes` to name the branch: `add-trees` or `rebalance-plastics` are good names. Do not fix unrelated bugs or work on unrelated features in a "feature" branch - create a new one, switch to an existing one or commit directly to `master` if the changes are small enough. + +"Feature" branches may not be formatted properly. Formatting is required before merging into `master` or other branches. + +### Commits +- Commits must leave the branch in a state that builds without compiler warnings (see below). +- Changes should be grouped in commits semantically. Avoid committing many small related changes in sequence; if necessary, wait and accumulate them. Avoid committing unrelated changes together; if necessary, split staged changes into several commits. This should normally result in about 1-2 commits for a day's work. +- Commit bulk changes (renaming, formatting, ...) separately. Don't ever commit whitespace changes outside formatting commits. +- Message format: + +``` +Short description of changes + +- Enumeration of changes + - Nest when appropriate +- Use dashes only +- List not needed for small commits +``` + +Example: + +``` +Changed packages for relations, renamed Face to ShapePart + +- Added BlockRelation as an abstract superclass to existing relations + - Must be given an absolute "up" direction before use +- Moved AbsFace, AbsRelation and BlockRelation to .world.rels +- Renamed Face to ShapePart to reduce confusion with AbsFace +``` + +- Only commit changes described in the commit message. Please double-check staged files before committing. +- Avoid merge conflicts. Pull before committing. +- Better sign commits than not. See: [git](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work), [GitHub](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification). + +## Code + +### Warnings +Make sure that all committed code contains no compiler warnings. This specifically includes unused imports, unused private members, missing `@Override`s and warnings related to generics. + +Warnings about unknown tokens in `@SuppressWarnings` are temporarily ignored. Please disable them in your IDE. + +### Code Style +Formatting code is important. + +- The format is specified within the files inside `/templates_and_presets/eclipse_ide`. Import the specifications into Eclipse or IntelliJ IDEA and use the IDEs' format feature. Alternatively format the code manually in accordance with existing files. +- Only use tabs for indentation. Never indent with spaces even when wrapping lines. This is to ensure that indentation does not break when tab width is different. +- Don't use `I` prefix for interfaces (not `IDoable` but `Doable`). +- Prioritize readability over compactness. Do not hesitate to use (very) long identifiers if they aid comprehension. +- Document all mathematics unless it is trivial, especially when using math notation for variable names. +- Use proper English when writing comments. Avoid boxes in comments. Use `//` for single-line comments. \ No newline at end of file diff --git a/src/main/java/ru/windcorp/progressia/ProgressiaLauncher.java b/src/main/java/ru/windcorp/progressia/ProgressiaLauncher.java index 4944f1c..c8935fc 100644 --- a/src/main/java/ru/windcorp/progressia/ProgressiaLauncher.java +++ b/src/main/java/ru/windcorp/progressia/ProgressiaLauncher.java @@ -40,6 +40,7 @@ public class ProgressiaLauncher { CrashReports.registerProvider(new OpenALContextProvider()); CrashReports.registerProvider(new ArgsContextProvider()); CrashReports.registerProvider(new LanguageContextProvider()); + CrashReports.registerProvider(new ScreenContextProvider()); // Analyzers CrashReports.registerAnalyzer(new OutOfMemoryAnalyzer()); diff --git a/src/main/java/ru/windcorp/progressia/client/ClientProxy.java b/src/main/java/ru/windcorp/progressia/client/ClientProxy.java index e9ad7d3..1d154e7 100644 --- a/src/main/java/ru/windcorp/progressia/client/ClientProxy.java +++ b/src/main/java/ru/windcorp/progressia/client/ClientProxy.java @@ -32,6 +32,7 @@ import ru.windcorp.progressia.common.resource.ResourceManager; import ru.windcorp.progressia.common.util.crash.CrashReports; import ru.windcorp.progressia.server.ServerState; import ru.windcorp.progressia.test.TestContent; +import ru.windcorp.progressia.test.TestMusicPlayer; public class ClientProxy implements Proxy { @@ -59,6 +60,8 @@ public class ClientProxy implements Proxy { ServerState.startServer(); ClientState.connectToLocalServer(); + + TestMusicPlayer.start(); } } diff --git a/src/main/java/ru/windcorp/progressia/client/audio/AudioManager.java b/src/main/java/ru/windcorp/progressia/client/audio/AudioManager.java index 0078b56..49f2312 100644 --- a/src/main/java/ru/windcorp/progressia/client/audio/AudioManager.java +++ b/src/main/java/ru/windcorp/progressia/client/audio/AudioManager.java @@ -23,6 +23,7 @@ import ru.windcorp.progressia.client.audio.backend.AudioReader; import ru.windcorp.progressia.client.audio.backend.Listener; import ru.windcorp.progressia.client.audio.backend.SoundType; import ru.windcorp.progressia.client.audio.backend.Speaker; +import ru.windcorp.progressia.common.resource.Resource; import static org.lwjgl.openal.AL11.*; import static org.lwjgl.openal.ALC10.*; @@ -40,7 +41,6 @@ public class AudioManager { private static List soundSpeakers = new ArrayList<>(SOUNDS_NUM); private static Speaker musicSpeaker; - private static ArrayList soundsBuffer = new ArrayList<>(); public static void initAL() { String defaultDeviceName = alcGetString( @@ -82,31 +82,19 @@ public class AudioManager { return speaker; } - private static SoundType findSoundType(String soundID) throws Exception { - for (SoundType s : soundsBuffer) { - if (s.getId().equals(soundID)) { - return s; - } - } - throw new Exception( - "ERROR: The selected sound is not loaded or" + - " not exists" - ); - } - - public static Speaker initSpeaker(String soundID) { + public static Speaker initSpeaker(SoundType st) { Speaker speaker = getLastSpeaker(); try { - findSoundType(soundID).initSpeaker(speaker); + st.initSpeaker(speaker); } catch (Exception ex) { throw new RuntimeException(); } return speaker; } - public static Speaker initMusicSpeaker(String soundID) { + public static Speaker initMusicSpeaker(SoundType st) { try { - findSoundType(soundID).initSpeaker(musicSpeaker); + st.initSpeaker(musicSpeaker); } catch (Exception ex) { throw new RuntimeException(); } @@ -120,11 +108,11 @@ public class AudioManager { } } - public static void loadSound(String path, String id, AudioFormat format) { + public static void loadSound(Resource resource, String id, AudioFormat format) { if (format == AudioFormat.MONO) { - soundsBuffer.add(AudioReader.readAsMono(path, id)); + AudioRegistry.getInstance().register(AudioReader.readAsMono(resource, id)); } else { - soundsBuffer.add(AudioReader.readAsStereo(path, id)); + AudioRegistry.getInstance().register(AudioReader.readAsStereo(resource, id)); } } diff --git a/src/main/java/ru/windcorp/progressia/client/audio/AudioRegistry.java b/src/main/java/ru/windcorp/progressia/client/audio/AudioRegistry.java new file mode 100644 index 0000000..3199622 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/audio/AudioRegistry.java @@ -0,0 +1,34 @@ +/* + * 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.client.audio; + +import ru.windcorp.progressia.client.audio.backend.SoundType; +import ru.windcorp.progressia.common.util.namespaces.NamespacedInstanceRegistry; + +public class AudioRegistry extends NamespacedInstanceRegistry { + + private static final AudioRegistry INSTANCE = new AudioRegistry(); + + /** + * @return the instance + */ + public static AudioRegistry getInstance() { + return INSTANCE; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/audio/AudioSystem.java b/src/main/java/ru/windcorp/progressia/client/audio/AudioSystem.java index d41f6d4..24cc2de 100644 --- a/src/main/java/ru/windcorp/progressia/client/audio/AudioSystem.java +++ b/src/main/java/ru/windcorp/progressia/client/audio/AudioSystem.java @@ -18,6 +18,8 @@ package ru.windcorp.progressia.client.audio; +import ru.windcorp.progressia.common.resource.ResourceManager; + public class AudioSystem { static public void initialize() { AudioManager.initAL(); @@ -28,7 +30,7 @@ public class AudioSystem { static void loadAudioData() { AudioManager.loadSound( - "assets/sounds/block_destroy_clap.ogg", + ResourceManager.getResource("assets/sounds/block_destroy_clap.ogg"), "Progressia:BlockDestroy", AudioFormat.MONO ); diff --git a/src/main/java/ru/windcorp/progressia/client/audio/Music.java b/src/main/java/ru/windcorp/progressia/client/audio/Music.java index 9be9cc3..031b58e 100644 --- a/src/main/java/ru/windcorp/progressia/client/audio/Music.java +++ b/src/main/java/ru/windcorp/progressia/client/audio/Music.java @@ -19,72 +19,37 @@ package ru.windcorp.progressia.client.audio; import glm.vec._3.Vec3; +import ru.windcorp.progressia.client.audio.backend.SoundType; import ru.windcorp.progressia.client.audio.backend.Speaker; -import ru.windcorp.progressia.common.util.namespaces.Namespaced; -public class Music extends Namespaced { - private Vec3 position = new Vec3(); - private Vec3 velocity = new Vec3(); - private float pitch = 1.0f; - private float gain = 1.0f; +public class Music + extends Sound { + + + + public Music(SoundType soundType, int timeLength, float pitch, float gain) { + super(soundType, timeLength, new Vec3(), new Vec3(), pitch, gain); + } + + public Music(SoundType soundType) { + super(soundType); + } + + public Music(String id, int timeLength, float pitch, float gain) { + super(id, timeLength, new Vec3(), new Vec3(), pitch, gain); + } public Music(String id) { super(id); } - public Music( - String id, - Vec3 position, - Vec3 velocity, - float pitch, - float gain - ) { - this(id); - this.position = position; - this.velocity = velocity; - this.pitch = pitch; - this.gain = gain; + @Override + protected Speaker initSpeaker() { + return AudioManager.initMusicSpeaker(soundType); } - - public void play(boolean loop) { - Speaker speaker = AudioManager.initMusicSpeaker(this.getId()); - speaker.setGain(gain); - speaker.setPitch(pitch); - speaker.setPosition(position); - speaker.setVelocity(velocity); - - if (loop) { - speaker.playLoop(); - } else { - speaker.play(); - } - } - - public void setGain(float gain) { - this.gain = gain; - } - - public void setPitch(float pitch) { - this.pitch = pitch; - } - - public void setVelocity(Vec3 velocity) { - this.velocity = velocity; - } - - public Vec3 getPosition() { - return position; - } - - public float getGain() { - return gain; - } - - public Vec3 getVelocity() { - return velocity; - } - - public float getPitch() { - return pitch; + + @Override + public void setPosition(Vec3 position) { + throw new UnsupportedOperationException(); } } diff --git a/src/main/java/ru/windcorp/progressia/client/audio/SoundEffect.java b/src/main/java/ru/windcorp/progressia/client/audio/Sound.java similarity index 65% rename from src/main/java/ru/windcorp/progressia/client/audio/SoundEffect.java rename to src/main/java/ru/windcorp/progressia/client/audio/Sound.java index d79c8b2..77f0c9e 100644 --- a/src/main/java/ru/windcorp/progressia/client/audio/SoundEffect.java +++ b/src/main/java/ru/windcorp/progressia/client/audio/Sound.java @@ -19,23 +19,30 @@ package ru.windcorp.progressia.client.audio; import glm.vec._3.Vec3; +import ru.windcorp.progressia.client.audio.backend.SoundType; import ru.windcorp.progressia.client.audio.backend.Speaker; -import ru.windcorp.progressia.common.util.namespaces.Namespaced; -public class SoundEffect - extends Namespaced { +public class Sound { - private Vec3 position = new Vec3(); - private Vec3 velocity = new Vec3(); - private float pitch = 1.0f; - private float gain = 1.0f; - - public SoundEffect(String id) { - super(id); + protected Vec3 position = new Vec3(0f, 0f, 0f); + protected Vec3 velocity = new Vec3(0f, 0f, 0f); + protected float pitch = 1.0f; + protected float gain = 1.0f; + protected int timeLength = 0; + + protected SoundType soundType; + + public Sound(SoundType soundType) { + this.soundType = soundType; } - public SoundEffect( + public Sound(String id) { + this(AudioRegistry.getInstance().get(id)); + } + + public Sound( String id, + int timeLength, Vec3 position, Vec3 velocity, float pitch, @@ -47,9 +54,28 @@ public class SoundEffect this.pitch = pitch; this.gain = gain; } + + public Sound( + SoundType soundType, + int timeLength, + Vec3 position, + Vec3 velocity, + float pitch, + float gain + ) { + this(soundType); + this.position = position; + this.velocity = velocity; + this.pitch = pitch; + this.gain = gain; + } + + protected Speaker initSpeaker() { + return AudioManager.initSpeaker(soundType); + } public void play(boolean loop) { - Speaker speaker = AudioManager.initSpeaker(this.getId()); + Speaker speaker = initSpeaker(); speaker.setGain(gain); speaker.setPitch(pitch); speaker.setPosition(position); @@ -93,4 +119,9 @@ public class SoundEffect public float getPitch() { return pitch; } + + public double getDuration() { + return soundType.getDuration(); + } + } diff --git a/src/main/java/ru/windcorp/progressia/client/audio/backend/AudioReader.java b/src/main/java/ru/windcorp/progressia/client/audio/backend/AudioReader.java index 2cbd1e2..9fa48e0 100644 --- a/src/main/java/ru/windcorp/progressia/client/audio/backend/AudioReader.java +++ b/src/main/java/ru/windcorp/progressia/client/audio/backend/AudioReader.java @@ -33,13 +33,11 @@ public class AudioReader { } // TODO fix converting from mono-stereo - private static SoundType readAsSpecified(String path, String id, int format) { + private static SoundType readAsSpecified(Resource resource, String id, int format) { IntBuffer channelBuffer = BufferUtils.createIntBuffer(1); IntBuffer rateBuffer = BufferUtils.createIntBuffer(1); - Resource res = ResourceManager.getResource(path); - - ShortBuffer rawAudio = decodeVorbis(res, channelBuffer, rateBuffer); + ShortBuffer rawAudio = decodeVorbis(resource, channelBuffer, rateBuffer); return new SoundType( id, @@ -49,12 +47,12 @@ public class AudioReader { ); } - public static SoundType readAsMono(String path, String id) { - return readAsSpecified(path, id, AL_FORMAT_MONO16); + public static SoundType readAsMono(Resource resource, String id) { + return readAsSpecified(resource, id, AL_FORMAT_MONO16); } - public static SoundType readAsStereo(String path, String id) { - return readAsSpecified(path, id, AL_FORMAT_STEREO16); + public static SoundType readAsStereo(Resource resource, String id) { + return readAsSpecified(resource, id, AL_FORMAT_STEREO16); } private static ShortBuffer decodeVorbis( diff --git a/src/main/java/ru/windcorp/progressia/client/audio/backend/SoundType.java b/src/main/java/ru/windcorp/progressia/client/audio/backend/SoundType.java index 92bd133..cdb1954 100644 --- a/src/main/java/ru/windcorp/progressia/client/audio/backend/SoundType.java +++ b/src/main/java/ru/windcorp/progressia/client/audio/backend/SoundType.java @@ -21,6 +21,9 @@ package ru.windcorp.progressia.client.audio.backend; import ru.windcorp.progressia.common.util.namespaces.Namespaced; import java.nio.ShortBuffer; + +import org.lwjgl.openal.AL10; + import static org.lwjgl.openal.AL11.*; public class SoundType extends Namespaced { @@ -29,6 +32,7 @@ public class SoundType extends Namespaced { private int sampleRate; private int format; private int audioBuffer; + private double duration; public SoundType( String id, @@ -46,9 +50,14 @@ public class SoundType extends Namespaced { private void createAudioBuffer() { this.audioBuffer = alGenBuffers(); alBufferData(audioBuffer, format, rawAudio, sampleRate); + duration = rawAudio.limit() / (double) sampleRate / (format == AL10.AL_FORMAT_STEREO16 ? 2 : 1); } public void initSpeaker(Speaker speaker) { speaker.setAudioData(audioBuffer); } -} + + public double getDuration() { + return duration; + } +} \ No newline at end of file diff --git a/src/main/java/ru/windcorp/progressia/client/audio/backend/Speaker.java b/src/main/java/ru/windcorp/progressia/client/audio/backend/Speaker.java index ea6b5ed..c649a6a 100644 --- a/src/main/java/ru/windcorp/progressia/client/audio/backend/Speaker.java +++ b/src/main/java/ru/windcorp/progressia/client/audio/backend/Speaker.java @@ -120,6 +120,7 @@ public class Speaker { } public void setAudioData(int audioData) { + stop(); this.audioData = audioData; alSourcei(this.sourceData, AL_BUFFER, audioData); } diff --git a/src/main/java/ru/windcorp/progressia/client/comms/controls/ControlTriggerLocalLambda.java b/src/main/java/ru/windcorp/progressia/client/comms/controls/ControlTriggerLocalLambda.java new file mode 100644 index 0000000..b2ae739 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/comms/controls/ControlTriggerLocalLambda.java @@ -0,0 +1,53 @@ +/* + * 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.client.comms.controls; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +import ru.windcorp.progressia.client.graphics.input.InputEvent; +import ru.windcorp.progressia.common.comms.controls.PacketControl; + +public class ControlTriggerLocalLambda extends ControlTriggerInputBased { + + private final Predicate predicate; + private final Consumer action; + + public ControlTriggerLocalLambda( + String id, + Predicate predicate, + Consumer action + ) { + super(id); + + this.predicate = predicate; + this.action = action; + } + + @Override + public PacketControl onInputEvent(InputEvent event) { + if (!predicate.test(event)) + return null; + + action.accept(event); + + return null; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/comms/controls/ControlTriggers.java b/src/main/java/ru/windcorp/progressia/client/comms/controls/ControlTriggers.java index 24ca620..2c0d61d 100644 --- a/src/main/java/ru/windcorp/progressia/client/comms/controls/ControlTriggers.java +++ b/src/main/java/ru/windcorp/progressia/client/comms/controls/ControlTriggers.java @@ -142,6 +142,96 @@ public class ControlTriggers { predicates ); } + + // + // + /// + /// + // + // + // + // + // + // + // + // + // + + public static ControlTriggerInputBased localOf( + String id, + Consumer action, + Predicate predicate + ) { + return new ControlTriggerLocalLambda(id, predicate, action); + } + + public static ControlTriggerInputBased localOf( + String id, + Runnable action, + Predicate predicate + ) { + return localOf( + id, + input -> action.run(), + predicate + ); + } + + @SafeVarargs + public static ControlTriggerInputBased localOf( + String id, + Class inputType, + Consumer action, + Predicate... predicates + ) { + return localOf( + id, + createCheckedAction(inputType, action), + createCheckedCompoundPredicate(inputType, predicates) + ); + } + + @SafeVarargs + public static ControlTriggerInputBased localOf( + String id, + Class inputType, + Runnable action, + Predicate... predicates + ) { + return localOf( + id, + inputType, + input -> action.run(), + predicates + ); + } + + @SafeVarargs + public static ControlTriggerInputBased localOf( + String id, + Consumer action, + Predicate... predicates + ) { + return localOf( + id, + InputEvent.class, + action, + predicates + ); + } + + @SafeVarargs + public static ControlTriggerInputBased localOf( + String id, + Runnable action, + Predicate... predicates + ) { + return of( + id, + input -> action.run(), + predicates + ); + } private static BiConsumer createCheckedDataWriter( Class inputType, @@ -149,6 +239,13 @@ public class ControlTriggers { ) { return (inputEvent, control) -> dataWriter.accept(inputType.cast(inputEvent), control); } + + private static Consumer createCheckedAction( + Class inputType, + Consumer action + ) { + return inputEvent -> action.accept(inputType.cast(inputEvent)); + } private static Predicate createCheckedCompoundPredicate( Class inputType, diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/Colors.java b/src/main/java/ru/windcorp/progressia/client/graphics/Colors.java index 78aa79b..b37a6a8 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/Colors.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/Colors.java @@ -34,7 +34,13 @@ public class Colors { DEBUG_BLUE = toVector(0xFF0000FF), DEBUG_CYAN = toVector(0xFF00FFFF), DEBUG_MAGENTA = toVector(0xFFFF00FF), - DEBUG_YELLOW = toVector(0xFFFFFF00); + DEBUG_YELLOW = toVector(0xFFFFFF00), + + LIGHT_GRAY = toVector(0xFFCBCBD0), + BLUE = toVector(0xFF37A2E6), + HOVER_BLUE = toVector(0xFFC3E4F7), + DISABLED_GRAY = toVector(0xFFE5E5E5), + DISABLED_BLUE = toVector(0xFFB2D8ED); public static Vec4 toVector(int argb) { return toVector(argb, new Vec4()); diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/GUI.java b/src/main/java/ru/windcorp/progressia/client/graphics/GUI.java index fc099d2..bb4d85b 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/GUI.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/GUI.java @@ -24,6 +24,7 @@ import java.util.List; import com.google.common.eventbus.Subscribe; +import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface; import ru.windcorp.progressia.client.graphics.input.CursorEvent; import ru.windcorp.progressia.client.graphics.input.FrameResizeEvent; import ru.windcorp.progressia.client.graphics.input.InputEvent; @@ -57,15 +58,24 @@ public class GUI { } public static void addBottomLayer(Layer layer) { - modify(layers -> layers.add(layer)); + modify(layers -> { + layers.add(layer); + layer.onAdded(); + }); } public static void addTopLayer(Layer layer) { - modify(layers -> layers.add(0, layer)); + modify(layers -> { + layers.add(0, layer); + layer.onAdded(); + }); } public static void removeLayer(Layer layer) { - modify(layers -> layers.remove(layer)); + modify(layers -> { + layers.remove(layer); + layer.onRemoved(); + }); } private static void modify(LayerStackModification mod) { @@ -78,12 +88,33 @@ public class GUI { public static void render() { synchronized (LAYERS) { - MODIFICATION_QUEUE.forEach(action -> action.affect(LAYERS)); - MODIFICATION_QUEUE.clear(); - + + if (!MODIFICATION_QUEUE.isEmpty()) { + MODIFICATION_QUEUE.forEach(action -> action.affect(LAYERS)); + MODIFICATION_QUEUE.clear(); + + boolean isMouseCurrentlyCaptured = GraphicsInterface.isMouseCaptured(); + Layer.CursorPolicy policy = Layer.CursorPolicy.REQUIRE; + + for (Layer layer : LAYERS) { + Layer.CursorPolicy currentPolicy = layer.getCursorPolicy(); + + if (currentPolicy != Layer.CursorPolicy.INDIFFERENT) { + policy = currentPolicy; + break; + } + } + + boolean shouldCaptureMouse = (policy == Layer.CursorPolicy.FORBID); + if (shouldCaptureMouse != isMouseCurrentlyCaptured) { + GraphicsInterface.setMouseCaptured(shouldCaptureMouse); + } + } + for (int i = LAYERS.size() - 1; i >= 0; --i) { LAYERS.get(i).render(); } + } } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/Layer.java b/src/main/java/ru/windcorp/progressia/client/graphics/Layer.java index 2dbef4a..dfa72d5 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/Layer.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/Layer.java @@ -30,15 +30,52 @@ public abstract class Layer { private boolean hasInitialized = false; private final AtomicBoolean isValid = new AtomicBoolean(false); + + /** + * Represents various requests that a {@link Layer} can make regarding the + * presence of a visible cursor. The value of the highest layer that is not + * {@link #INDIFFERENT} is used. + */ + public static enum CursorPolicy { + /** + * Require that a cursor is visible. + */ + REQUIRE, + + /** + * The {@link Layer} should not affect the presence or absence of a + * visible cursor; lower layers should be consulted. + */ + INDIFFERENT, + + /** + * Forbid a visible cursor. + */ + FORBID + } + + private CursorPolicy cursorPolicy = CursorPolicy.INDIFFERENT; public Layer(String name) { this.name = name; } + + public String getName() { + return name; + } @Override public String toString() { return "Layer " + name; } + + public CursorPolicy getCursorPolicy() { + return cursorPolicy; + } + + public void setCursorPolicy(CursorPolicy cursorPolicy) { + this.cursorPolicy = cursorPolicy; + } void render() { GraphicsInterface.startNextLayer(); @@ -78,5 +115,13 @@ public abstract class Layer { protected int getHeight() { return GraphicsInterface.getFrameHeight(); } + + protected void onAdded() { + // Do nothing + } + + protected void onRemoved() { + // Do nothing + } } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsBackend.java b/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsBackend.java index c464d34..1635c7a 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsBackend.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsBackend.java @@ -18,11 +18,11 @@ package ru.windcorp.progressia.client.graphics.backend; -import static org.lwjgl.opengl.GL11.*; - import glm.vec._2.i.Vec2i; +import org.lwjgl.glfw.GLFWVidMode; import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL11.*; public class GraphicsBackend { @@ -38,9 +38,30 @@ public class GraphicsBackend { private static boolean faceCullingEnabled = false; + private static boolean isFullscreen = false; + private static boolean vSyncEnabled = false; + private static boolean isGLFWInitialized = false; + private static boolean isOpenGLInitialized = false; + private GraphicsBackend() { } + public static boolean isGLFWInitialized() { + return isGLFWInitialized; + } + + static void setGLFWInitialized(boolean isGLFWInitialized) { + GraphicsBackend.isGLFWInitialized = isGLFWInitialized; + } + + public static boolean isOpenGLInitialized() { + return isOpenGLInitialized; + } + + static void setOpenGLInitialized(boolean isOpenGLInitialized) { + GraphicsBackend.isOpenGLInitialized = isOpenGLInitialized; + } + public static void initialize() { startRenderThread(); } @@ -128,4 +149,61 @@ public class GraphicsBackend { faceCullingEnabled = useFaceCulling; } + public static boolean isFullscreen() { + return isFullscreen; + } + + public static boolean isVSyncEnabled() { + return vSyncEnabled; + } + + public static void setFullscreen() { + GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor()); + glfwSetWindowMonitor( + getWindowHandle(), + glfwGetPrimaryMonitor(), + 0, + 0, + vidmode.width(), + vidmode.height(), + 0); + isFullscreen = true; + } + + public static void setWindowed() { + GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor()); + glfwSetWindowMonitor( + getWindowHandle(), + 0, + (vidmode.width() - getFrameWidth()) / 2, + (vidmode.height() - getFrameHeight()) / 2, + getFrameWidth(), + getFrameHeight(), + 0); + isFullscreen = false; + } + + public static void setVSyncEnabled(boolean enable) { + glfwSwapInterval(enable ? 1 : 0); + vSyncEnabled = enable; + } + + public static int getRefreshRate() { + GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor()); + return vidmode.refreshRate(); + } + + public static boolean isMouseCaptured() { + return glfwGetInputMode(windowHandle, GLFW_CURSOR) == GLFW_CURSOR_DISABLED; + } + + public static void setMouseCaptured(boolean capture) { + int mode = capture ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL; + glfwSetInputMode(windowHandle, GLFW_CURSOR, mode); + + if (!capture) { + glfwSetCursorPos(windowHandle, FRAME_SIZE.x / 2.0, FRAME_SIZE.y / 2.0); + } + } + } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsInterface.java b/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsInterface.java index 9af984f..ffd0b49 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsInterface.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsInterface.java @@ -73,4 +73,21 @@ public class GraphicsInterface { GraphicsBackend.startNextLayer(); } + public static void makeFullscreen(boolean state) { + if (state) { + GraphicsBackend.setFullscreen(); + } else { + GraphicsBackend.setWindowed(); + } + GraphicsBackend.setVSyncEnabled(GraphicsBackend.isVSyncEnabled()); + } + + public static boolean isMouseCaptured() { + return GraphicsBackend.isMouseCaptured(); + } + + public static void setMouseCaptured(boolean capture) { + GraphicsBackend.setMouseCaptured(capture); + } + } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java b/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java index af95b19..9239150 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java @@ -50,6 +50,7 @@ class LWJGLInitializer { private static void initializeGLFW() { // TODO Do GLFW error handling: check glfwInit, setup error callback glfwInit(); + GraphicsBackend.setGLFWInitialized(true); } private static void createWindow() { @@ -64,10 +65,8 @@ class LWJGLInitializer { GraphicsBackend.setWindowHandle(handle); - glfwSetInputMode(handle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); - glfwMakeContextCurrent(handle); - glfwSwapInterval(0); + glfwSwapInterval(0); // TODO: remove after config system is added } private static void positionWindow() { @@ -87,6 +86,7 @@ class LWJGLInitializer { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); RenderTaskQueue.schedule(OpenGLObjectTracker::deleteEnqueuedObjects); + GraphicsBackend.setOpenGLInitialized(true); } private static void setupWindowCallbacks() { diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/flat/RenderTarget.java b/src/main/java/ru/windcorp/progressia/client/graphics/flat/RenderTarget.java index 0501897..70f5471 100755 --- a/src/main/java/ru/windcorp/progressia/client/graphics/flat/RenderTarget.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/flat/RenderTarget.java @@ -189,13 +189,10 @@ public class RenderTarget { public void addCustomRenderer(Renderable renderable) { assembleCurrentClipFromFaces(); - assembled.add( - new Clip( - maskStack, - getTransform(), - renderable - ) - ); + + float depth = this.depth--; + Mat4 transform = new Mat4().translate(0, 0, depth).mul(getTransform()); + assembled.add(new Clip(maskStack, transform, renderable)); } protected void addFaceToCurrentClip(ShapePart face) { diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/BasicButton.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/BasicButton.java new file mode 100644 index 0000000..cd30152 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/BasicButton.java @@ -0,0 +1,151 @@ +/* + * 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.client.graphics.gui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.function.Consumer; + +import org.lwjgl.glfw.GLFW; + +import com.google.common.eventbus.Subscribe; + +import ru.windcorp.progressia.client.graphics.font.Font; +import ru.windcorp.progressia.client.graphics.gui.event.ButtonEvent; +import ru.windcorp.progressia.client.graphics.gui.event.EnableEvent; +import ru.windcorp.progressia.client.graphics.gui.event.FocusEvent; +import ru.windcorp.progressia.client.graphics.gui.event.HoverEvent; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; +import ru.windcorp.progressia.client.graphics.input.KeyEvent; + +public abstract class BasicButton extends Component { + + private final Label label; + + private boolean isPressed = false; + private final Collection> actions = Collections.synchronizedCollection(new ArrayList<>()); + + public BasicButton(String name, String label, Font labelFont) { + super(name); + this.label = new Label(name + ".Label", labelFont, label); + + setLayout(new LayoutAlign(10)); + addChild(this.label); + + setFocusable(true); + reassembleAt(ARTrigger.HOVER, ARTrigger.FOCUS, ARTrigger.ENABLE); + + // Click triggers + addListener(KeyEvent.class, e -> { + if (e.isRepeat()) { + return false; + } else if ( + e.isLeftMouseButton() || + e.getKey() == GLFW.GLFW_KEY_SPACE || + e.getKey() == GLFW.GLFW_KEY_ENTER + ) { + setPressed(e.isPress()); + return true; + } else { + return false; + } + }); + + addListener(new Object() { + + // Release when losing focus + @Subscribe + public void onFocusChange(FocusEvent e) { + if (!e.getNewState()) { + setPressed(false); + } + } + + // Release when hover ends + @Subscribe + public void onHoverEnded(HoverEvent e) { + if (!e.isNowHovered()) { + setPressed(false); + } + } + + // Release when disabled + @Subscribe + public void onDisabled(EnableEvent e) { + if (!e.getComponent().isEnabled()) { + setPressed(false); + } + } + + // Trigger virtualClick when button is released + @Subscribe + public void onRelease(ButtonEvent.Release e) { + virtualClick(); + } + + }); + } + + public BasicButton(String name, String label) { + this(name, label, new Font()); + } + + public boolean isPressed() { + return isPressed; + } + + public void click() { + setPressed(true); + setPressed(false); + } + + public void setPressed(boolean isPressed) { + if (this.isPressed != isPressed) { + this.isPressed = isPressed; + + if (isPressed) { + takeFocus(); + } + + dispatchEvent(ButtonEvent.create(this, this.isPressed)); + } + } + + public BasicButton addAction(Consumer action) { + this.actions.add(Objects.requireNonNull(action, "action")); + return this; + } + + public boolean removeAction(Consumer action) { + return this.actions.remove(action); + } + + public void virtualClick() { + this.actions.forEach(action -> { + action.accept(this); + }); + } + + public Label getLabel() { + return label; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Button.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Button.java new file mode 100644 index 0000000..bbeb361 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Button.java @@ -0,0 +1,79 @@ +/* + * 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.client.graphics.gui; + +import glm.vec._4.Vec4; +import ru.windcorp.progressia.client.graphics.flat.RenderTarget; +import ru.windcorp.progressia.client.graphics.font.Font; +import ru.windcorp.progressia.client.graphics.Colors; + +public class Button extends BasicButton { + + public Button(String name, String label, Font labelFont) { + super(name, label, labelFont); + } + + public Button(String name, String label) { + this(name, label, new Font()); + } + + @Override + protected void assembleSelf(RenderTarget target) { + // Border + + Vec4 borderColor; + if (isPressed() || isHovered() || isFocused()) { + borderColor = Colors.BLUE; + } else { + borderColor = Colors.LIGHT_GRAY; + } + target.fill(getX(), getY(), getWidth(), getHeight(), borderColor); + + // Inside area + + if (isPressed()) { + // Do nothing + } else { + Vec4 backgroundColor; + if (isHovered() && isEnabled()) { + backgroundColor = Colors.HOVER_BLUE; + } else { + backgroundColor = Colors.WHITE; + } + target.fill(getX() + 2, getY() + 2, getWidth() - 4, getHeight() - 4, backgroundColor); + } + + // Change label font color + + if (isPressed()) { + getLabel().setFont(getLabel().getFont().withColor(Colors.WHITE)); + } else { + getLabel().setFont(getLabel().getFont().withColor(Colors.BLACK)); + } + } + + @Override + protected void postAssembleSelf(RenderTarget target) { + // Apply disable tint + + if (!isEnabled()) { + target.fill(getX(), getY(), getWidth(), getHeight(), Colors.toVector(0x88FFFFFF)); + } + } +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Checkbox.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Checkbox.java new file mode 100644 index 0000000..5f9d0df --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Checkbox.java @@ -0,0 +1,149 @@ +/* + * 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.client.graphics.gui; + +import glm.vec._2.i.Vec2i; +import glm.vec._4.Vec4; +import ru.windcorp.progressia.client.graphics.Colors; +import ru.windcorp.progressia.client.graphics.flat.RenderTarget; +import ru.windcorp.progressia.client.graphics.font.Font; +import ru.windcorp.progressia.client.graphics.font.Typefaces; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutHorizontal; + +public class Checkbox extends BasicButton { + + private class Tick extends Component { + + public Tick() { + super(Checkbox.this.getName() + ".Tick"); + + setPreferredSize(new Vec2i(Typefaces.getDefault().getLineHeight() * 3 / 2)); + } + + @Override + protected void assembleSelf(RenderTarget target) { + + int size = getPreferredSize().x; + int x = getX(); + int y = getY() + (getHeight() - size) / 2; + + // Border + + Vec4 borderColor; + if (Checkbox.this.isPressed() || Checkbox.this.isHovered() || Checkbox.this.isFocused()) { + borderColor = Colors.BLUE; + } else { + borderColor = Colors.LIGHT_GRAY; + } + target.fill(x, y, size, size, borderColor); + + // Inside area + + if (Checkbox.this.isPressed()) { + // Do nothing + } else { + Vec4 backgroundColor; + if (Checkbox.this.isHovered() && Checkbox.this.isEnabled()) { + backgroundColor = Colors.HOVER_BLUE; + } else { + backgroundColor = Colors.WHITE; + } + target.fill(x + 2, y + 2, size - 4, size - 4, backgroundColor); + } + + // "Tick" + + if (Checkbox.this.isChecked()) { + target.fill(x + 4, y + 4, size - 8, size - 8, Colors.BLUE); + } + } + + } + + private boolean checked; + + public Checkbox(String name, String label, Font labelFont, boolean check) { + super(name, label, labelFont); + this.checked = check; + + assert getChildren().size() == 1 : "Checkbox expects that BasicButton contains exactly one child"; + Component basicChild = getChild(0); + + Group group = new Group(getName() + ".LabelAndTick", new LayoutHorizontal(0, 10)); + removeChild(basicChild); + setLayout(new LayoutAlign(0, 0.5f, 10)); + group.setLayoutHint(basicChild.getLayoutHint()); + group.addChild(new Tick()); + group.addChild(basicChild); + addChild(group); + + addAction(b -> switchState()); + } + + public Checkbox(String name, String label, Font labelFont) { + this(name, label, labelFont, false); + } + + public Checkbox(String name, String label, boolean check) { + this(name, label, new Font(), check); + } + + public Checkbox(String name, String label) { + this(name, label, false); + } + + public void switchState() { + setChecked(!isChecked()); + } + + /** + * @return the checked + */ + public boolean isChecked() { + return checked; + } + + /** + * @param checked the checked to set + */ + public void setChecked(boolean checked) { + this.checked = checked; + } + + @Override + protected void assembleSelf(RenderTarget target) { + // Change label font color + + if (isPressed()) { + getLabel().setFont(getLabel().getFont().withColor(Colors.BLUE)); + } else { + getLabel().setFont(getLabel().getFont().withColor(Colors.BLACK)); + } + } + + @Override + protected void postAssembleSelf(RenderTarget target) { + // Apply disable tint + + if (!isEnabled()) { + target.fill(getX(), getY(), getWidth(), getHeight(), Colors.toVector(0x88FFFFFF)); + } + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Component.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Component.java index 4a131d8..bb6f24d 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Component.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Component.java @@ -19,18 +19,23 @@ package ru.windcorp.progressia.client.graphics.gui; import java.util.Collections; +import java.util.EnumMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import org.lwjgl.glfw.GLFW; import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; import glm.vec._2.i.Vec2i; import ru.windcorp.progressia.client.graphics.backend.InputTracker; import ru.windcorp.progressia.client.graphics.flat.RenderTarget; import ru.windcorp.progressia.client.graphics.gui.event.ChildAddedEvent; import ru.windcorp.progressia.client.graphics.gui.event.ChildRemovedEvent; +import ru.windcorp.progressia.client.graphics.gui.event.EnableEvent; import ru.windcorp.progressia.client.graphics.gui.event.FocusEvent; import ru.windcorp.progressia.client.graphics.gui.event.HoverEvent; import ru.windcorp.progressia.client.graphics.gui.event.ParentChangedEvent; @@ -61,6 +66,8 @@ public class Component extends Named { private Object layoutHint = null; private Layout layout = null; + + private boolean isEnabled = true; private boolean isFocusable = false; private boolean isFocused = false; @@ -285,9 +292,30 @@ public class Component extends Named { return this; } + /** + * Checks whether this component is focusable. A component needs to be + * focusable to become focused. A component that is focusable may not + * necessarily be ready to gain focus (see {@link #canGainFocusNow()}). + * + * @return {@code true} iff the component is focusable + * @see #canGainFocusNow() + */ public boolean isFocusable() { return isFocusable; } + + /** + * Checks whether this component can become focused at this moment. + *

+ * The implementation of this method in {@link Component} considers the + * component a focus candidate if it is both focusable and enabled. + * + * @return {@code true} iff the component can receive focus + * @see #isFocusable() + */ + public boolean canGainFocusNow() { + return isFocusable() && isEnabled(); + } public Component setFocusable(boolean focusable) { this.isFocusable = focusable; @@ -337,7 +365,7 @@ public class Component extends Named { return; } - if (component.isFocusable()) { + if (component.canGainFocusNow()) { setFocused(false); component.setFocused(true); return; @@ -379,7 +407,7 @@ public class Component extends Named { return; } - if (component.isFocusable()) { + if (component.canGainFocusNow()) { setFocused(false); component.setFocused(true); return; @@ -432,13 +460,52 @@ public class Component extends Named { return null; } + + public boolean isEnabled() { + return isEnabled; + } + + /** + * Enables or disables this component. An {@link EnableEvent} is dispatched + * if the state changes. + * + * @param enabled {@code true} to enable the component, {@code false} to + * disable the component + * @see #setEnabledRecursively(boolean) + */ + public void setEnabled(boolean enabled) { + if (this.isEnabled != enabled) { + if (isFocused() && isEnabled()) { + focusNext(); + } + + if (isEnabled()) { + setHovered(false); + } + + this.isEnabled = enabled; + dispatchEvent(new EnableEvent(this)); + } + } + + /** + * Enables or disables this component and all of its children recursively. + * + * @param enabled {@code true} to enable the components, {@code false} to + * disable the components + * @see #setEnabled(boolean) + */ + public void setEnabledRecursively(boolean enabled) { + setEnabled(enabled); + getChildren().forEach(c -> c.setEnabledRecursively(enabled)); + } public boolean isHovered() { return isHovered; } protected void setHovered(boolean isHovered) { - if (this.isHovered != isHovered) { + if (this.isHovered != isHovered && isEnabled()) { this.isHovered = isHovered; if (!isHovered && !getChildren().isEmpty()) { @@ -502,7 +569,7 @@ public class Component extends Named { } protected void handleInput(Input input) { - if (inputBus != null) { + if (inputBus != null && isEnabled()) { inputBus.dispatch(input); } } @@ -598,6 +665,17 @@ public class Component extends Named { } } + /** + * Schedules the reassembly to occur. + *

+ * This method is invoked in root components whenever a + * {@linkplain #requestReassembly() reassembly request} is made by one of + * its children. When creating the dedicated root component, override this + * method to perform any implementation-specific actions that will cause a + * reassembly as soon as possible. + *

+ * The default implementation of this method does nothing. + */ protected void handleReassemblyRequest() { // To be overridden } @@ -637,6 +715,135 @@ public class Component extends Named { protected void assembleChildren(RenderTarget target) { getChildren().forEach(child -> child.assemble(target)); } + + /* + * Automatic Reassembly + */ + + /** + * The various kinds of changes that may be used with + * {@link Component#reassembleAt(ARTrigger...)}. + */ + protected static enum ARTrigger { + /** + * Reassemble the component whenever its hover status changes, e.g. + * whenever the pointer enters or leaves its bounds. + */ + HOVER, + + /** + * Reassemble the component whenever it gains or loses focus. + *

+ * Component must be focusable to be able to gain focus. The + * component will not be reassembled unless + * {@link Component#setFocusable(boolean) setFocusable(true)} has been + * invoked. + */ + FOCUS, + + /** + * Reassemble the component whenever it is enabled or disabled. + */ + ENABLE + } + + /** + * All trigger objects (event listeners) that are currently registered with + * {@link #eventBus}. The field is {@code null} until the first trigger is + * installed. + */ + private Map autoReassemblyTriggerObjects = null; + + private Object createTriggerObject(ARTrigger type) { + switch (type) { + case HOVER: + return new Object() { + @Subscribe + public void onHoverChanged(HoverEvent e) { + requestReassembly(); + } + }; + case FOCUS: + return new Object() { + @Subscribe + public void onFocusChanged(FocusEvent e) { + requestReassembly(); + } + }; + case ENABLE: + return new Object() { + @Subscribe + public void onEnabled(EnableEvent e) { + requestReassembly(); + } + }; + default: + throw new NullPointerException("type"); + } + } + + /** + * Requests that {@link #requestReassembly()} is invoked on this component + * whenever any of the specified changes occur. Duplicate attempts to + * register the same trigger are silently ignored. + *

+ * {@code triggers} may be empty, which results in a no-op. It must not be + * {@code null}. + * + * @param triggers the {@linkplain ARTrigger triggers} to + * request reassembly with. + * @see #disableAutoReassemblyAt(ARTrigger...) + */ + protected synchronized void reassembleAt(ARTrigger... triggers) { + + Objects.requireNonNull(triggers, "triggers"); + if (triggers.length == 0) + return; + + if (autoReassemblyTriggerObjects == null) { + autoReassemblyTriggerObjects = new EnumMap<>(ARTrigger.class); + } + + for (ARTrigger trigger : triggers) { + if (!autoReassemblyTriggerObjects.containsKey(trigger)) { + Object triggerObject = createTriggerObject(trigger); + addListener(trigger); + autoReassemblyTriggerObjects.put(trigger, triggerObject); + } + } + + } + + /** + * Requests that {@link #requestReassembly()} is no longer invoked on this + * component whenever any of the specified changes occur. After a trigger is + * removed, it may be reinstalled with + * {@link #reassembleAt(ARTrigger...)}. Attempts to remove a + * nonexistant trigger are silently ignored. + *

+ * {@code triggers} may be empty, which results in a no-op. It must not be + * {@code null}. + * + * @param triggers the {@linkplain ARTrigger triggers} to remove + * @see #reassemblyAt(ARTrigger...) + */ + protected synchronized void disableAutoReassemblyAt(ARTrigger... triggers) { + + Objects.requireNonNull(triggers, "triggers"); + if (triggers.length == 0) + return; + + if (autoReassemblyTriggerObjects == null) + return; + + for (ARTrigger trigger : triggers) { + Object triggerObject = autoReassemblyTriggerObjects.remove(trigger); + if (triggerObject != null) { + removeListener(trigger); + } + } + + } // /** // * Returns a component that displays this component in its center. diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Group.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Group.java new file mode 100755 index 0000000..d8a8b23 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Group.java @@ -0,0 +1,28 @@ +/* + * 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.client.graphics.gui; + +public class Group extends Component { + + public Group(String name, Layout layout) { + super(name); + setLayout(layout); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Label.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Label.java index f7dbe33..4450f33 100755 --- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Label.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Label.java @@ -82,6 +82,11 @@ public class Label extends Component { public Font getFont() { return font; } + + public void setFont(Font font) { + this.font = font; + requestReassembly(); + } public String getCurrentText() { return currentText; @@ -96,11 +101,7 @@ public class Label extends Component { float startX = getX() + font.getAlign() * (getWidth() - currentSize.x); target.pushTransform( - new Mat4().identity().translate(startX, getY(), -1000) // TODO wtf - // is this - // magic - // <--- - .scale(2) + new Mat4().identity().translate(startX, getY(), 0).scale(2) ); target.addCustomRenderer(font.assemble(currentText, maxWidth)); diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Panel.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Panel.java old mode 100755 new mode 100644 index 88e10f1..90357ef --- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Panel.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Panel.java @@ -15,14 +15,66 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package ru.windcorp.progressia.client.graphics.gui; -public class Panel extends Component { +import java.util.Objects; +import glm.vec._4.Vec4; +import ru.windcorp.progressia.client.graphics.Colors; +import ru.windcorp.progressia.client.graphics.flat.RenderTarget; + +public class Panel extends Group { + + private Vec4 fill; + private Vec4 border; + + public Panel(String name, Layout layout, Vec4 fill, Vec4 border) { + super(name, layout); + + this.fill = Objects.requireNonNull(fill, "fill"); + this.border = border; + } + public Panel(String name, Layout layout) { - super(name); - setLayout(layout); + this(name, layout, Colors.WHITE, Colors.LIGHT_GRAY); + } + + /** + * @return the fill + */ + public Vec4 getFill() { + return fill; + } + + /** + * @param fill the fill to set + */ + public void setFill(Vec4 fill) { + this.fill = Objects.requireNonNull(fill, "fill"); + } + + /** + * @return the border + */ + public Vec4 getBorder() { + return border; + } + + /** + * @param border the border to set + */ + public void setBorder(Vec4 border) { + this.border = border; + } + + @Override + protected void assembleSelf(RenderTarget target) { + if (border == null) { + target.fill(getX(), getY(), getWidth(), getHeight(), fill); + } else { + target.fill(getX(), getY(), getWidth(), getHeight(), border); + target.fill(getX() + 2, getY() + 2, getWidth() - 4, getHeight() - 4, fill); + } } } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButton.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButton.java new file mode 100644 index 0000000..471efb6 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButton.java @@ -0,0 +1,205 @@ +/* + * 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.client.graphics.gui; + +import org.lwjgl.glfw.GLFW; + +import glm.vec._2.i.Vec2i; +import glm.vec._4.Vec4; +import ru.windcorp.progressia.client.graphics.Colors; +import ru.windcorp.progressia.client.graphics.flat.RenderTarget; +import ru.windcorp.progressia.client.graphics.font.Font; +import ru.windcorp.progressia.client.graphics.font.Typefaces; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutHorizontal; +import ru.windcorp.progressia.client.graphics.input.KeyEvent; + +public class RadioButton extends BasicButton { + + private class Tick extends Component { + + public Tick() { + super(RadioButton.this.getName() + ".Tick"); + + setPreferredSize(new Vec2i(Typefaces.getDefault().getLineHeight() * 3 / 2)); + } + + private void cross(RenderTarget target, int x, int y, int size, Vec4 color) { + target.fill(x + 4, y, size - 8, size, color); + target.fill(x + 2, y + 2, size - 4, size - 4, color); + target.fill(x, y + 4, size, size - 8, color); + } + + @Override + protected void assembleSelf(RenderTarget target) { + + int size = getPreferredSize().x; + int x = getX(); + int y = getY() + (getHeight() - size) / 2; + + // Border + + Vec4 borderColor; + if (RadioButton.this.isPressed() || RadioButton.this.isHovered() || RadioButton.this.isFocused()) { + borderColor = Colors.BLUE; + } else { + borderColor = Colors.LIGHT_GRAY; + } + cross(target, x, y, size, borderColor); + + // Inside area + + if (RadioButton.this.isPressed()) { + // Do nothing + } else { + Vec4 backgroundColor; + if (RadioButton.this.isHovered() && RadioButton.this.isEnabled()) { + backgroundColor = Colors.HOVER_BLUE; + } else { + backgroundColor = Colors.WHITE; + } + cross(target, x + 2, y + 2, size - 4, backgroundColor); + } + + // "Tick" + + if (RadioButton.this.isChecked()) { + cross(target, x + 4, y + 4, size - 8, Colors.BLUE); + } + } + + } + + private boolean checked; + + private RadioButtonGroup group = null; + + public RadioButton(String name, String label, Font labelFont, boolean check) { + super(name, label, labelFont); + this.checked = check; + + assert getChildren().size() == 1 : "RadioButton expects that BasicButton contains exactly one child"; + Component basicChild = getChild(0); + + Group group = new Group(getName() + ".LabelAndTick", new LayoutHorizontal(0, 10)); + removeChild(basicChild); + setLayout(new LayoutAlign(0, 0.5f, 10)); + group.setLayoutHint(basicChild.getLayoutHint()); + group.addChild(new Tick()); + group.addChild(basicChild); + addChild(group); + + addListener(KeyEvent.class, e -> { + if (e.isRelease()) return false; + + if (e.getKey() == GLFW.GLFW_KEY_LEFT || e.getKey() == GLFW.GLFW_KEY_UP) { + if (this.group != null) { + this.group.selectPrevious(); + this.group.getSelected().takeFocus(); + } + + return true; + } else if (e.getKey() == GLFW.GLFW_KEY_RIGHT || e.getKey() == GLFW.GLFW_KEY_DOWN) { + if (this.group != null) { + this.group.selectNext(); + this.group.getSelected().takeFocus(); + } + return true; + } + + return false; + }); + + addAction(b -> setChecked(true)); + } + + public RadioButton(String name, String label, Font labelFont) { + this(name, label, labelFont, false); + } + + public RadioButton(String name, String label, boolean check) { + this(name, label, new Font(), check); + } + + public RadioButton(String name, String label) { + this(name, label, false); + } + + /** + * @param group the group to set + */ + public RadioButton setGroup(RadioButtonGroup group) { + + if (this.group != null) { + group.selectNext(); + removeAction(group.listener); + group.buttons.remove(this); + group.getSelected(); // Clear reference if this was the only button in the group + } + + this.group = group; + + if (this.group != null) { + group.buttons.add(this); + addAction(group.listener); + } + + setChecked(false); + + return this; + } + + /** + * @return the checked + */ + public boolean isChecked() { + return checked; + } + + /** + * @param checked the checked to set + */ + public void setChecked(boolean checked) { + this.checked = checked; + + if (group != null) { + group.listener.accept(this); // Failsafe for manual invocations of setChecked() + } + } + + @Override + protected void assembleSelf(RenderTarget target) { + // Change label font color + + if (isPressed()) { + getLabel().setFont(getLabel().getFont().withColor(Colors.BLUE)); + } else { + getLabel().setFont(getLabel().getFont().withColor(Colors.BLACK)); + } + } + + @Override + protected void postAssembleSelf(RenderTarget target) { + // Apply disable tint + + if (!isEnabled()) { + target.fill(getX(), getY(), getWidth(), getHeight(), Colors.toVector(0x88FFFFFF)); + } + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButtonGroup.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButtonGroup.java new file mode 100644 index 0000000..3887018 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButtonGroup.java @@ -0,0 +1,119 @@ +/* + * 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.client.graphics.gui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public class RadioButtonGroup { + + private final Collection> actions = Collections.synchronizedCollection(new ArrayList<>()); + final List buttons = Collections.synchronizedList(new ArrayList<>()); + + private RadioButton selected = null; + + Consumer listener = b -> { + if (b instanceof RadioButton && ((RadioButton) b).isChecked() && buttons.contains(b)) { + select((RadioButton) b); + } + }; + + public RadioButtonGroup addAction(Consumer action) { + this.actions.add(Objects.requireNonNull(action, "action")); + return this; + } + + public boolean removeAction(Consumer action) { + return this.actions.remove(action); + } + + public List getButtons() { + return Collections.unmodifiableList(buttons); + } + + public synchronized RadioButton getSelected() { + if (!buttons.contains(selected)) { + selected = null; + } + return selected; + } + + public synchronized void select(RadioButton button) { + if (button != null && !buttons.contains(button)) { + throw new IllegalArgumentException("Button " + button + " is not in the group"); + } + + getSelected(); // Clear if invalid + + if (selected == button) { + return; // Terminate listener-setter recursion + } + + if (selected != null) { + selected.setChecked(false); + } + + selected = button; + + if (selected != null) { + selected.setChecked(true); + } + + actions.forEach(action -> action.accept(this)); + } + + public void selectNext() { + selectNeighbour(+1); + } + + public void selectPrevious() { + selectNeighbour(-1); + } + + private synchronized void selectNeighbour(int direction) { + if (getSelected() == null) { + if (buttons.isEmpty()) { + throw new IllegalStateException("Cannot select neighbour button: group empty"); + } + + select(buttons.get(0)); + } else { + RadioButton button; + int index = buttons.indexOf(selected); + + do { + index += direction; + + if (index >= buttons.size()) { + index = 0; + } else if (index < 0) { + index = buttons.size() - 1; + } + + button = buttons.get(index); + } while (button != getSelected() && !button.isEnabled()); + + select(button); + } + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/ButtonEvent.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/ButtonEvent.java new file mode 100644 index 0000000..071f06e --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/ButtonEvent.java @@ -0,0 +1,60 @@ +/* + * 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.client.graphics.gui.event; + +import ru.windcorp.progressia.client.graphics.gui.BasicButton; + +public class ButtonEvent extends ComponentEvent { + + public static class Press extends ButtonEvent { + public Press(BasicButton button) { + super(button, true); + } + } + + public static class Release extends ButtonEvent { + public Release(BasicButton button) { + super(button, false); + } + } + + private final boolean isPress; + + protected ButtonEvent(BasicButton button, boolean isPress) { + super(button); + this.isPress = isPress; + } + + public static ButtonEvent create(BasicButton button, boolean isPress) { + if (isPress) { + return new Press(button); + } else { + return new Release(button); + } + } + + public boolean isPress() { + return isPress; + } + + public boolean isRelease() { + return !isPress; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/EnableEvent.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/EnableEvent.java new file mode 100644 index 0000000..f56df2c --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/EnableEvent.java @@ -0,0 +1,11 @@ +package ru.windcorp.progressia.client.graphics.gui.event; + +import ru.windcorp.progressia.client.graphics.gui.Component; + +public class EnableEvent extends ComponentEvent { + + public EnableEvent(Component component) { + super(component); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/layout/LayoutFill.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/layout/LayoutFill.java new file mode 100644 index 0000000..c65fc4a --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/layout/LayoutFill.java @@ -0,0 +1,78 @@ +/* + * 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.client.graphics.gui.layout; + +import static java.lang.Math.max; + +import glm.vec._2.i.Vec2i; +import ru.windcorp.progressia.client.graphics.gui.Component; +import ru.windcorp.progressia.client.graphics.gui.Layout; + +public class LayoutFill implements Layout { + + private final int margin; + + public LayoutFill(int margin) { + this.margin = margin; + } + + public LayoutFill() { + this(0); + } + + @Override + public void layout(Component c) { + c.getChildren().forEach(child -> { + + int cWidth = c.getWidth() - 2 * margin; + int cHeight = c.getHeight() - 2 * margin; + + child.setBounds( + c.getX() + margin, + c.getY() + margin, + cWidth, + cHeight + ); + + }); + } + + @Override + public Vec2i calculatePreferredSize(Component c) { + Vec2i result = new Vec2i(0, 0); + + c.getChildren().stream() + .map(child -> child.getPreferredSize()) + .forEach(size -> { + result.x = max(size.x, result.x); + result.y = max(size.y, result.y); + }); + + result.x += 2 * margin; + result.y += 2 * margin; + + return result; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + margin + ")"; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/layout/LayoutGrid.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/layout/LayoutGrid.java index fa2cdfe..a164378 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/layout/LayoutGrid.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/layout/LayoutGrid.java @@ -97,16 +97,27 @@ public class LayoutGrid implements Layout { void setBounds(int column, int row, Component child, Component parent) { if (!isSummed) throw new IllegalStateException("Not summed yet"); + + int width, height; + + if (column == columns.length - 1) { + width = parent.getWidth() - margin - columns[column]; + } else { + width = columns[column + 1] - columns[column] - gap; + } + + if (row == rows.length - 1) { + height = parent.getHeight() - margin - rows[row]; + } else { + height = rows[row + 1] - rows[row] - gap; + } child.setBounds( parent.getX() + columns[column], - parent.getY() + rows[row], + parent.getY() + parent.getHeight() - (rows[row] + height), - (column != (columns.length - 1) ? (columns[column + 1] - columns[column] - gap) - : (parent.getWidth() - margin - columns[column])), - - (row != (rows.length - 1) ? (rows[row + 1] - rows[row] - gap) - : (parent.getHeight() - margin - rows[row])) + width, + height ); } } @@ -132,10 +143,9 @@ public class LayoutGrid implements Layout { GridDimensions grid = calculateGrid(c); grid.sum(); - int[] coords; for (Component child : c.getChildren()) { - coords = (int[]) child.getLayoutHint(); - grid.setBounds(coords[0], coords[1], child, c); + Vec2i coords = (Vec2i) child.getLayoutHint(); + grid.setBounds(coords.x, coords.y, child, c); } } } @@ -149,11 +159,10 @@ public class LayoutGrid implements Layout { private GridDimensions calculateGrid(Component parent) { GridDimensions result = new GridDimensions(); - int[] coords; for (Component child : parent.getChildren()) { - coords = (int[]) child.getLayoutHint(); - result.add(coords[0], coords[1], child.getPreferredSize()); + Vec2i coords = (Vec2i) child.getLayoutHint(); + result.add(coords.x, coords.y, child.getPreferredSize()); } return result; diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/menu/MenuLayer.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/menu/MenuLayer.java new file mode 100644 index 0000000..4fa155c --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/menu/MenuLayer.java @@ -0,0 +1,117 @@ +/* + * 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.client.graphics.gui.menu; + +import org.lwjgl.glfw.GLFW; + +import glm.vec._2.i.Vec2i; +import ru.windcorp.progressia.client.graphics.Colors; +import ru.windcorp.progressia.client.graphics.GUI; +import ru.windcorp.progressia.client.graphics.font.Font; +import ru.windcorp.progressia.client.graphics.gui.Component; +import ru.windcorp.progressia.client.graphics.gui.GUILayer; +import ru.windcorp.progressia.client.graphics.gui.Label; +import ru.windcorp.progressia.client.graphics.gui.Layout; +import ru.windcorp.progressia.client.graphics.gui.Panel; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutFill; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical; +import ru.windcorp.progressia.client.graphics.input.InputEvent; +import ru.windcorp.progressia.client.graphics.input.KeyEvent; +import ru.windcorp.progressia.client.graphics.input.bus.Input; +import ru.windcorp.progressia.client.localization.MutableString; +import ru.windcorp.progressia.client.localization.MutableStringLocalized; + +public class MenuLayer extends GUILayer { + + private final Component content; + private final Component background; + + private final Runnable closeAction = () -> { + GUI.removeLayer(this); + }; + + public MenuLayer(String name, Component content) { + super(name, new LayoutFill(0)); + + setCursorPolicy(CursorPolicy.REQUIRE); + + this.background = new Panel(name + ".Background", new LayoutAlign(10), Colors.toVector(0x66000000), null); + this.content = content; + + background.addChild(content); + getRoot().addChild(background); + } + + public MenuLayer(String name, Layout contentLayout) { + this(name, new Panel(name + ".Content", contentLayout)); + } + + public MenuLayer(String name) { + this(name, new LayoutVertical(20, 10)); + } + + public Component getContent() { + return content; + } + + public Component getBackground() { + return background; + } + + protected void addTitle() { + String translationKey = "Layer" + getName() + ".Title"; + MutableString titleText = new MutableStringLocalized(translationKey); + Font titleFont = new Font().deriveBold().withColor(Colors.BLACK).withAlign(0.5f); + + Label label = new Label(getName() + ".Title", titleFont, titleText); + getContent().addChild(label); + + Panel panel = new Panel(getName() + ".Title.Underscore", null, Colors.BLUE, null); + panel.setLayout(new LayoutFill() { + @Override + public Vec2i calculatePreferredSize(Component c) { + return new Vec2i(label.getPreferredSize().x + 40, 4); + } + }); + getContent().addChild(panel); + } + + protected Runnable getCloseAction() { + return closeAction; + } + + @Override + protected void handleInput(Input input) { + + if (!input.isConsumed()) { + InputEvent event = input.getEvent(); + + if (event instanceof KeyEvent) { + KeyEvent keyEvent = (KeyEvent) event; + if (keyEvent.isPress() && keyEvent.getKey() == GLFW.GLFW_KEY_ESCAPE) { + getCloseAction().run(); + } + } + } + + super.handleInput(input); + input.consume(); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java b/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java index c58b806..27cf84d 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java @@ -59,6 +59,8 @@ public class LayerWorld extends Layer { super("World"); this.client = client; this.inputBasedControls = new InputBasedControls(client); + + setCursorPolicy(CursorPolicy.FORBID); } @Override diff --git a/src/main/java/ru/windcorp/progressia/client/world/ChunkUpdateListener.java b/src/main/java/ru/windcorp/progressia/client/world/ChunkUpdateListener.java index 37ea0ba..da3d10d 100644 --- a/src/main/java/ru/windcorp/progressia/client/world/ChunkUpdateListener.java +++ b/src/main/java/ru/windcorp/progressia/client/world/ChunkUpdateListener.java @@ -19,6 +19,8 @@ package ru.windcorp.progressia.client.world; import glm.vec._3.i.Vec3i; +import ru.windcorp.progressia.common.util.VectorUtil; +import ru.windcorp.progressia.common.util.Vectors; import ru.windcorp.progressia.common.world.ChunkData; import ru.windcorp.progressia.common.world.ChunkDataListener; import ru.windcorp.progressia.common.world.block.BlockData; @@ -66,25 +68,33 @@ class ChunkUpdateListener implements ChunkDataListener { } private void onLocationChanged(ChunkData chunk, Vec3i blockInChunk) { - Vec3i chunkPos = new Vec3i(chunk.getPosition()); + Vec3i chunkPos = Vectors.grab3i().set(chunk.getX(), chunk.getY(), chunk.getZ()); - if (blockInChunk.x == 0) { - chunkPos.x -= 1; - } else if (blockInChunk.x == ChunkData.BLOCKS_PER_CHUNK - 1) { - chunkPos.x += 1; - } else if (blockInChunk.y == 0) { - chunkPos.y -= 1; - } else if (blockInChunk.y == ChunkData.BLOCKS_PER_CHUNK - 1) { - chunkPos.y += 1; - } else if (blockInChunk.z == 0) { - chunkPos.z -= 1; - } else if (blockInChunk.z == ChunkData.BLOCKS_PER_CHUNK - 1) { - chunkPos.z += 1; + checkCoordinate(blockInChunk, chunkPos, VectorUtil.Axis.X); + checkCoordinate(blockInChunk, chunkPos, VectorUtil.Axis.Y); + checkCoordinate(blockInChunk, chunkPos, VectorUtil.Axis.Z); + + Vectors.release(chunkPos); + } + + private void checkCoordinate(Vec3i blockInChunk, Vec3i chunkPos, VectorUtil.Axis axis) { + int block = VectorUtil.get(blockInChunk, axis); + int diff = 0; + + if (block == 0) { + diff = -1; + } else if (block == ChunkData.BLOCKS_PER_CHUNK - 1) { + diff = +1; } else { return; } + int previousChunkPos = VectorUtil.get(chunkPos, axis); + VectorUtil.set(chunkPos, axis, previousChunkPos + diff); + world.markChunkForUpdate(chunkPos); + + VectorUtil.set(chunkPos, axis, previousChunkPos); } } diff --git a/src/main/java/ru/windcorp/progressia/common/resource/ClasspathResourceReader.java b/src/main/java/ru/windcorp/progressia/common/resource/ClasspathResourceReader.java new file mode 100644 index 0000000..485efee --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/resource/ClasspathResourceReader.java @@ -0,0 +1,31 @@ +/* + * 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.common.resource; + +import java.io.InputStream; + +import ru.windcorp.progressia.Progressia; + +public class ClasspathResourceReader implements ResourceReader { + + @Override + public InputStream read(String name) { + return Progressia.class.getClassLoader().getResourceAsStream(name); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/common/resource/FilesystemResourceReader.java b/src/main/java/ru/windcorp/progressia/common/resource/FilesystemResourceReader.java new file mode 100644 index 0000000..38b30ba --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/resource/FilesystemResourceReader.java @@ -0,0 +1,36 @@ +/* + * 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.common.resource; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class FilesystemResourceReader implements ResourceReader { + + @Override + public InputStream read(String name) { + try { + return Files.newInputStream(Paths.get(name)); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/ru/windcorp/progressia/common/resource/Resource.java b/src/main/java/ru/windcorp/progressia/common/resource/Resource.java index 90acc5b..6cd4d32 100644 --- a/src/main/java/ru/windcorp/progressia/common/resource/Resource.java +++ b/src/main/java/ru/windcorp/progressia/common/resource/Resource.java @@ -30,19 +30,24 @@ import org.lwjgl.BufferUtils; import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; -import ru.windcorp.progressia.Progressia; import ru.windcorp.progressia.common.util.Named; import ru.windcorp.progressia.common.util.crash.CrashReports; public class Resource extends Named { + + private final ResourceReader resourceReader; - public Resource(String name) { + public Resource(String name, ResourceReader resourceReader) { super(name); + this.resourceReader = resourceReader; } public InputStream getInputStream() { - // TODO Do proper resource lookup - return Progressia.class.getClassLoader().getResourceAsStream(getName()); + return getResourceReader().read(getName()); + } + + public ResourceReader getResourceReader() { + return resourceReader; } public Reader getReader() { diff --git a/src/main/java/ru/windcorp/progressia/common/resource/ResourceManager.java b/src/main/java/ru/windcorp/progressia/common/resource/ResourceManager.java index 60b1698..33db64d 100644 --- a/src/main/java/ru/windcorp/progressia/common/resource/ResourceManager.java +++ b/src/main/java/ru/windcorp/progressia/common/resource/ResourceManager.java @@ -19,9 +19,16 @@ package ru.windcorp.progressia.common.resource; public class ResourceManager { + + private static final ResourceReader CLASSPATH_READER = new ClasspathResourceReader(); + private static final ResourceReader FILESYSTEM_READER = new FilesystemResourceReader(); public static Resource getResource(String name) { - return new Resource(name); + return new Resource(name, CLASSPATH_READER); + } + + public static Resource getFileResource(String name) { + return new Resource(name, FILESYSTEM_READER); } public static Resource getTextureResource(String name) { diff --git a/src/main/java/ru/windcorp/progressia/common/resource/ResourceReader.java b/src/main/java/ru/windcorp/progressia/common/resource/ResourceReader.java new file mode 100644 index 0000000..1f95f2c --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/resource/ResourceReader.java @@ -0,0 +1,26 @@ +/* + * 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.common.resource; + +import java.io.InputStream; + +public interface ResourceReader { + + InputStream read(String name); + +} diff --git a/src/main/java/ru/windcorp/progressia/common/util/crash/CrashReports.java b/src/main/java/ru/windcorp/progressia/common/util/crash/CrashReports.java index f33d4c5..ab0ec3e 100644 --- a/src/main/java/ru/windcorp/progressia/common/util/crash/CrashReports.java +++ b/src/main/java/ru/windcorp/progressia/common/util/crash/CrashReports.java @@ -203,13 +203,12 @@ public class CrashReports { if (provider == null) continue; - addSeparator(output); - try { Map buf = new HashMap<>(); provider.provideContext(buf); if (!buf.isEmpty()) { + addSeparator(output); output.append(StringUtil.center(provider.getName(), 80)).append("\n"); for (Map.Entry entry : buf.entrySet()) { output.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n"); diff --git a/src/main/java/ru/windcorp/progressia/common/util/crash/providers/RAMContextProvider.java b/src/main/java/ru/windcorp/progressia/common/util/crash/providers/RAMContextProvider.java index e455d78..9c60921 100644 --- a/src/main/java/ru/windcorp/progressia/common/util/crash/providers/RAMContextProvider.java +++ b/src/main/java/ru/windcorp/progressia/common/util/crash/providers/RAMContextProvider.java @@ -26,12 +26,12 @@ public class RAMContextProvider implements ContextProvider { @Override public void provideContext(Map output) { - output.put("Max Memory", Long.toString(Runtime.getRuntime().maxMemory() / 1024 / 1024) + " MB"); - output.put("Total Memory", Long.toString(Runtime.getRuntime().totalMemory() / 1024 / 1024) + " MB"); - output.put("Free Memory", Long.toString(Runtime.getRuntime().freeMemory() / 1024 / 1024) + " MB"); + output.put("Max Memory", Runtime.getRuntime().maxMemory() / 1024 / 1024 + " MB"); + output.put("Total Memory", Runtime.getRuntime().totalMemory() / 1024 / 1024 + " MB"); + output.put("Free Memory", Runtime.getRuntime().freeMemory() / 1024 / 1024 + " MB"); output.put( "Used Memory", - Long.toString((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024 / 1024) + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024 / 1024 + " MB" ); } diff --git a/src/main/java/ru/windcorp/progressia/common/util/crash/providers/ScreenContextProvider.java b/src/main/java/ru/windcorp/progressia/common/util/crash/providers/ScreenContextProvider.java new file mode 100644 index 0000000..7951e27 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/util/crash/providers/ScreenContextProvider.java @@ -0,0 +1,41 @@ +/* + * 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.common.util.crash.providers; + +import ru.windcorp.progressia.client.graphics.backend.GraphicsBackend; +import ru.windcorp.progressia.common.util.crash.ContextProvider; + +import java.util.Map; + +public class ScreenContextProvider implements ContextProvider { + + @Override + public void provideContext(Map output) { + if (GraphicsBackend.isGLFWInitialized()) { + output.put("Refresh rate", GraphicsBackend.getRefreshRate() + " Hz"); + output.put("Size", GraphicsBackend.getFrameWidth() + "x" + GraphicsBackend.getFrameHeight()); + output.put("Fullscreen", GraphicsBackend.isFullscreen() ? "enabled" : "disabled"); + } + } + + @Override + public String getName() { + return "Screen Context Provider"; + } +} diff --git a/src/main/java/ru/windcorp/progressia/server/Server.java b/src/main/java/ru/windcorp/progressia/server/Server.java index 63435a1..a0febbf 100644 --- a/src/main/java/ru/windcorp/progressia/server/Server.java +++ b/src/main/java/ru/windcorp/progressia/server/Server.java @@ -207,6 +207,18 @@ public class Server { return this.serverThread.getTicker().getTPS(); } + /** + * Returns the amount of ticks performed since the server has started. This + * value resets on shutdowns. The counter is incremented at the end of a + * tick. + * + * @return the number of times the world has finished a tick since the + * server has started. + */ + public long getUptimeTicks() { + return this.serverThread.getTicker().getUptimeTicks(); + } + /** * Returns the {@link WorldAccessor} object for this server. Use the * provided accessor to request common {@link Evaluation}s and diff --git a/src/main/java/ru/windcorp/progressia/server/world/ticking/TickerCoordinator.java b/src/main/java/ru/windcorp/progressia/server/world/ticking/TickerCoordinator.java index fa2a2d2..7d58d6d 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/ticking/TickerCoordinator.java +++ b/src/main/java/ru/windcorp/progressia/server/world/ticking/TickerCoordinator.java @@ -82,6 +82,7 @@ public class TickerCoordinator { private boolean isTickStartSet = false; private long tickStart = -1; private double tickLength = 1.0 / 20; // Do something about it + private long ticks = 0; private final Logger logger = LogManager.getLogger("Ticker Coordinator"); @@ -151,6 +152,10 @@ public class TickerCoordinator { public double getTPS() { return 1 / tickLength; } + + public long getUptimeTicks() { + return ticks; + } private void onTickStart() { long now = System.currentTimeMillis(); @@ -163,6 +168,10 @@ public class TickerCoordinator { tickStart = System.currentTimeMillis(); } + + private void onTickEnd() { + ticks++; + } /* * runOneTick & Friends @@ -182,6 +191,8 @@ public class TickerCoordinator { logger.debug("Pass complete"); passes++; } + + onTickEnd(); logger.debug("Tick complete; run {} passes", passes); @@ -191,7 +202,7 @@ public class TickerCoordinator { // ...or almost silently logger.debug("Tick interrupted. WTF?"); } catch (Exception e) { - crash(e, "Coordinator"); + throw CrashReports.report(e, "Coordinator"); } } diff --git a/src/main/java/ru/windcorp/progressia/test/LayerAbout.java b/src/main/java/ru/windcorp/progressia/test/LayerAbout.java index 3eaff87..7b45fd4 100644 --- a/src/main/java/ru/windcorp/progressia/test/LayerAbout.java +++ b/src/main/java/ru/windcorp/progressia/test/LayerAbout.java @@ -23,7 +23,7 @@ import ru.windcorp.progressia.client.graphics.font.Font; import ru.windcorp.progressia.client.graphics.font.Typeface; import ru.windcorp.progressia.client.graphics.gui.GUILayer; import ru.windcorp.progressia.client.graphics.gui.Label; -import ru.windcorp.progressia.client.graphics.gui.Panel; +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.MutableStringLocalized; @@ -33,12 +33,12 @@ public class LayerAbout extends GUILayer { public LayerAbout() { super("LayerAbout", new LayoutAlign(1, 1, 5)); - Panel panel = new Panel("ControlDisplays", new LayoutVertical(5)); + Group group = new Group("ControlDisplays", new LayoutVertical(5)); Font font = new Font().withColor(Colors.WHITE).deriveOutlined().withAlign(Typeface.ALIGN_RIGHT); Font aboutFont = font.withColor(0xFF37A3E6).deriveBold(); - panel.addChild( + group.addChild( new Label( "About", aboutFont, @@ -46,7 +46,7 @@ public class LayerAbout extends GUILayer { ) ); - panel.addChild( + group.addChild( new Label( "Version", font, @@ -54,7 +54,7 @@ public class LayerAbout extends GUILayer { ) ); - panel.addChild( + group.addChild( new Label( "DebugHint", font, @@ -62,7 +62,7 @@ public class LayerAbout extends GUILayer { ) ); - getRoot().addChild(panel); + getRoot().addChild(group); } diff --git a/src/main/java/ru/windcorp/progressia/test/LayerButtonTest.java b/src/main/java/ru/windcorp/progressia/test/LayerButtonTest.java new file mode 100644 index 0000000..e505291 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/test/LayerButtonTest.java @@ -0,0 +1,70 @@ +/* + * 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 ru.windcorp.progressia.client.graphics.Colors; +import ru.windcorp.progressia.client.graphics.font.Font; +import ru.windcorp.progressia.client.graphics.gui.Button; +import ru.windcorp.progressia.client.graphics.gui.Checkbox; +import ru.windcorp.progressia.client.graphics.gui.Label; +import ru.windcorp.progressia.client.graphics.gui.RadioButton; +import ru.windcorp.progressia.client.graphics.gui.RadioButtonGroup; +import ru.windcorp.progressia.client.graphics.gui.menu.MenuLayer; + +public class LayerButtonTest extends MenuLayer { + + public LayerButtonTest() { + super("ButtonTest"); + + addTitle(); + + Button blockableButton; + getContent().addChild((blockableButton = new Button("BlockableButton", "Blockable")).addAction(b -> { + System.out.println("Button Blockable!"); + })); + blockableButton.setEnabled(false); + + getContent().addChild(new Checkbox("EnableButton", "Enable").addAction(b -> { + blockableButton.setEnabled(((Checkbox) b).isChecked()); + })); + + RadioButtonGroup group = new RadioButtonGroup().addAction(g -> { + System.out.println("RBG! " + g.getSelected().getLabel().getCurrentText()); + }); + + getContent().addChild(new RadioButton("RB1", "Moon").setGroup(group)); + getContent().addChild(new RadioButton("RB2", "Type").setGroup(group)); + getContent().addChild(new RadioButton("RB3", "Ice").setGroup(group)); + getContent().addChild(new RadioButton("RB4", "Cream").setGroup(group)); + + getContent().getChild(getContent().getChildren().size() - 1).setEnabled(false); + + getContent().addChild(new Label("Hint", new Font().withColor(Colors.LIGHT_GRAY), "This is a MenuLayer")); + + getContent().addChild(new Button("Continue", "Continue").addAction(b -> { + getCloseAction().run(); + })); + + getContent().addChild(new Button("Quit", "Quit").addAction(b -> { + System.exit(0); + })); + + getContent().takeFocus(); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java b/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java index fa50625..fcdc1ac 100755 --- a/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java +++ b/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java @@ -18,22 +18,18 @@ package ru.windcorp.progressia.test; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Objects; -import java.util.function.Supplier; - 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.Panel; +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; @@ -44,19 +40,24 @@ 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)); - Panel panel = new Panel("ControlDisplays", new LayoutVertical(5)); + Group group = new Group("ControlDisplays", new LayoutVertical(5)); Vec4 color = Colors.WHITE; Font font = new Font().withColor(color).deriveOutlined(); TestPlayerControls tpc = TestPlayerControls.getInstance(); - panel.addChild( + group.addChild( new Label( "IsFlyingDisplay", font, @@ -64,7 +65,7 @@ public class LayerTestGUI extends GUILayer { ) ); - panel.addChild( + group.addChild( new Label( "IsSprintingDisplay", font, @@ -72,15 +73,7 @@ public class LayerTestGUI extends GUILayer { ) ); - panel.addChild( - new Label( - "IsMouseCapturedDisplay", - font, - tmp_dynFormat("LayerTestGUI.IsMouseCapturedDisplay", tpc::isMouseCaptured) - ) - ); - - panel.addChild( + group.addChild( new Label( "CameraModeDisplay", font, @@ -91,7 +84,7 @@ public class LayerTestGUI extends GUILayer { ) ); - panel.addChild( + group.addChild( new Label( "LanguageDisplay", font, @@ -99,7 +92,23 @@ public class LayerTestGUI extends GUILayer { ) ); - panel.addChild( + 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, @@ -111,7 +120,7 @@ public class LayerTestGUI extends GUILayer { ) ); - panel.addChild( + group.addChild( new DynamicLabel( "TPSDisplay", font, @@ -120,7 +129,7 @@ public class LayerTestGUI extends GUILayer { ) ); - panel.addChild( + group.addChild( new DynamicLabel( "ChunkUpdatesDisplay", font, @@ -132,7 +141,7 @@ public class LayerTestGUI extends GUILayer { ) ); - panel.addChild( + group.addChild( new DynamicLabel( "PosDisplay", font, @@ -141,7 +150,7 @@ public class LayerTestGUI extends GUILayer { ) ); - panel.addChild( + group.addChild( new Label( "SelectedBlockDisplay", font, @@ -152,7 +161,7 @@ public class LayerTestGUI extends GUILayer { ) ) ); - panel.addChild( + group.addChild( new Label( "SelectedTileDisplay", font, @@ -163,7 +172,7 @@ public class LayerTestGUI extends GUILayer { ) ) ); - panel.addChild( + group.addChild( new Label( "PlacementModeHint", font, @@ -171,7 +180,7 @@ public class LayerTestGUI extends GUILayer { ) ); - getRoot().addChild(panel); + getRoot().addChild(group); } public Runnable getUpdateCallback() { diff --git a/src/main/java/ru/windcorp/progressia/test/TestContent.java b/src/main/java/ru/windcorp/progressia/test/TestContent.java index 0c78b35..46bf460 100644 --- a/src/main/java/ru/windcorp/progressia/test/TestContent.java +++ b/src/main/java/ru/windcorp/progressia/test/TestContent.java @@ -32,7 +32,7 @@ import org.lwjgl.glfw.GLFW; import glm.vec._3.i.Vec3i; import ru.windcorp.progressia.client.ClientState; -import ru.windcorp.progressia.client.audio.SoundEffect; +import ru.windcorp.progressia.client.audio.Sound; import ru.windcorp.progressia.client.comms.controls.*; import ru.windcorp.progressia.client.graphics.input.KeyEvent; import ru.windcorp.progressia.client.graphics.input.KeyMatcher; @@ -289,6 +289,15 @@ public class TestContent { ) ); logic.register(ControlLogic.of("Test:PlaceTile", TestContent::onTilePlaceReceived)); + + triggers.register( + ControlTriggers.localOf( + "Test:StartNextMusic", + KeyEvent.class, + TestMusicPlayer::startNextNow, + KeyMatcher.of(GLFW.GLFW_KEY_M).matcher() + ) + ); } private static void register(BlockData x) { @@ -362,7 +371,7 @@ public class TestContent { private static void onBlockBreakTrigger(ControlData control) { ((ControlBreakBlockData) control).setBlockInWorld(getSelection().getBlock()); - SoundEffect sfx = new SoundEffect("Progressia:BlockDestroy"); + Sound sfx = new Sound("Progressia:BlockDestroy"); sfx.setPosition(getSelection().getPoint()); sfx.setPitch((float) (Math.random() + 1 * 0.5)); sfx.play(false); diff --git a/src/main/java/ru/windcorp/progressia/test/TestMusicPlayer.java b/src/main/java/ru/windcorp/progressia/test/TestMusicPlayer.java new file mode 100644 index 0000000..1674d5b --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/test/TestMusicPlayer.java @@ -0,0 +1,152 @@ +/* + * 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 java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +import org.apache.logging.log4j.LogManager; + +import ru.windcorp.progressia.client.audio.AudioFormat; +import ru.windcorp.progressia.client.audio.AudioManager; +import ru.windcorp.progressia.client.audio.AudioRegistry; +import ru.windcorp.progressia.client.audio.Music; +import ru.windcorp.progressia.client.audio.Sound; +import ru.windcorp.progressia.client.audio.backend.SoundType; +import ru.windcorp.progressia.common.resource.ResourceManager; +import ru.windcorp.progressia.common.util.crash.CrashReports; + +public class TestMusicPlayer implements Runnable { + + private static final int MIN_SILENCE = 15 * 1000; // 15 seconds + private static final int MAX_SILENCE = 60 * 1000; // one minute + + private static TestMusicPlayer instance = null; + + private final List compositions = new ArrayList<>(); + + private final Random random = new Random(); + private long nextStart; + private Sound lastStarted = null; + + public TestMusicPlayer() { + this.nextStart = System.currentTimeMillis(); + + instance = this; + } + + public static void start() { + Thread thread = new Thread(new TestMusicPlayer(), "Music Thread"); + thread.setDaemon(true); + thread.start(); + } + + @Override + public void run() { + loadCompositions(); + + if (compositions.isEmpty()) { + LogManager.getLogger().warn("No music found"); + return; + } + + while (true) { + + try { + synchronized (this) { + while (true) { + long now = System.currentTimeMillis(); + if (nextStart > now) { + wait(nextStart - now); + } else { + break; + } + } + } + } catch (InterruptedException e) { + LogManager.getLogger().warn("Received interrupt in music thread, terminating thread..."); + return; + } + + startNextComposition(); + + } + } + + private void loadCompositions() { + try { + + Path directory = Paths.get("music"); + + if (!Files.isDirectory(directory)) { + Files.createDirectories(directory); + } + + Iterator it = Files.walk(directory).filter(Files::isRegularFile).iterator(); + int i = 0; + + while (it.hasNext()) { + String file = it.next().toString(); + if (!file.endsWith(".ogg") && !file.endsWith(".oga")) { + LogManager.getLogger().warn("Skipping " + file + ": not .ogg nor .oga"); + } + + String id = "Progressia:Music" + (i++); + + AudioManager.loadSound(ResourceManager.getFileResource(file.toString()), id, AudioFormat.STEREO); + SoundType composition = AudioRegistry.getInstance().get(id); + compositions.add(composition); + + LogManager.getLogger().info("Loaded " + file); + } + + } catch (IOException e) { + throw CrashReports.report(e, "Could not load music"); + } + } + + private synchronized void startNextComposition() { + int index = random.nextInt(compositions.size()); + SoundType composition = compositions.get(index); + + long now = System.currentTimeMillis(); + long durationInMs = (long) (composition.getDuration() * 1000); + long silence = random.nextInt(MAX_SILENCE - MIN_SILENCE) + MIN_SILENCE; + + nextStart = now + durationInMs + silence; + + lastStarted = new Music(composition); + lastStarted.play(false); + } + + public static void startNextNow() { + if (instance == null) return; + + synchronized (instance) { + instance.nextStart = System.currentTimeMillis(); + instance.notifyAll(); + } + } + +} diff --git a/src/main/java/ru/windcorp/progressia/test/TestPlayerControls.java b/src/main/java/ru/windcorp/progressia/test/TestPlayerControls.java index 541d909..ee42396 100644 --- a/src/main/java/ru/windcorp/progressia/test/TestPlayerControls.java +++ b/src/main/java/ru/windcorp/progressia/test/TestPlayerControls.java @@ -84,8 +84,6 @@ public class TestPlayerControls { private double lastSpacePress = Double.NEGATIVE_INFINITY; private double lastSprintPress = Double.NEGATIVE_INFINITY; - private boolean captureMouse = true; - private int selectedBlock = 0; private int selectedTile = 0; private boolean isBlockSelected = true; @@ -203,9 +201,24 @@ public class TestPlayerControls { case GLFW.GLFW_KEY_ESCAPE: if (!event.isPress()) return false; + handleEscape(); break; + case GLFW.GLFW_KEY_F11: + if (!event.isPress()) + return false; + GraphicsInterface.makeFullscreen(!GraphicsBackend.isFullscreen()); + updateGUI(); + break; + + case GLFW.GLFW_KEY_F12: + if (!event.isPress()) + return false; + GraphicsBackend.setVSyncEnabled(!GraphicsBackend.isVSyncEnabled()); + updateGUI(); + break; + case GLFW.GLFW_KEY_F3: if (!event.isPress()) return false; @@ -298,14 +311,10 @@ public class TestPlayerControls { } private void handleEscape() { - if (captureMouse) { - GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL); - } else { - GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED); - } - - captureMouse = !captureMouse; - updateGUI(); + movementForward = 0; + movementRight = 0; + movementUp = 0; + GUI.addTopLayer(new LayerButtonTest()); } private void handleDebugLayerSwitch() { @@ -344,10 +353,6 @@ public class TestPlayerControls { } private void onMouseMoved(CursorMoveEvent event) { - if (!captureMouse) { - return; - } - if (ClientState.getInstance() == null || !ClientState.getInstance().isReady()) { return; } @@ -445,10 +450,6 @@ public class TestPlayerControls { return isSprinting; } - public boolean isMouseCaptured() { - return captureMouse; - } - public BlockData getSelectedBlock() { return TestContent.PLACEABLE_BLOCKS.get(selectedBlock); } diff --git a/src/main/resources/assets/languages/en-US.lang b/src/main/resources/assets/languages/en-US.lang index 62e2e6e..e4b69de 100644 --- a/src/main/resources/assets/languages/en-US.lang +++ b/src/main/resources/assets/languages/en-US.lang @@ -6,7 +6,6 @@ LayerAbout.DebugHint = Debug GUI: F3 LayerTestGUI.IsFlyingDisplay = Flying: %5s (Space bar x2) LayerTestGUI.IsSprintingDisplay = Sprinting: %5s (W x2) -LayerTestGUI.IsMouseCapturedDisplay = Mouse captured: %5s (Esc) LayerTestGUI.CameraModeDisplay = Camera mode: %5d (F5) LayerTestGUI.LanguageDisplay = Language: %5s (L) LayerTestGUI.FPSDisplay = FPS: @@ -18,4 +17,8 @@ LayerTestGUI.PosDisplay.NA.Client = Pos: client n/a LayerTestGUI.PosDisplay.NA.Entity = Pos: entity n/a LayerTestGUI.SelectedBlockDisplay = %s Block: %s LayerTestGUI.SelectedTileDisplay = %s Tile: %s -LayerTestGUI.PlacementModeHint = (Blocks %s Tiles: Ctrl + Mouse Wheel) \ No newline at end of file +LayerTestGUI.PlacementModeHint = (Blocks %s Tiles: Ctrl + Mouse Wheel) +LayerTestGUI.IsFullscreen = Fullscreen: %5s (F11) +LayerTestGUI.IsVSync = VSync: %5s (F12) + +LayerButtonTest.Title = Button Test \ No newline at end of file diff --git a/src/main/resources/assets/languages/ru-RU.lang b/src/main/resources/assets/languages/ru-RU.lang index 081a630..a01e6ad 100644 --- a/src/main/resources/assets/languages/ru-RU.lang +++ b/src/main/resources/assets/languages/ru-RU.lang @@ -6,7 +6,6 @@ LayerAbout.DebugHint = Отладочный GUI: F3 LayerTestGUI.IsFlyingDisplay = Полёт: %5s (Пробел x2) LayerTestGUI.IsSprintingDisplay = Бег: %5s (W x2) -LayerTestGUI.IsMouseCapturedDisplay = Захват мыши: %5s (Esc) LayerTestGUI.CameraModeDisplay = Камера: %5d (F5) LayerTestGUI.LanguageDisplay = Язык: %5s (L) LayerTestGUI.FPSDisplay = FPS: @@ -18,4 +17,8 @@ LayerTestGUI.PosDisplay.NA.Client = Поз: клиент н/д LayerTestGUI.PosDisplay.NA.Entity = Поз: сущность н/д LayerTestGUI.SelectedBlockDisplay = %s Блок: %s LayerTestGUI.SelectedTileDisplay = %s Плитка: %s -LayerTestGUI.PlacementModeHint = (Блок %s плитки: Ctrl + прокрутка) \ No newline at end of file +LayerTestGUI.PlacementModeHint = (Блок %s плитки: Ctrl + прокрутка) +LayerTestGUI.IsFullscreen = Полный экран: %5s (F11) +LayerTestGUI.IsVSync = Верт. синхр.: %5s (F12) + +LayerButtonTest.Title = Тест Кнопок \ No newline at end of file