Client work of the Week #4
@@ -13,12 +13,12 @@ public class AcousticHandler {
|
||||
|
||||
private boolean playGame = false;
|
||||
private ArrayList<MusicAsset> gameTracks = new ArrayList<>();
|
||||
private NanoTimer trackTimer = new NanoTimer();
|
||||
private NanoTimer trackTimer = new NanoTimer();
|
||||
|
||||
private boolean fading = false;
|
||||
private NanoTimer fadeTimer = new NanoTimer();
|
||||
private final float FADE_DURATION = 3.0f;
|
||||
private final float CROSSFADE_DURATION = 1.5f;
|
||||
private static final float FADE_DURATION = 3.0f;
|
||||
private static final float CROSSFADE_DURATION = 1.5f;
|
||||
|
||||
private float mainVolume = 1.0f;
|
||||
private float musicVolume = 1.0f;
|
||||
@@ -38,7 +38,7 @@ public AcousticHandler(MdgaApp app) {
|
||||
public void update() {
|
||||
updateVolumeAndTrack();
|
||||
|
||||
if(playGame) {
|
||||
if (playGame) {
|
||||
updateGameTracks();
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ public void playSound(MdgaSound sound) {
|
||||
* @param state the state of which the corresponding music should be played to be played
|
||||
*/
|
||||
public void playState(MdgaState state) {
|
||||
if(this.state == state) {
|
||||
if (this.state == state) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public void playState(MdgaState state) {
|
||||
case GAME:
|
||||
addGameTracks();
|
||||
playGame = true;
|
||||
assert(gameTracks.size() > 0) : "no more game music available";
|
||||
assert (gameTracks.size() > 0) : "no more game music available";
|
||||
asset = gameTracks.remove(0);
|
||||
break;
|
||||
case CEREMONY:
|
||||
@@ -112,7 +112,7 @@ public void playState(MdgaState state) {
|
||||
break;
|
||||
}
|
||||
|
||||
assert(null != asset) : "music sceduling went wrong";
|
||||
assert (null != asset) : "music sceduling went wrong";
|
||||
|
||||
scheduled = new GameMusic(app, asset, getMusicVolumeTotal(), asset.getSubVolume(), asset.getLoop());
|
||||
}
|
||||
@@ -121,8 +121,8 @@ public void playState(MdgaState state) {
|
||||
* Performs linear interpolation between two float values.
|
||||
*
|
||||
* @param start The starting value.
|
||||
* @param end The ending value.
|
||||
* @param t The interpolation factor, typically between 0 and 1.
|
||||
* @param end The ending value.
|
||||
* @param t The interpolation factor, typically between 0 and 1.
|
||||
* @return The interpolated value between start and end.
|
||||
*/
|
||||
private float lerp(float start, float end, float t) {
|
||||
@@ -154,7 +154,7 @@ private void updateVolumeAndTrack() {
|
||||
float t = Math.min(time / FADE_DURATION, 1.0f);
|
||||
float oldVolume = lerp(1.0f, 0.0f, t);
|
||||
if (playing != null) {
|
||||
playing.update(getMusicVolumeTotal()* oldVolume);
|
||||
playing.update(getMusicVolumeTotal() * oldVolume);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,8 @@ private void updateVolumeAndTrack() {
|
||||
|
||||
fading = false;
|
||||
}
|
||||
} else if (playing != null) {
|
||||
}
|
||||
else if (playing != null) {
|
||||
playing.update(getMusicVolumeTotal());
|
||||
}
|
||||
}
|
||||
@@ -200,7 +201,7 @@ private void addGameTracks() {
|
||||
* a new track will be scheduled to play. If the list of game tracks is empty, it will be refreshed.
|
||||
*/
|
||||
private void updateGameTracks() {
|
||||
if(playing.nearEnd(10)) {
|
||||
if (playing.nearEnd(10)) {
|
||||
if (gameTracks.isEmpty()) {
|
||||
addGameTracks();
|
||||
}
|
||||
@@ -221,7 +222,7 @@ private void updateGameTracks() {
|
||||
* @return The current main volume level.
|
||||
*/
|
||||
public float getMainVolume() {
|
||||
return mainVolume;
|
||||
return mainVolume;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,11 +18,11 @@ class GameMusic {
|
||||
/**
|
||||
* Constructs a new GameMusic object.
|
||||
*
|
||||
* @param app The instance of the application, used to access the asset manager.
|
||||
* @param asset The music asset to be played.
|
||||
* @param volume The total volume of the music, adjusted by the main volume.
|
||||
* @param app The instance of the application, used to access the asset manager.
|
||||
* @param asset The music asset to be played.
|
||||
* @param volume The total volume of the music, adjusted by the main volume.
|
||||
* @param subVolume A relative volume that modifies the base music volume, typically a percentage.
|
||||
* @param loop A flag indicating whether the music should loop once it finishes.
|
||||
* @param loop A flag indicating whether the music should loop once it finishes.
|
||||
*/
|
||||
GameMusic(MdgaApp app, MusicAsset asset, float volume, float subVolume, boolean loop) {
|
||||
this.volume = volume;
|
||||
@@ -42,7 +42,7 @@ class GameMusic {
|
||||
* If the music is not available, no action is performed.
|
||||
*/
|
||||
void play() {
|
||||
if(null == music) {
|
||||
if (null == music) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ void play() {
|
||||
* If the music is not available or is not playing, no action is performed.
|
||||
*/
|
||||
void pause() {
|
||||
if(null == music) {
|
||||
if (null == music) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ boolean nearEnd(float thresholdSeconds) {
|
||||
* @param newVolume The new total volume for the music.
|
||||
*/
|
||||
void update(float newVolume) {
|
||||
if(volume != newVolume) {
|
||||
if (volume != newVolume) {
|
||||
volume = newVolume;
|
||||
music.setVolume(volume * subVolume);
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ class GameSound {
|
||||
/**
|
||||
* Constructs a new GameSound object.
|
||||
*
|
||||
* @param app The instance of the application, used to access the asset manager.
|
||||
* @param asset The sound asset to be played.
|
||||
* @param volume The total volume of the sound, adjusted by the main volume.
|
||||
* @param app The instance of the application, used to access the asset manager.
|
||||
* @param asset The sound asset to be played.
|
||||
* @param volume The total volume of the sound, adjusted by the main volume.
|
||||
* @param subVolume A relative volume that modifies the base sound volume, typically a percentage.
|
||||
* @param delay The delay before the sound starts playing, in seconds.
|
||||
* @param delay The delay before the sound starts playing, in seconds.
|
||||
*/
|
||||
GameSound(MdgaApp app, SoundAsset asset, float volume, float subVolume, float delay) {
|
||||
this.volume = volume;
|
||||
@@ -62,21 +62,21 @@ boolean isPlaying() {
|
||||
* @param newVolume The new total volume for the sound.
|
||||
*/
|
||||
void update(float newVolume) {
|
||||
if(!playing && timer.getTimeInSeconds() > delay) {
|
||||
if (!playing && timer.getTimeInSeconds() > delay) {
|
||||
sound.play();
|
||||
playing = true;
|
||||
}
|
||||
|
||||
if(!playing) {
|
||||
if (!playing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(volume != newVolume) {
|
||||
if (volume != newVolume) {
|
||||
volume = newVolume;
|
||||
sound.setVolume(volume * subVolume);
|
||||
}
|
||||
|
||||
if(sound != null && sound.getStatus() == AudioSource.Status.Playing) {
|
||||
if (sound != null && sound.getStatus() == AudioSource.Status.Playing) {
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Enum representing the various sound effects used in the game.
|
||||
* Each sound corresponds to an event or action in the game and may consist of one or more
|
||||
* audio files, potentially with time delays between them.
|
||||
*
|
||||
* <p>
|
||||
* These sounds are used to play specific audio cues, such as when a dice is rolled,
|
||||
* a turn starts or ends, a piece is moved or lost, and various other events in the game.
|
||||
*/
|
||||
@@ -19,5 +19,5 @@ public enum MdgaSound {
|
||||
DESELECT,
|
||||
HURRY,
|
||||
VICTORY,
|
||||
LOST;
|
||||
LOST
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ enum MusicAsset {
|
||||
* Constructs a new MusicAsset object with the specified name and sub-volume.
|
||||
* The track will not loop by default.
|
||||
*
|
||||
* @param name The name of the music file.
|
||||
* @param name The name of the music file.
|
||||
* @param subVolume A relative volume that modifies the base volume of the track (typically a percentage).
|
||||
*/
|
||||
MusicAsset(String name, float subVolume) {
|
||||
@@ -37,8 +37,8 @@ enum MusicAsset {
|
||||
/**
|
||||
* Constructs a new MusicAsset object with the specified name, loop flag, and sub-volume.
|
||||
*
|
||||
* @param name The name of the music file.
|
||||
* @param loop If true, the track will loop; otherwise, it will play once.
|
||||
* @param name The name of the music file.
|
||||
* @param loop If true, the track will loop; otherwise, it will play once.
|
||||
* @param subVolume A relative volume that modifies the base volume of the track (typically a percentage).
|
||||
*/
|
||||
MusicAsset(String name, boolean loop, float subVolume) {
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
* A record that encapsulates a sound asset along with its playback settings:
|
||||
* the relative volume (subVolume) and a delay before it starts playing.
|
||||
*/
|
||||
record SoundAssetDelayVolume(SoundAsset asset, float subVolume, float delay) { }
|
||||
record SoundAssetDelayVolume(SoundAsset asset, float subVolume, float delay) {}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
abstract class Animation {
|
||||
abstract void play();
|
||||
|
||||
abstract void stop();
|
||||
|
||||
abstract boolean isOver();
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ public void playAnimation(MdgaAnimation type) {
|
||||
}
|
||||
|
||||
public void update() {
|
||||
if(null == animation) {
|
||||
if (null == animation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(animation.isOver()) {
|
||||
if (animation.isOver()) {
|
||||
animation = null;
|
||||
|
||||
//trigger next state in model
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package pp.mdga.client.Board;
|
||||
|
||||
record AssetOnMap(BoardAsset asset, int x, int y, float rot){}
|
||||
record AssetOnMap(BoardAsset asset, int x, int y, float rot) {}
|
||||
|
||||
@@ -28,20 +28,20 @@ enum BoardAsset {
|
||||
private final String diffPath;
|
||||
private final float size;
|
||||
|
||||
BoardAsset(){
|
||||
BoardAsset() {
|
||||
String folderFileName = "./" + name() + "/" + name();
|
||||
this.modelPath = folderFileName + ".j3o";
|
||||
this.diffPath = folderFileName + "_diff.png";
|
||||
this.size = 1f;
|
||||
}
|
||||
|
||||
BoardAsset(String modelPath, String diffPath){
|
||||
BoardAsset(String modelPath, String diffPath) {
|
||||
this.modelPath = modelPath;
|
||||
this.diffPath = diffPath;
|
||||
this.size = 1f;
|
||||
}
|
||||
|
||||
BoardAsset(float size){
|
||||
BoardAsset(float size) {
|
||||
String folderFileName = "./" + name() + "/" + name();
|
||||
this.modelPath = folderFileName + ".j3o";
|
||||
this.diffPath = folderFileName + "_diff.png";
|
||||
@@ -56,8 +56,7 @@ public String getDiffPath() {
|
||||
return diffPath;
|
||||
}
|
||||
|
||||
public float getSize(){
|
||||
public float getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class BoardView {
|
||||
private Map<Color, List<AssetOnMap>> playerMap;
|
||||
|
||||
public BoardView(MdgaApp app) {
|
||||
assert(app != null) : "app is null";
|
||||
assert (app != null) : "app is null";
|
||||
|
||||
this.app = app;
|
||||
|
||||
@@ -41,11 +41,11 @@ public BoardView(MdgaApp app) {
|
||||
initCamera();
|
||||
}
|
||||
|
||||
private void addFigureToPlayerMap(Color col, AssetOnMap assetOnMap){
|
||||
private void addFigureToPlayerMap(Color col, AssetOnMap assetOnMap) {
|
||||
List<AssetOnMap> inMap = playerMap.getOrDefault(col, new ArrayList<>());
|
||||
inMap.add(assetOnMap);
|
||||
|
||||
assert(inMap.size() <= 4) : "to many assets for one player";
|
||||
assert (inMap.size() <= 4) : "to many assets for one player";
|
||||
|
||||
playerMap.put(col, inMap);
|
||||
}
|
||||
@@ -53,13 +53,14 @@ private void addFigureToPlayerMap(Color col, AssetOnMap assetOnMap){
|
||||
private void initMap() {
|
||||
List<AssetOnMap> assetOnMaps = MapLoader.loadMap(MAP_NAME);
|
||||
|
||||
for (AssetOnMap assetOnMap : assetOnMaps){
|
||||
switch (assetOnMap.asset()){
|
||||
for (AssetOnMap assetOnMap : assetOnMaps) {
|
||||
switch (assetOnMap.asset()) {
|
||||
case lw -> addFigureToPlayerMap(assetToColor(BoardAsset.lw), assetOnMap);
|
||||
case heer -> addFigureToPlayerMap(assetToColor(BoardAsset.heer), assetOnMap);
|
||||
case cir -> addFigureToPlayerMap(assetToColor(BoardAsset.cir), assetOnMap);
|
||||
case marine -> addFigureToPlayerMap(assetToColor(BoardAsset.marine), assetOnMap);
|
||||
case node_normal, node_bonus, node_start -> infield.addLast(displayAndControl(assetOnMap, new NodeControl()));
|
||||
case node_normal, node_bonus, node_start ->
|
||||
infield.addLast(displayAndControl(assetOnMap, new NodeControl()));
|
||||
default -> displayAsset(assetOnMap);
|
||||
}
|
||||
}
|
||||
@@ -68,27 +69,26 @@ private void initMap() {
|
||||
private void initCamera() {
|
||||
app.getFlyByCamera().setEnabled(true);
|
||||
int zoom = 20;
|
||||
app.getCamera().setLocation(new Vector3f(-zoom,0,zoom));
|
||||
app.getCamera().lookAt(new Vector3f(0,0,0), new Vector3f(0,0,1));
|
||||
app.getCamera().setLocation(new Vector3f(-zoom, 0, zoom));
|
||||
app.getCamera().lookAt(new Vector3f(0, 0, 0), new Vector3f(0, 0, 1));
|
||||
|
||||
DirectionalLight sun = new DirectionalLight();
|
||||
sun.setColor(ColorRGBA.White);
|
||||
sun.setDirection(new Vector3f(0.3f,0,-1));
|
||||
sun.setDirection(new Vector3f(0.3f, 0, -1));
|
||||
app.getRootNode().addLight(sun);
|
||||
|
||||
AmbientLight ambient = new AmbientLight();
|
||||
ambient.setColor(new ColorRGBA(0.3f,0.3f,0.3f,1));
|
||||
ambient.setColor(new ColorRGBA(0.3f, 0.3f, 0.3f, 1));
|
||||
app.getRootNode().addLight(ambient);
|
||||
|
||||
final int SHADOWMAP_SIZE= 1024 * 8;
|
||||
int SHADOWMAP_SIZE = 1024 * 8;
|
||||
DirectionalLightShadowRenderer dlsr = new DirectionalLightShadowRenderer(app.getAssetManager(), SHADOWMAP_SIZE, 4);
|
||||
dlsr.setLight(sun);
|
||||
app.getViewPort().addProcessor(dlsr);
|
||||
}
|
||||
|
||||
|
||||
private Color assetToColor(BoardAsset asset){
|
||||
return switch(asset){
|
||||
private Color assetToColor(BoardAsset asset) {
|
||||
return switch (asset) {
|
||||
case lw -> Color.AIRFORCE;
|
||||
case heer -> Color.ARMY;
|
||||
case marine -> Color.NAVY;
|
||||
@@ -97,7 +97,7 @@ private Color assetToColor(BoardAsset asset){
|
||||
};
|
||||
}
|
||||
|
||||
private Spatial createModel(BoardAsset asset, Vector3f pos, float rot){
|
||||
private Spatial createModel(BoardAsset asset, Vector3f pos, float rot) {
|
||||
String modelName = asset.getModelPath();
|
||||
String texName = asset.getDiffPath();
|
||||
Spatial model = app.getAssetManager().loadModel(modelName);
|
||||
@@ -116,23 +116,23 @@ private static Vector3f gridToWorld(int x, int y) {
|
||||
return new Vector3f(GRID_SIZE * x, GRID_SIZE * y, GRID_ELEVATION);
|
||||
}
|
||||
|
||||
public void addPlayer(Color color, UUID uuid){
|
||||
public void addPlayer(Color color, UUID uuid) {
|
||||
List<AssetOnMap> playerAssets = playerMap.get(color);
|
||||
assert(playerAssets != null) : "Assets for Player color are not defined";
|
||||
assert (playerAssets != null) : "Assets for Player color are not defined";
|
||||
|
||||
for (AssetOnMap assetOnMap : playerAssets){
|
||||
for (AssetOnMap assetOnMap : playerAssets) {
|
||||
pieces.put(uuid, displayAndControl(assetOnMap, new PieceControl()));
|
||||
}
|
||||
}
|
||||
|
||||
//displays an assets and return the created asset
|
||||
private Spatial displayAsset(AssetOnMap assetOnMap){
|
||||
private Spatial displayAsset(AssetOnMap assetOnMap) {
|
||||
int x = assetOnMap.x();
|
||||
int y = assetOnMap.y();
|
||||
return createModel(assetOnMap.asset(), gridToWorld(x,y), assetOnMap.rot());
|
||||
return createModel(assetOnMap.asset(), gridToWorld(x, y), assetOnMap.rot());
|
||||
}
|
||||
|
||||
private <T extends AbstractControl> T displayAndControl(AssetOnMap assetOnMap, T control){
|
||||
private <T extends AbstractControl> T displayAndControl(AssetOnMap assetOnMap, T control) {
|
||||
Spatial spatial = displayAsset(assetOnMap);
|
||||
spatial.addControl(control);
|
||||
return control;
|
||||
|
||||
@@ -7,35 +7,36 @@
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MapLoader {
|
||||
private MapLoader(){
|
||||
class MapLoader {
|
||||
private MapLoader() {
|
||||
|
||||
}
|
||||
|
||||
public static List<AssetOnMap> loadMap(String mapName) {
|
||||
List<AssetOnMap> assetsOnMap = new ArrayList<>();
|
||||
|
||||
try (InputStream inputStream = MapLoader.class.getClassLoader().getResourceAsStream(mapName);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
||||
try (
|
||||
InputStream inputStream = MapLoader.class.getClassLoader().getResourceAsStream(mapName);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))
|
||||
) {
|
||||
|
||||
while (true) {
|
||||
String entry = reader.readLine();
|
||||
if(entry == null) break;
|
||||
if (entry == null) break;
|
||||
|
||||
entry = entry.trim();
|
||||
|
||||
if(entry.isEmpty()) continue;
|
||||
if(entry.charAt(0) == '#') continue;
|
||||
if (entry.isEmpty()) continue;
|
||||
if (entry.charAt(0) == '#') continue;
|
||||
|
||||
String[] parts = entry.trim().split(" ");
|
||||
if(parts.length != 3){
|
||||
parts = parts;
|
||||
}
|
||||
assert(parts.length == 3) : "MapLoader: line has not 3 parts";
|
||||
|
||||
assert (parts.length == 3) : "MapLoader: line has not 3 parts";
|
||||
|
||||
String assetName = parts[0];
|
||||
String[] coordinates = parts[1].split(",");
|
||||
|
||||
assert(coordinates.length == 2) : "MapLoade: coordinates are wrong";
|
||||
assert (coordinates.length == 2) : "MapLoade: coordinates are wrong";
|
||||
|
||||
int x = Integer.parseInt(coordinates[0]);
|
||||
int y = Integer.parseInt(coordinates[1]);
|
||||
@@ -45,9 +46,11 @@ public static List<AssetOnMap> loadMap(String mapName) {
|
||||
BoardAsset asset = getLoadedAsset(assetName);
|
||||
assetsOnMap.add(new AssetOnMap(asset, x, y, rot));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalArgumentException e) {
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -55,7 +58,7 @@ public static List<AssetOnMap> loadMap(String mapName) {
|
||||
}
|
||||
|
||||
private static BoardAsset getLoadedAsset(String assetName) {
|
||||
return switch(assetName){
|
||||
return switch (assetName) {
|
||||
case "lw" -> BoardAsset.lw;
|
||||
case "cir" -> BoardAsset.cir;
|
||||
case "marine" -> BoardAsset.marine;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
public class DialogView {
|
||||
private MdgaApp app;
|
||||
|
||||
private DialogManager dialogManager = new DialogManager(app);
|
||||
private DialogManager dialogManager = new DialogManager(app);
|
||||
|
||||
private StartDialog dialog;
|
||||
|
||||
|
||||
@@ -24,12 +24,11 @@ public class StartDialog extends SimpleDialog {
|
||||
input.addChild(serverHost);
|
||||
|
||||
DialogBuilder.simple(app.getDialogView().getDialogManager())
|
||||
.setTitle("server.dialog")
|
||||
.setOkButton("button.connect")
|
||||
.setNoButton("button.cancel")
|
||||
.setOkClose(false)
|
||||
.setNoClose(false)
|
||||
.build(this);
|
||||
|
||||
.setTitle("server.dialog")
|
||||
.setOkButton("button.connect")
|
||||
.setNoButton("button.cancel")
|
||||
.setOkClose(false)
|
||||
.setNoClose(false)
|
||||
.build(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,11 +50,12 @@ public void simpleUpdate(float tpf) {
|
||||
acousticHandler.update();
|
||||
|
||||
//test.reset();
|
||||
if(test.getTimeInSeconds() > 10){
|
||||
if(testState == MdgaState.MAIN) {
|
||||
if (test.getTimeInSeconds() > 10) {
|
||||
if (testState == MdgaState.MAIN) {
|
||||
testState = MdgaState.LOBBY;
|
||||
acousticHandler.playState(MdgaState.MAIN);
|
||||
} else if (testState == MdgaState.LOBBY) {
|
||||
}
|
||||
else if (testState == MdgaState.LOBBY) {
|
||||
testState = MdgaState.CEREMONY;
|
||||
acousticHandler.playState(MdgaState.LOBBY);
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ void handleNotification(MdgaApp app, Notification notification) {
|
||||
GAME {
|
||||
@Override
|
||||
void handleNotification(MdgaApp app, Notification notification) {
|
||||
if(notification instanceof PlayerInGameNotification) {
|
||||
if (notification instanceof PlayerInGameNotification) {
|
||||
//TODO
|
||||
}
|
||||
else if(notification instanceof PieceInGameNotification){
|
||||
else if (notification instanceof PieceInGameNotification) {
|
||||
//TODO
|
||||
}
|
||||
else {
|
||||
|
||||
Reference in New Issue
Block a user