mirror of
https://github.com/RandomityGuy/MBHaxe.git
synced 2025-10-30 08:11:25 +00:00
attempt implement the restarts and add shader/mega helicopter
This commit is contained in:
parent
7837be38e9
commit
36bc46d7b9
8 changed files with 166 additions and 38 deletions
|
|
@ -204,7 +204,8 @@ class DifBuilder {
|
|||
worker.run();
|
||||
}
|
||||
|
||||
static function createNoiseTileMaterial(onFinish:hxsl.Shader->Void, baseTexture:String, noiseSuffix:String, shininess:Float, specular:Vector) {
|
||||
static function createNoiseTileMaterial(onFinish:hxsl.Shader->Void, baseTexture:String, noiseSuffix:String, shininess:Float, specular:Vector,
|
||||
uvScale:Float = 1) {
|
||||
var worker = new ResourceLoaderWorker(() -> {
|
||||
var diffuseTex = ResourceLoader.getTexture('data/interiors_mbu/${baseTexture}').resource;
|
||||
diffuseTex.wrap = Repeat;
|
||||
|
|
@ -216,7 +217,7 @@ class DifBuilder {
|
|||
noiseTex.wrap = Repeat;
|
||||
noiseTex.mipMap = Nearest;
|
||||
var shader = new NoiseTileMaterial(diffuseTex, normalTex, noiseTex, shininess, specular, MarbleGame.instance.world.ambient,
|
||||
MarbleGame.instance.world.dirLight, MarbleGame.instance.world.dirLightDir, 1);
|
||||
MarbleGame.instance.world.dirLight, MarbleGame.instance.world.dirLightDir, uvScale);
|
||||
onFinish(shader);
|
||||
});
|
||||
worker.loadFile('interiors_mbu/${baseTexture}');
|
||||
|
|
@ -244,33 +245,33 @@ class DifBuilder {
|
|||
'interiors_mbu/plate_1.jpg' => (onFinish) -> createPhongMaterial(onFinish, 'plate.randomize.png', 'plate.normal.png', 8, new Vector(1, 1, 0.8, 1), 0.5),
|
||||
'interiors_mbu/tile_beginner.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_beginner.png', '', 40, new Vector(1, 1, 1, 1)),
|
||||
'interiors_mbu/tile_beginner_shadow.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_beginner.png', '_shadow', 40,
|
||||
new Vector(0.2, 0.2, 0.2, 0.2)),
|
||||
new Vector(0.2, 0.2, 0.2, 0.2), 1 / 4),
|
||||
'interiors_mbu/tile_beginner_red.jpg' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_beginner.png', '_red', 40, new Vector(1, 1, 1, 1)),
|
||||
'interiors_mbu/tile_beginner_red_shadow.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_beginner.png', '_red_shadow', 40,
|
||||
new Vector(0.2, 0.2, 0.2, 0.2)),
|
||||
new Vector(0.2, 0.2, 0.2, 0.2), 1 / 4),
|
||||
'interiors_mbu/tile_beginner_blue.jpg' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_beginner.png', '_blue', 40, new Vector(1, 1, 1, 1)),
|
||||
'interiors_mbu/tile_beginner_blue_shadow.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_beginner.png', '_blue_shadow', 40,
|
||||
new Vector(0.2, 0.2, 0.2, 0.2)),
|
||||
new Vector(0.2, 0.2, 0.2, 0.2), 1 / 4),
|
||||
'interiors_mbu/tile_intermediate.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_intermediate.png', '', 40, new Vector(1, 1, 1, 1)),
|
||||
'interiors_mbu/tile_intermediate_shadow.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_intermediate.png', '_shadow', 40,
|
||||
new Vector(0.2, 0.2, 0.2, 0.2)),
|
||||
new Vector(0.2, 0.2, 0.2, 0.2), 1 / 4),
|
||||
'interiors_mbu/tile_intermediate_red.jpg' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_intermediate.png', '_red', 40,
|
||||
new Vector(1, 1, 1, 1)),
|
||||
'interiors_mbu/tile_intermediate_red_shadow.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_intermediate.png', '_red_shadow', 40,
|
||||
new Vector(0.2, 0.2, 0.2, 0.2)),
|
||||
new Vector(0.2, 0.2, 0.2, 0.2), 1 / 4),
|
||||
'interiors_mbu/tile_intermediate_green.jpg' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_intermediate.png', '_green', 40,
|
||||
new Vector(1, 1, 1, 1)),
|
||||
'interiors_mbu/tile_intermediate_green_shadow.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_intermediate.png', '_green_shadow', 40,
|
||||
new Vector(0.2, 0.2, 0.2, 0.2)),
|
||||
new Vector(0.2, 0.2, 0.2, 0.2), 1 / 4),
|
||||
'interiors_mbu/tile_advanced.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_advanced.png', '', 40, new Vector(1, 1, 1, 1)),
|
||||
'interiors_mbu/tile_advanced_shadow.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_advanced.png', '_shadow', 40,
|
||||
new Vector(0.2, 0.2, 0.2, 0.2)),
|
||||
new Vector(0.2, 0.2, 0.2, 0.2), 1 / 4),
|
||||
'interiors_mbu/tile_advanced_blue.jpg' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_advanced.png', '_blue', 40, new Vector(1, 1, 1, 1)),
|
||||
'interiors_mbu/tile_advanced_blue_shadow.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_advanced.png', '_blue_shadow', 40,
|
||||
new Vector(0.2, 0.2, 0.2, 0.2)),
|
||||
new Vector(0.2, 0.2, 0.2, 0.2), 1 / 4),
|
||||
'interiors_mbu/tile_advanced_green.jpg' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_advanced.png', '_green', 40, new Vector(1, 1, 1, 1)),
|
||||
'interiors_mbu/tile_advanced_green_shadow.jpg' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_advanced.png', '_green_shadow', 40,
|
||||
new Vector(0.2, 0.2, 0.2, 0.2)),
|
||||
new Vector(0.2, 0.2, 0.2, 0.2), 1 / 4),
|
||||
'interiors_mbu/tile_underside.png' => (onFinish) -> createNoiseTileMaterial(onFinish, 'tile_underside.png', '', 40, new Vector(1, 1, 1, 1)),
|
||||
'interiors_mbu/wall_beginner.png' => (onFinish) -> createPhongMaterial(onFinish, 'wall_beginner.png', 'wall_mbu.normal.png', 12,
|
||||
new Vector(0.8, 0.8, 0.6, 1)),
|
||||
|
|
@ -710,8 +711,12 @@ class DifBuilder {
|
|||
texture.mipMap = Nearest;
|
||||
var exactName = StringTools.replace(texture.name, "data/", "");
|
||||
material = h3d.mat.Material.create(texture);
|
||||
if (shaderMaterialDict.exists(exactName)) {
|
||||
var retrievefunc = shaderMaterialDict[exactName];
|
||||
var matDictName = exactName;
|
||||
if (!shaderMaterialDict.exists(matDictName)) {
|
||||
matDictName = StringTools.replace(exactName, "multiplayer/interiors/mbu", "interiors_mbu");
|
||||
}
|
||||
if (shaderMaterialDict.exists(matDictName)) {
|
||||
var retrievefunc = shaderMaterialDict[matDictName];
|
||||
shaderWorker.addTask(fwd -> {
|
||||
retrievefunc(shad -> {
|
||||
material.mainPass.removeShader(material.textureShader);
|
||||
|
|
|
|||
|
|
@ -273,6 +273,7 @@ class Marble extends GameObject {
|
|||
|
||||
var forcefield:DtsObject;
|
||||
var helicopter:DtsObject;
|
||||
var megaHelicopter:DtsObject;
|
||||
var superBounceEnableTime:Float = -1e8;
|
||||
var shockAbsorberEnableTime:Float = -1e8;
|
||||
var helicopterEnableTime:Float = -1e8;
|
||||
|
|
@ -554,9 +555,21 @@ class Marble extends GameObject {
|
|||
this.helicopter.y = 1e8;
|
||||
this.helicopter.z = 1e8;
|
||||
|
||||
this.megaHelicopter = new DtsObject();
|
||||
this.megaHelicopter.dtsPath = "data/shapes/items/megahelicopter.dts";
|
||||
this.megaHelicopter.useInstancing = true;
|
||||
this.megaHelicopter.identifier = "MegaHelicopter";
|
||||
this.megaHelicopter.showSequences = true;
|
||||
this.megaHelicopter.isBoundingBoxCollideable = false;
|
||||
// this.addChild(this.helicopter);
|
||||
this.megaHelicopter.x = 1e8;
|
||||
this.megaHelicopter.y = 1e8;
|
||||
this.megaHelicopter.z = 1e8;
|
||||
|
||||
var worker = new ResourceLoaderWorker(onFinish);
|
||||
worker.addTask(fwd -> level.addDtsObject(this.forcefield, fwd));
|
||||
worker.addTask(fwd -> level.addDtsObject(this.helicopter, fwd));
|
||||
worker.addTask(fwd -> level.addDtsObject(this.megaHelicopter, fwd));
|
||||
worker.run();
|
||||
}
|
||||
|
||||
|
|
@ -1873,6 +1886,7 @@ class Marble extends GameObject {
|
|||
marbleUpdate.netFlags = this.netFlags;
|
||||
marbleUpdate.gravityDirection = this.currentUp;
|
||||
marbleUpdate.serialize(b);
|
||||
|
||||
return b.getBytes();
|
||||
}
|
||||
|
||||
|
|
@ -1987,7 +2001,7 @@ class Marble extends GameObject {
|
|||
lastMove = move.move;
|
||||
}
|
||||
}
|
||||
if (move == null && !this.controllable) {
|
||||
if (move == null && !this.controllable || this.mode == Finish) {
|
||||
var axis = moveMotionDir != null ? moveMotionDir : new Vector(0, -1, 0);
|
||||
var innerMove = lastMove;
|
||||
if (innerMove == null) {
|
||||
|
|
@ -2243,6 +2257,7 @@ class Marble extends GameObject {
|
|||
var shockEnabled = isShockAbsorberEnabled(timeState);
|
||||
var bounceEnabled = isSuperBounceEnabled(timeState);
|
||||
var helicopterEnabled = isHelicopterEnabled(timeState);
|
||||
var megaEnabled = isMegaMarbleEnabled(timeState);
|
||||
var selfMarble = level.marble == cast this;
|
||||
if (selfMarble) {
|
||||
if (shockEnabled) {
|
||||
|
|
@ -2264,15 +2279,30 @@ class Marble extends GameObject {
|
|||
this.forcefield.y = 1e8;
|
||||
this.forcefield.z = 1e8;
|
||||
}
|
||||
if (helicopterEnabled) {
|
||||
this.helicopter.setPosition(x, y, z);
|
||||
this.helicopter.setRotationQuat(this.level.getOrientationQuat(timeState.currentAttemptTime));
|
||||
if (selfMarble)
|
||||
this.helicopterSound.pause = false;
|
||||
} else {
|
||||
if (megaEnabled) {
|
||||
this.helicopter.setPosition(1e8, 1e8, 1e8);
|
||||
if (selfMarble)
|
||||
this.helicopterSound.pause = true;
|
||||
if (helicopterEnabled) {
|
||||
this.megaHelicopter.setPosition(x, y, z);
|
||||
this.megaHelicopter.setRotationQuat(this.level.getOrientationQuat(timeState.currentAttemptTime));
|
||||
if (selfMarble)
|
||||
this.helicopterSound.pause = false;
|
||||
} else {
|
||||
this.megaHelicopter.setPosition(1e8, 1e8, 1e8);
|
||||
if (selfMarble)
|
||||
this.helicopterSound.pause = true;
|
||||
}
|
||||
} else {
|
||||
this.megaHelicopter.setPosition(1e8, 1e8, 1e8);
|
||||
if (helicopterEnabled) {
|
||||
this.helicopter.setPosition(x, y, z);
|
||||
this.helicopter.setRotationQuat(this.level.getOrientationQuat(timeState.currentAttemptTime));
|
||||
if (selfMarble)
|
||||
this.helicopterSound.pause = false;
|
||||
} else {
|
||||
this.helicopter.setPosition(1e8, 1e8, 1e8);
|
||||
if (selfMarble)
|
||||
this.helicopterSound.pause = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -873,6 +873,10 @@ class MarbleWorld extends Scheduler {
|
|||
marble.setMode(Play);
|
||||
|
||||
this.playGui.setCenterText('go');
|
||||
|
||||
var huntMode = cast(this.gameMode, HuntMode);
|
||||
|
||||
huntMode.freeSpawns();
|
||||
}
|
||||
}
|
||||
if (this.multiplayerStarted) {
|
||||
|
|
@ -1253,6 +1257,49 @@ class MarbleWorld extends Scheduler {
|
|||
}
|
||||
}
|
||||
|
||||
// MP ONLY
|
||||
public function completeRestart() {
|
||||
for (id => client in Net.clientIdMap) {
|
||||
client.state = LOBBY;
|
||||
client.lobbyReady = false;
|
||||
}
|
||||
Net.hostReady = false;
|
||||
Net.lobbyHostReady = false;
|
||||
Net.lobbyClientReady = false;
|
||||
|
||||
this.finishTime = null;
|
||||
this.multiplayerStarted = false;
|
||||
this.timeState.ticks = 0;
|
||||
|
||||
for (marble in this.marbles) {
|
||||
restart(marble, true);
|
||||
}
|
||||
|
||||
showPreGame();
|
||||
|
||||
serverStartTicks = 0;
|
||||
startTime = 1e8;
|
||||
}
|
||||
|
||||
public function partialRestart() {
|
||||
this.finishTime = null;
|
||||
this.multiplayerStarted = false;
|
||||
this.timeState.ticks = 0;
|
||||
|
||||
for (marble in this.marbles) {
|
||||
restart(marble, true);
|
||||
}
|
||||
|
||||
startTime = this.timeState.timeSinceLoad + 3.5;
|
||||
|
||||
if (Net.isHost) {
|
||||
haxe.Timer.delay(() -> {
|
||||
NetCommands.setStartTicks(this.timeState.ticks);
|
||||
}, 500);
|
||||
}
|
||||
this.gameMode.onRestart();
|
||||
}
|
||||
|
||||
public function getWorldStateForClientJoin() {
|
||||
var packets = [];
|
||||
// First, gem spawn packet
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package gui;
|
||||
|
||||
import net.NetCommands;
|
||||
import net.Net;
|
||||
import h3d.shader.AlphaChannel;
|
||||
import hxd.res.BitmapFont;
|
||||
|
|
@ -109,8 +110,13 @@ class MPEndGameGui extends GuiImage {
|
|||
var restartBtn = new GuiButton(loadButtonImagesExt("data/ui/mp/end/restart"));
|
||||
restartBtn.position = new Vector(5, 7);
|
||||
restartBtn.extent = new Vector(49, 49);
|
||||
restartBtn.pressedAction = (e) -> {
|
||||
MarbleGame.canvas.popDialog(this);
|
||||
MarbleGame.instance.paused = false;
|
||||
NetCommands.completeRestartGame();
|
||||
}
|
||||
if (Net.isClient) {
|
||||
lobbyBtn.disabled = true;
|
||||
restartBtn.disabled = true;
|
||||
}
|
||||
sidebar.addChild(restartBtn);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package gui;
|
||||
|
||||
import net.NetCommands;
|
||||
import h2d.filter.DropShadow;
|
||||
import net.Net;
|
||||
import src.MarbleGame;
|
||||
|
|
@ -43,6 +44,11 @@ class MPExitGameDlg extends GuiControl {
|
|||
partialRestart.position = new Vector(133, 80);
|
||||
partialRestart.extent = new Vector(94, 45);
|
||||
partialRestart.vertSizing = Top;
|
||||
partialRestart.pressedAction = (e) -> {
|
||||
MarbleGame.instance.paused = false;
|
||||
NetCommands.partialRestartGame();
|
||||
MarbleGame.canvas.popDialog(this);
|
||||
}
|
||||
dialogImg.addChild(partialRestart);
|
||||
if (!Net.isHost) {
|
||||
partialRestart.disabled = true;
|
||||
|
|
@ -93,6 +99,11 @@ class MPExitGameDlg extends GuiControl {
|
|||
completeRestart.position = new Vector(224, 80);
|
||||
completeRestart.extent = new Vector(104, 45);
|
||||
completeRestart.vertSizing = Top;
|
||||
completeRestart.pressedAction = (e) -> {
|
||||
MarbleGame.instance.paused = false;
|
||||
NetCommands.completeRestartGame();
|
||||
MarbleGame.canvas.popDialog(this);
|
||||
}
|
||||
dialogImg.addChild(completeRestart);
|
||||
if (!Net.isHost) {
|
||||
completeRestart.disabled = true;
|
||||
|
|
|
|||
|
|
@ -114,6 +114,12 @@ class HuntMode extends NullMode {
|
|||
}
|
||||
}
|
||||
|
||||
public function freeSpawns() {
|
||||
for (i in 0...playerSpawnPoints.length) {
|
||||
spawnPointTaken[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
override function getRespawnTransform(marble:Marble) {
|
||||
var lastContactPos = marble.lastContactPosition;
|
||||
if (lastContactPos == null) {
|
||||
|
|
@ -181,12 +187,17 @@ class HuntMode extends NullMode {
|
|||
}
|
||||
}
|
||||
}
|
||||
for (i in 0...spawnPointTaken.length) {
|
||||
spawnPointTaken[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setupGems() {
|
||||
hideExisting();
|
||||
this.activeGems = [];
|
||||
this.activeGemSpawnGroup = [];
|
||||
this.rng.setSeed(cast Math.random() * 10000);
|
||||
this.rng2.setSeed(cast Math.random() * 10000);
|
||||
prepareGems();
|
||||
spawnHuntGems();
|
||||
}
|
||||
|
|
@ -362,6 +373,7 @@ class HuntMode extends NullMode {
|
|||
if (level.finishTime != null)
|
||||
return;
|
||||
|
||||
AudioManager.playSound(ResourceLoader.getResource("data/sound/firewrks.wav", ResourceLoader.getAudio, @:privateAccess level.soundResources));
|
||||
// AudioManager.playSound(ResourceLoader.getResource('data/sound/finish.wav', ResourceLoader.getAudio, @:privateAccess level.soundResources));
|
||||
level.finishTime = level.timeState.clone();
|
||||
level.marble.setMode(Finish);
|
||||
|
|
@ -389,7 +401,7 @@ class HuntMode extends NullMode {
|
|||
}
|
||||
|
||||
level.schedule(level.timeState.currentAttemptTime + 2, () -> {
|
||||
MarbleGame.canvas.setContent(new MPEndGameGui());
|
||||
MarbleGame.canvas.pushDialog(new MPEndGameGui());
|
||||
level.setCursorLock(false);
|
||||
return 0;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package net;
|
||||
|
||||
import gui.MPExitGameDlg;
|
||||
import gui.MPEndGameGui;
|
||||
import gui.MPPreGameDlg;
|
||||
import gui.MPPlayMissionGui;
|
||||
import net.ClientConnection.NetPlatform;
|
||||
|
|
@ -308,21 +310,36 @@ class NetCommands {
|
|||
}
|
||||
}
|
||||
|
||||
@:rpc(server) public static function restartGame() {
|
||||
var world = MarbleGame.instance.world;
|
||||
if (Net.isHost) {
|
||||
world.startTime = 1e8;
|
||||
haxe.Timer.delay(() -> world.allClientsReady(), 1500);
|
||||
}
|
||||
@:rpc(server) public static function completeRestartGame() {
|
||||
if (Net.isClient) {
|
||||
var gui = MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1];
|
||||
if (gui is EndGameGui) {
|
||||
var egg = cast(gui, EndGameGui);
|
||||
if (gui is MPEndGameGui || gui is MPExitGameDlg) {
|
||||
MarbleGame.instance.paused = false;
|
||||
MarbleGame.canvas.popDialog(gui);
|
||||
// egg.retryFunc(null);
|
||||
world.restartMultiplayerState();
|
||||
}
|
||||
}
|
||||
world.multiplayerStarted = false;
|
||||
var world = MarbleGame.instance.world;
|
||||
world.completeRestart();
|
||||
if (Net.isClient) {
|
||||
world.restartMultiplayerState();
|
||||
}
|
||||
}
|
||||
|
||||
@:rpc(server) public static function partialRestartGame() {
|
||||
if (Net.isClient) {
|
||||
var gui = MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1];
|
||||
if (gui is MPEndGameGui || gui is MPExitGameDlg) {
|
||||
MarbleGame.instance.paused = false;
|
||||
MarbleGame.canvas.popDialog(gui);
|
||||
// egg.retryFunc(null);
|
||||
}
|
||||
}
|
||||
var world = MarbleGame.instance.world;
|
||||
world.partialRestart();
|
||||
if (Net.isClient) {
|
||||
world.restartMultiplayerState();
|
||||
}
|
||||
}
|
||||
|
||||
@:rpc(server) public static function ping(sendTime:Float) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class NoiseTileMaterial extends hxsl.Shader {
|
|||
@param var ambientLight:Vec3;
|
||||
@param var dirLight:Vec3;
|
||||
@param var dirLightDir:Vec3;
|
||||
@param var secondaryMapUvFactor:Float;
|
||||
@param var uvScaleFactor:Float;
|
||||
@global var camera:{
|
||||
var position:Vec3;
|
||||
@var var dir:Vec3;
|
||||
|
|
@ -40,7 +40,7 @@ class NoiseTileMaterial extends hxsl.Shader {
|
|||
return saturate(result);
|
||||
}
|
||||
function vertex() {
|
||||
calculatedUV = input.uv;
|
||||
calculatedUV = input.uv * uvScaleFactor;
|
||||
fragLightW = step(0, dot(dirLight, input.normal));
|
||||
}
|
||||
function fragment() {
|
||||
|
|
@ -77,7 +77,7 @@ class NoiseTileMaterial extends hxsl.Shader {
|
|||
var outCol = diffuse + noiseAdd;
|
||||
|
||||
var n = transformedNormal;
|
||||
var nf = unpackNormal(normalMap.get(calculatedUV * secondaryMapUvFactor));
|
||||
var nf = unpackNormal(normalMap.get(calculatedUV));
|
||||
var tanX = transformedTangent.xyz.normalize();
|
||||
var tanY = n.cross(tanX) * transformedTangent.w;
|
||||
transformedNormal = (nf.x * tanX + nf.y * tanY + nf.z * n).normalize();
|
||||
|
|
@ -116,6 +116,6 @@ class NoiseTileMaterial extends hxsl.Shader {
|
|||
this.ambientLight = ambientLight.clone();
|
||||
this.dirLight = dirLight.clone();
|
||||
this.dirLightDir = dirLightDir.clone();
|
||||
this.secondaryMapUvFactor = secondaryMapUvFactor;
|
||||
this.uvScaleFactor = secondaryMapUvFactor;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue