add some configurable options to replaystuff, fix count50, add misses, ..
This commit is contained in:
parent
8725e0b31c
commit
4117357f31
|
@ -29,17 +29,17 @@ import yugecin.opsudance.core.Entrypoint;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
||||||
import static itdelatrisu.opsu.GameData.*;
|
import static itdelatrisu.opsu.GameData.*;
|
||||||
import static itdelatrisu.opsu.Utils.*;
|
import static itdelatrisu.opsu.Utils.*;
|
||||||
import static itdelatrisu.opsu.ui.animations.AnimationEquation.*;
|
import static itdelatrisu.opsu.ui.animations.AnimationEquation.*;
|
||||||
import static yugecin.opsudance.core.InstanceContainer.*;
|
import static yugecin.opsudance.core.InstanceContainer.*;
|
||||||
|
import static yugecin.opsudance.options.Options.*;
|
||||||
|
|
||||||
public class ReplayPlayback {
|
public class ReplayPlayback
|
||||||
|
{
|
||||||
private static final boolean HIDEMOUSEBTNS = true;
|
|
||||||
|
|
||||||
private final HitData hitdata;
|
private final HitData hitdata;
|
||||||
public final Replay replay;
|
public final Replay replay;
|
||||||
public ReplayFrame currentFrame;
|
public ReplayFrame currentFrame;
|
||||||
|
@ -59,19 +59,20 @@ public class ReplayPlayback {
|
||||||
private String currentAcc;
|
private String currentAcc;
|
||||||
private int currentAccWidth;
|
private int currentAccWidth;
|
||||||
private final int ACCMAXWIDTH;
|
private final int ACCMAXWIDTH;
|
||||||
private float failposx, failposy;
|
|
||||||
|
|
||||||
private int c300, c100, c50;
|
private int c300, c100, c50, fakecmiss;
|
||||||
|
|
||||||
private Image hitImage;
|
private Image hitImage;
|
||||||
private int hitImageTimer = 0;
|
private int hitImageTimer = 0;
|
||||||
private boolean missed;
|
private boolean knockedout;
|
||||||
|
private final LinkedList<MissIndicator> missIndicators;
|
||||||
|
|
||||||
private Image gradeImage;
|
private Image gradeImage;
|
||||||
|
|
||||||
private static final Color missedColor = new Color(0.4f, 0.4f, 0.4f, 1f);
|
private static final Color missedColor = new Color(0.4f, 0.4f, 0.4f, 1f);
|
||||||
|
|
||||||
public ReplayPlayback(Replay replay, HitData hitdata, Color color, ReplayCursor cursor) {
|
public ReplayPlayback(Replay replay, HitData hitdata, Color color, ReplayCursor cursor) {
|
||||||
|
this.missIndicators = new LinkedList<>();
|
||||||
this.replay = replay;
|
this.replay = replay;
|
||||||
this.hitdata = hitdata;
|
this.hitdata = hitdata;
|
||||||
resetFrameIndex();
|
resetFrameIndex();
|
||||||
|
@ -133,17 +134,20 @@ public class ReplayPlayback {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateGradeImage() {
|
private void updateGradeImage() {
|
||||||
if (missed) {
|
if (knockedout || !OPTION_RP_SHOW_GRADES.state) {
|
||||||
gradeImage = null;
|
gradeImage = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean silver = (replay.mods & 0x408) > 0 && (replay.mods & 0x200) == 0;
|
boolean silver = (replay.mods & 0x408) > 0 && (replay.mods & 0x200) == 0;
|
||||||
GameData.Grade grade = GameData.getGrade(c300, c100, c50, 0, silver);
|
GameData.Grade grade = GameData.getGrade(c300, c100, c50, fakecmiss, silver);
|
||||||
|
|
||||||
if (grade == GameData.Grade.NULL) {
|
if (grade == GameData.Grade.NULL) {
|
||||||
gradeImage = null;
|
if ((replay.mods & 0x8) > 0 && (replay.mods & 0x200) == 0) {
|
||||||
return;
|
grade = GameData.Grade.SSH;
|
||||||
|
} else {
|
||||||
|
grade = GameData.Grade.SS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
gradeImage = grade.getSmallImage().getScaledCopy(SQSIZE + 5, SQSIZE + 5);
|
gradeImage = grade.getSmallImage().getScaledCopy(SQSIZE + 5, SQSIZE + 5);
|
||||||
}
|
}
|
||||||
|
@ -160,16 +164,16 @@ public class ReplayPlayback {
|
||||||
}
|
}
|
||||||
|
|
||||||
hitImageTimer += renderdelta;
|
hitImageTimer += renderdelta;
|
||||||
if (!missed && hitImageTimer > HITIMAGETIMERFADEEND) {
|
if (!knockedout && hitImageTimer > HITIMAGETIMERFADEEND) {
|
||||||
hitImage = null;
|
hitImage = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Color color = new Color(1f, 1f, 1f, 1f);
|
Color color = new Color(1f, 1f, 1f, 1f);
|
||||||
if (!missed && hitImageTimer > HITIMAGETIMERFADESTART) {
|
if (!knockedout && hitImageTimer > HITIMAGETIMERFADESTART) {
|
||||||
color.a = (HITIMAGETIMERFADEEND - hitImageTimer) / HITIMAGETIMERFADEDELTA;
|
color.a = (HITIMAGETIMERFADEEND - hitImageTimer) / HITIMAGETIMERFADEDELTA;
|
||||||
}
|
}
|
||||||
if (missed) {
|
if (knockedout) {
|
||||||
if (hitImageTimer > HITIMAGEDEADFADE) {
|
if (hitImageTimer > HITIMAGEDEADFADE) {
|
||||||
this.color.a = color.a = 0f;
|
this.color.a = color.a = 0f;
|
||||||
} else {
|
} else {
|
||||||
|
@ -195,8 +199,8 @@ public class ReplayPlayback {
|
||||||
return UNITHEIGHT * (1f - AnimationEquation.OUT_QUART.calc((hitImageTimer - HITIMAGEDEADFADE) / SHRINKTIME));
|
return UNITHEIGHT * (1f - AnimationEquation.OUT_QUART.calc((hitImageTimer - HITIMAGEDEADFADE) / SHRINKTIME));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void render(int renderdelta, Graphics g, float ypos, int time) {
|
public void render(int renderdelta, Graphics g, float ypos, int time)
|
||||||
|
{
|
||||||
while (nextFrame != null && nextFrame.getTime() < time) {
|
while (nextFrame != null && nextFrame.getTime() < time) {
|
||||||
currentFrame = nextFrame;
|
currentFrame = nextFrame;
|
||||||
processKeys();
|
processKeys();
|
||||||
|
@ -209,7 +213,7 @@ public class ReplayPlayback {
|
||||||
}
|
}
|
||||||
processKeys();
|
processKeys();
|
||||||
g.setColor(color);
|
g.setColor(color);
|
||||||
if (!missed) {
|
if (!knockedout) {
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
if (keydelay[i] > 0) {
|
if (keydelay[i] > 0) {
|
||||||
g.fillRect(SQSIZE * i, ypos + 5, SQSIZE, SQSIZE);
|
g.fillRect(SQSIZE * i, ypos + 5, SQSIZE, SQSIZE);
|
||||||
|
@ -240,36 +244,44 @@ public class ReplayPlayback {
|
||||||
while (!hitdata.time50.isEmpty() && hitdata.time50.getFirst() <= time) {
|
while (!hitdata.time50.isEmpty() && hitdata.time50.getFirst() <= time) {
|
||||||
hitdata.time50.removeFirst();
|
hitdata.time50.removeFirst();
|
||||||
hitImageTimer = 0;
|
hitImageTimer = 0;
|
||||||
hitImage = GameData.hitResults[GameData.HIT_100].getScaledCopy(SQSIZE + 5, SQSIZE + 5);
|
hitImage = GameData.hitResults[GameData.HIT_50].getScaledCopy(SQSIZE + 5, SQSIZE + 5);
|
||||||
c50++;
|
c50++;
|
||||||
hitschanged = true;
|
hitschanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while (!hitdata.timeCombobreaks.isEmpty() && hitdata.timeCombobreaks.getFirst() <= time) {
|
||||||
|
hitdata.timeCombobreaks.removeFirst();
|
||||||
|
hitImageTimer = 0;
|
||||||
|
hitImage = GameData.hitResults[GameData.HIT_MISS].getScaledCopy(SQSIZE + 5, SQSIZE + 5);
|
||||||
|
fakecmiss++;
|
||||||
|
hitschanged = true;
|
||||||
|
if (OPTION_RP_SHOW_MISSES.state) {
|
||||||
|
float posx = currentFrame.getScaledX();
|
||||||
|
float posy = currentFrame.getScaledY();
|
||||||
|
if (hr) {
|
||||||
|
posy = height - posy;
|
||||||
|
}
|
||||||
|
this.missIndicators.add(new MissIndicator(posx, posy));
|
||||||
|
}
|
||||||
|
if (OPTION_RP_KNOCKOUT.state) {
|
||||||
|
knockedout = true;
|
||||||
|
color = new Color(missedColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hitschanged) {
|
if (hitschanged) {
|
||||||
updateGradeImage();
|
updateGradeImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (time >= hitdata.combobreaktime) {
|
|
||||||
if (!missed) {
|
|
||||||
failposx = currentFrame.getScaledX();
|
|
||||||
failposy = currentFrame.getScaledY();
|
|
||||||
if (hr) {
|
|
||||||
failposy = height - failposy;
|
|
||||||
}
|
}
|
||||||
}
|
int xpos = SQSIZE * (OPTION_RP_SHOW_MOUSECOLUMN.state ? 5 : 3);
|
||||||
missed = true;
|
if (OPTION_RP_SHOW_ACC.state) {
|
||||||
color = new Color(missedColor);
|
|
||||||
hitImageTimer = 0;
|
|
||||||
hitImage = GameData.hitResults[GameData.HIT_MISS].getScaledCopy(SQSIZE + 5, SQSIZE + 5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int xpos = SQSIZE * (HIDEMOUSEBTNS ? 3 : 5);
|
|
||||||
Fonts.SMALLBOLD.drawString(xpos + ACCMAXWIDTH - currentAccWidth - 10, ypos, currentAcc, new Color(.4f, .4f, .4f, color.a));
|
Fonts.SMALLBOLD.drawString(xpos + ACCMAXWIDTH - currentAccWidth - 10, ypos, currentAcc, new Color(.4f, .4f, .4f, color.a));
|
||||||
xpos += ACCMAXWIDTH;
|
xpos += ACCMAXWIDTH;
|
||||||
if (!missed && gradeImage != null) {
|
|
||||||
gradeImage.draw(xpos, ypos);
|
|
||||||
}
|
}
|
||||||
|
if (gradeImage != null) {
|
||||||
|
gradeImage.draw(xpos, ypos);
|
||||||
xpos += SQSIZE + 10;
|
xpos += SQSIZE + 10;
|
||||||
|
}
|
||||||
Fonts.SMALLBOLD.drawString(xpos, ypos, this.player, color);
|
Fonts.SMALLBOLD.drawString(xpos, ypos, this.player, color);
|
||||||
xpos += playerwidth;
|
xpos += playerwidth;
|
||||||
if (!this.mods.isEmpty()) {
|
if (!this.mods.isEmpty()) {
|
||||||
|
@ -277,18 +289,29 @@ public class ReplayPlayback {
|
||||||
xpos += modwidth;
|
xpos += modwidth;
|
||||||
}
|
}
|
||||||
xpos += 10;
|
xpos += 10;
|
||||||
|
if (OPTION_RP_SHOW_HITS.state) {
|
||||||
showHitImage(renderdelta, xpos, ypos);
|
showHitImage(renderdelta, xpos, ypos);
|
||||||
if (missed) {
|
}
|
||||||
if (hitImageTimer < HITIMAGEDEADFADE) {
|
if (OPTION_RP_SHOW_MISSES.state) {
|
||||||
float progress = (float) hitImageTimer / HITIMAGEDEADFADE;
|
final Iterator<MissIndicator> iter = this.missIndicators.iterator();
|
||||||
float failposy = this.failposy + 50f * OUT_QUART.calc(progress);
|
while (iter.hasNext()) {
|
||||||
|
final MissIndicator mi = iter.next();
|
||||||
|
if (mi.timer >= HITIMAGEDEADFADE) {
|
||||||
|
iter.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float progress = (float) mi.timer / HITIMAGEDEADFADE;
|
||||||
|
float failposy = mi.posy + 50f * OUT_QUART.calc(progress);
|
||||||
Color col = new Color(originalcolor);
|
Color col = new Color(originalcolor);
|
||||||
col.a = 1f - IN_QUAD.calc(clamp(progress * 2f, 0f, 1f));
|
col.a = 1f - IN_QUAD.calc(clamp(progress * 2f, 0f, 1f));
|
||||||
Fonts.SMALLBOLD.drawString(failposx - playerwidth / 2, failposy, player, col);
|
Fonts.SMALLBOLD.drawString(mi.posx - playerwidth / 2, failposy, player, col);
|
||||||
Color failimgcol = new Color(1f, 1f, 1f, col.a);
|
Color failimgcol = new Color(1f, 1f, 1f, col.a);
|
||||||
Image failimg = hitResults[HIT_MISS].getScaledCopy(SQSIZE + 5, SQSIZE + 5);
|
Image failimg = hitResults[HIT_MISS].getScaledCopy(SQSIZE + 5, SQSIZE + 5);
|
||||||
failimg.draw(failposx + playerwidth / 2 + 5, failposy + 2f, failimgcol);
|
failimg.draw(mi.posx + playerwidth / 2 + 5, failposy + 2f, failimgcol);
|
||||||
|
mi.timer += renderdelta;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (knockedout) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int y = currentFrame.getScaledY();
|
int y = currentFrame.getScaledY();
|
||||||
|
@ -300,32 +323,44 @@ public class ReplayPlayback {
|
||||||
|
|
||||||
public boolean shouldDrawCursor()
|
public boolean shouldDrawCursor()
|
||||||
{
|
{
|
||||||
return !missed;
|
return !knockedout;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processKeys() {
|
private void processKeys() {
|
||||||
int keys = currentFrame.getKeys();
|
int keys = currentFrame.getKeys();
|
||||||
int KEY_DELAY = 10;
|
|
||||||
if ((keys & 5) == 5) {
|
if ((keys & 5) == 5) {
|
||||||
keydelay[0] = KEY_DELAY;
|
keydelay[0] = OPTION_RP_KEYPRESS_DELAY.val;
|
||||||
}
|
}
|
||||||
if ((keys & 10) == 10) {
|
if ((keys & 10) == 10) {
|
||||||
keydelay[1] = KEY_DELAY;
|
keydelay[1] = OPTION_RP_KEYPRESS_DELAY.val;
|
||||||
}
|
}
|
||||||
if ((keys ^ 5) == 4) {
|
if ((keys ^ 5) == 4) {
|
||||||
keydelay[2] = KEY_DELAY;
|
keydelay[2] = OPTION_RP_KEYPRESS_DELAY.val;
|
||||||
}
|
}
|
||||||
if ((keys ^ 10) == 8) {
|
if ((keys ^ 10) == 8) {
|
||||||
keydelay[3] = KEY_DELAY;
|
keydelay[3] = OPTION_RP_KEYPRESS_DELAY.val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class HitData {
|
private static class MissIndicator
|
||||||
|
{
|
||||||
|
private float posx, posy;
|
||||||
|
private int timer;
|
||||||
|
|
||||||
int combobreaktime = -1;
|
private MissIndicator(float posx, float posy)
|
||||||
|
{
|
||||||
|
this.posx = posx;
|
||||||
|
this.posy = posy;
|
||||||
|
this.timer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HitData
|
||||||
|
{
|
||||||
LinkedList<Integer> time300 = new LinkedList<>();
|
LinkedList<Integer> time300 = new LinkedList<>();
|
||||||
LinkedList<Integer> time100 = new LinkedList<>();
|
LinkedList<Integer> time100 = new LinkedList<>();
|
||||||
LinkedList<Integer> time50 = new LinkedList<>();
|
LinkedList<Integer> time50 = new LinkedList<>();
|
||||||
|
LinkedList<Integer> timeCombobreaks = new LinkedList<>();
|
||||||
LinkedList<AccData> acc = new LinkedList<>();
|
LinkedList<AccData> acc = new LinkedList<>();
|
||||||
LinkedList<ComboData> combo = new LinkedList<>();
|
LinkedList<ComboData> combo = new LinkedList<>();
|
||||||
|
|
||||||
|
@ -339,11 +374,11 @@ public class ReplayPlayback {
|
||||||
while (true) {
|
while (true) {
|
||||||
byte[] time = new byte[4];
|
byte[] time = new byte[4];
|
||||||
int rd = in.read(time);
|
int rd = in.read(time);
|
||||||
if (rd == 0) {
|
if (rd <= 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (rd != 4) {
|
if (rd != 4) {
|
||||||
throw new RuntimeException();
|
throw new RuntimeException("expected 4 bytes, got " + rd);
|
||||||
}
|
}
|
||||||
byte[] _time = { time[3], time[2], time[1], time[0] };
|
byte[] _time = { time[3], time[2], time[1], time[0] };
|
||||||
lasttime = ByteBuffer.wrap(_time).getInt();
|
lasttime = ByteBuffer.wrap(_time).getInt();
|
||||||
|
@ -379,30 +414,22 @@ public class ReplayPlayback {
|
||||||
int c = ByteBuffer.wrap(_time).getInt();
|
int c = ByteBuffer.wrap(_time).getInt();
|
||||||
combo.add(new ComboData(lasttime, c));
|
combo.add(new ComboData(lasttime, c));
|
||||||
if (c < lastcombo) {
|
if (c < lastcombo) {
|
||||||
combobreaktime = lasttime;
|
timeCombobreaks.add(lasttime);
|
||||||
} else {
|
|
||||||
lastcombo = c;
|
|
||||||
}
|
}
|
||||||
|
lastcombo = c;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException();
|
throw new RuntimeException("unexpected data");
|
||||||
}
|
|
||||||
if (combobreaktime != -1) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (combobreaktime == -1) {
|
if (lasttime == -1) {
|
||||||
combobreaktime = lasttime;
|
|
||||||
}
|
|
||||||
if (combobreaktime == -1) {
|
|
||||||
throw new RuntimeException("nodata");
|
throw new RuntimeException("nodata");
|
||||||
}
|
}
|
||||||
Entrypoint.sout(String.format(
|
Entrypoint.sout(String.format(
|
||||||
"%s combobreak at %d, lastcombo %d lastacc %f",
|
"%s lastcombo %d lasttime %d",
|
||||||
file.getName(),
|
file.getName(),
|
||||||
combobreaktime,
|
|
||||||
lastcombo,
|
lastcombo,
|
||||||
acc.getLast().acc
|
lasttime
|
||||||
));
|
));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
@ -446,5 +473,4 @@ public class ReplayPlayback {
|
||||||
this.combo = combo;
|
this.combo = combo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,6 +161,14 @@ public class OptionGroups {
|
||||||
OPTION_DANCE_RGB_CURSOR_INC,
|
OPTION_DANCE_RGB_CURSOR_INC,
|
||||||
OPTION_DANCE_CURSOR_TRAIL_OVERRIDE,
|
OPTION_DANCE_CURSOR_TRAIL_OVERRIDE,
|
||||||
}),
|
}),
|
||||||
|
new OptionTab("REPLAYSTUFF", new Option[] {
|
||||||
|
OPTION_RP_KNOCKOUT,
|
||||||
|
OPTION_RP_SHOW_MISSES,
|
||||||
|
OPTION_RP_SHOW_GRADES,
|
||||||
|
OPTION_RP_SHOW_HITS,
|
||||||
|
OPTION_RP_SHOW_ACC,
|
||||||
|
OPTION_RP_KEYPRESS_DELAY,
|
||||||
|
}),
|
||||||
new OptionTab("MISC", new Option[] {
|
new OptionTab("MISC", new Option[] {
|
||||||
OPTION_DANCE_HIDE_UI,
|
OPTION_DANCE_HIDE_UI,
|
||||||
OPTION_DANCE_REMOVE_BG,
|
OPTION_DANCE_REMOVE_BG,
|
||||||
|
|
|
@ -998,4 +998,19 @@ public class Options {
|
||||||
public static final ToggleOption OPTION_PIPPI_SLIDER_FOLLOW_EXPAND = new ToggleOption("Followcircle expand", "PippiFollowExpand", "Increase radius in followcircles", false);
|
public static final ToggleOption OPTION_PIPPI_SLIDER_FOLLOW_EXPAND = new ToggleOption("Followcircle expand", "PippiFollowExpand", "Increase radius in followcircles", false);
|
||||||
public static final ToggleOption OPTION_PIPPI_PREVENT_WOBBLY_STREAMS = new ToggleOption("Prevent wobbly streams", "PippiPreventWobblyStreams", "Force linear mover while doing streams to prevent wobbly pippi", true);
|
public static final ToggleOption OPTION_PIPPI_PREVENT_WOBBLY_STREAMS = new ToggleOption("Prevent wobbly streams", "PippiPreventWobblyStreams", "Force linear mover while doing streams to prevent wobbly pippi", true);
|
||||||
|
|
||||||
|
public static final ToggleOption
|
||||||
|
OPTION_RP_KNOCKOUT = new ToggleOption("Knockout", "ReplayKnockout", "Remove replays on combobreaks.", true),
|
||||||
|
OPTION_RP_SHOW_MISSES = new ToggleOption("Show misses", "ReplayShowMiss", "Show falling miss indicators.", true),
|
||||||
|
OPTION_RP_SHOW_GRADES = new ToggleOption("Show grades", "ReplayShowGrades", "Show grades next to players.", true),
|
||||||
|
OPTION_RP_SHOW_HITS = new ToggleOption("Show hits", "ReplayShowHits", "Show miss, 50, 100 hits next to players.", true),
|
||||||
|
OPTION_RP_SHOW_MOUSECOLUMN = new ToggleOption("Mouse buttons column", "ReplayShowMouseColumn", "Preserve space for mouse button columns.", true),
|
||||||
|
OPTION_RP_SHOW_ACC = new ToggleOption("Show accuracy", "ReplayShowAcc", "Show accuracy next to players.", true);
|
||||||
|
|
||||||
|
public static final NumericOption
|
||||||
|
OPTION_RP_KEYPRESS_DELAY = new NumericOption("Key indicator delay", "ReplayKeyDelay", "How long the key indicator should show after the key being released (ms).", 10, 1, 50) {
|
||||||
|
@Override
|
||||||
|
public String getValueString() {
|
||||||
|
return String.valueOf(val);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user