Files
opsu-dance/src/yugecin/opsudance/ReplayPlayback.java
2018-04-14 18:21:59 +02:00

415 rader
11 KiB
Java

/*
* opsu!dance - fork of opsu! with cursordance auto
* Copyright (C) 2017 yugecin
*
* opsu!dance 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.
*
* opsu!dance 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 opsu!dance. If not, see <http://www.gnu.org/licenses/>.
*/
package yugecin.opsudance;
import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame;
import itdelatrisu.opsu.ui.Cursor;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import org.newdawn.slick.Color;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import yugecin.opsudance.core.DisplayContainer;
import yugecin.opsudance.core.Entrypoint;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.LinkedList;
public class ReplayPlayback {
private final DisplayContainer container;
private final HitData hitdata;
public final Replay replay;
public ReplayFrame currentFrame;
public ReplayFrame nextFrame;
private int frameIndex;
public Color color;
public Cursor cursor;
private int keydelay[];
public static final int SQSIZE = 15;
public static final int UNITHEIGHT = SQSIZE + 5;
private boolean hr;
private String player;
private String mods;
private int playerwidth;
private int modwidth;
private String currentAcc;
private int currentAccWidth;
private final int ACCMAXWIDTH;
private int c300, c100, c50;
private Image hitImage;
private int hitImageTimer = 0;
private boolean missed;
private Image gradeImage;
private static final Color missedColor = new Color(0.4f, 0.4f, 0.4f, 1f);
public ReplayPlayback(DisplayContainer container, Replay replay, HitData hitdata, Color color) {
this.container = container;
this.replay = replay;
this.hitdata = hitdata;
resetFrameIndex();
this.color = color;
Color cursorcolor = new Color(color);
//cursorcolor.a = 0.5f;
cursor = new Cursor(cursorcolor);
keydelay = new int[4];
this.player = replay.playerName;
this.playerwidth = Fonts.SMALLBOLD.getWidth(this.player);
this.mods = "";
this.currentAcc = "100,00%";
this.currentAccWidth = Fonts.SMALLBOLD.getWidth(currentAcc);
this.ACCMAXWIDTH = currentAccWidth + 10;
if ((replay.mods & 0x1) > 0) {
this.mods += "NF";
}
if ((replay.mods & 0x2) > 0) {
this.mods += "EZ";
}
if ((replay.mods & 0x8) > 0 && (replay.mods & 0x200) == 0) {
this.mods += "HD";
}
if ((replay.mods & 0x10) > 0) {
this.mods += "HR";
hr = true;
}
if ((replay.mods & 0x20) > 0) {
this.mods += "SD";
}
if ((replay.mods & 0x40) > 0) {
this.mods += "DT";
}
if ((replay.mods & 0x80) > 0) {
this.mods += "RL";
}
if ((replay.mods & 0x100) > 0) {
this.mods += "HT";
}
if ((replay.mods & 0x200) > 0) {
this.mods += "NC";
}
if ((replay.mods & 0x400) > 0) {
this.mods += "FL";
}
if ((replay.mods & 0x4000) > 0) {
this.mods += "PF";
}
if (this.mods.length() > 0) {
this.mods = " +" + this.mods;
this.modwidth = Fonts.SMALLBOLD.getWidth(this.mods);
}
updateGradeImage();
}
public void resetFrameIndex() {
frameIndex = 0;
currentFrame = replay.frames[frameIndex++];
nextFrame = replay.frames[frameIndex];
}
private void updateGradeImage() {
if (missed) {
gradeImage = null;
return;
}
boolean silver = (replay.mods & 0x408) > 0 && (replay.mods & 0x200) == 0;
GameData.Grade grade = GameData.getGrade(c300, c100, c50, 0, silver);
if (grade == GameData.Grade.NULL) {
gradeImage = null;
return;
}
gradeImage = grade.getSmallImage().getScaledCopy(SQSIZE + 5, SQSIZE + 5);
}
private int HITIMAGETIMEREXPAND = 200;
private int HITIMAGETIMERFADESTART = 500;
private int HITIMAGETIMERFADEEND = 700;
private float HITIMAGETIMERFADEDELTA = HITIMAGETIMERFADEEND - HITIMAGETIMERFADESTART;
private int HITIMAGEDEADFADE = 10000;
private float SHRINKTIME = 500f;
private void showHitImage(int renderdelta, int xpos, float ypos) {
if (hitImage == null) {
return;
}
hitImageTimer += renderdelta;
if (!missed && hitImageTimer > HITIMAGETIMERFADEEND) {
hitImage = null;
return;
}
Color color = new Color(1f, 1f, 1f, 1f);
if (!missed && hitImageTimer > HITIMAGETIMERFADESTART) {
color.a = (HITIMAGETIMERFADEEND - hitImageTimer) / HITIMAGETIMERFADEDELTA;
}
if (missed) {
if (hitImageTimer > HITIMAGEDEADFADE) {
this.color.a = color.a = 0f;
} else {
this.color.a = color.a = 1f - AnimationEquation.IN_CIRC.calc((float) hitImageTimer / HITIMAGEDEADFADE);
}
}
float scale = 1f;
float offset = 0f;
if (hitImageTimer < HITIMAGETIMEREXPAND) {
scale = AnimationEquation.OUT_EXPO.calc((float) hitImageTimer / HITIMAGETIMEREXPAND);
offset = UNITHEIGHT / 2f * (1f - scale);
}
hitImage.draw(xpos + offset, 2f + ypos + offset, scale, color);
}
public float getHeight() {
if (hitImageTimer < HITIMAGEDEADFADE) {
return UNITHEIGHT;
}
if (hitImageTimer >= HITIMAGEDEADFADE + SHRINKTIME) {
return 0f;
}
return UNITHEIGHT * (1f - AnimationEquation.OUT_QUART.calc((hitImageTimer - HITIMAGEDEADFADE) / SHRINKTIME));
}
public void render(int renderdelta, Graphics g, float ypos, int time) {
while (nextFrame != null && nextFrame.getTime() < time) {
currentFrame = nextFrame;
processKeys();
frameIndex++;
if (frameIndex >= replay.frames.length) {
nextFrame = null;
continue;
}
nextFrame = replay.frames[frameIndex];
}
processKeys();
g.setColor(color);
if (!missed) {
for (int i = 0; i < 4; i++) {
if (keydelay[i] > 0) {
g.fillRect(SQSIZE * i, ypos + 5, SQSIZE, SQSIZE);
}
keydelay[i] -= renderdelta;
}
boolean hitschanged = false;
while (!hitdata.acc.isEmpty() && hitdata.acc.getFirst().time <= time) {
currentAcc = String.format("%.2f%%", hitdata.acc.removeFirst().acc).replace('.', ',');
currentAccWidth = Fonts.SMALLBOLD.getWidth(currentAcc);
}
while (!hitdata.time300.isEmpty() && hitdata.time300.getFirst() <= time) {
hitdata.time300.removeFirst();
c300++;
hitschanged = true;
}
while (!hitdata.time100.isEmpty() && hitdata.time100.getFirst() <= time) {
hitdata.time100.removeFirst();
hitImageTimer = 0;
hitImage = GameData.hitResults[GameData.HIT_100].getScaledCopy(SQSIZE + 5, SQSIZE + 5);
c100++;
hitschanged = true;
}
while (!hitdata.time50.isEmpty() && hitdata.time50.getFirst() <= time) {
hitdata.time50.removeFirst();
hitImageTimer = 0;
hitImage = GameData.hitResults[GameData.HIT_100].getScaledCopy(SQSIZE + 5, SQSIZE + 5);
c50++;
hitschanged = true;
}
if (hitschanged) {
updateGradeImage();
}
if (time >= hitdata.combobreaktime) {
missed = true;
color = new Color(missedColor);
hitImageTimer = 0;
hitImage = GameData.hitResults[GameData.HIT_MISS].getScaledCopy(SQSIZE + 5, SQSIZE + 5);
}
}
Color fadecolor = new Color(1f, 1f, 1f, color.a);
int xpos = SQSIZE * 5;
Fonts.SMALLBOLD.drawString(xpos + ACCMAXWIDTH - currentAccWidth - 10, ypos, currentAcc, new Color(.4f, .4f, .4f, color.a));
xpos += ACCMAXWIDTH;
if (gradeImage != null) {
gradeImage.draw(xpos, ypos, fadecolor);
}
xpos += SQSIZE + 10;
Fonts.SMALLBOLD.drawString(xpos, ypos, this.player, color);
xpos += playerwidth;
if (!this.mods.isEmpty()) {
Fonts.SMALLBOLD.drawString(xpos, ypos, this.mods, fadecolor);
xpos += modwidth;
}
xpos += 10;
showHitImage(renderdelta, xpos, ypos);
if (missed) {
return;
}
int y = currentFrame.getScaledY();
if (hr) {
y = container.height - y;
}
cursor.setCursorPosition(renderdelta, currentFrame.getScaledX(), y);
cursor.draw(false);
}
private void processKeys() {
int keys = currentFrame.getKeys();
int KEY_DELAY = 10;
if ((keys & 5) == 5) {
keydelay[0] = KEY_DELAY;
}
if ((keys & 10) == 10) {
keydelay[1] = KEY_DELAY;
}
if ((keys ^ 5) == 4) {
keydelay[2] = KEY_DELAY;
}
if ((keys ^ 10) == 8) {
keydelay[3] = KEY_DELAY;
}
}
public static class HitData {
int combobreaktime = -1;
LinkedList<Integer> time300 = new LinkedList();
LinkedList<Integer> time100 = new LinkedList();
LinkedList<Integer> time50 = new LinkedList();
LinkedList<AccData> acc = new LinkedList();
public HitData(File file) {
try (InputStream in = new FileInputStream(file)) {
int lasttime = -1;
int lastcombo = 0;
int last300 = 0;
int last100 = 0;
int last50 = 0;
while (true) {
byte[] time = new byte[4];
int rd = in.read(time);
if (rd == 0) {
break;
}
if (rd != 4) {
throw new RuntimeException();
}
byte[] _time = { time[3], time[2], time[1], time[0] };
lasttime = ByteBuffer.wrap(_time).getInt();
int type = in.read();
if (type == -1) {
throw new RuntimeException();
}
if (in.read(time) != 4) {
throw new RuntimeException();
}
_time = new byte[] { time[3], time[2], time[1], time[0] };
switch (type) {
case 1:
int this100 = ByteBuffer.wrap(_time).getInt();
spread(time100, lasttime, this100 - last100);
last100 = this100;
break;
case 3:
int this300 = ByteBuffer.wrap(_time).getInt();
spread(time300, lasttime, this300 - last300);
last300 = this300;
break;
case 5:
int this50 = ByteBuffer.wrap(_time).getInt();
spread(time50, lasttime, this50 - last50);
last50 = this50;
break;
case 10:
acc.add(new AccData(lasttime, ByteBuffer.wrap(_time).getFloat()));
break;
case 12:
int c = ByteBuffer.wrap(_time).getInt();
if (c < lastcombo) {
combobreaktime = lasttime;
} else {
lastcombo = c;
}
break;
default:
throw new RuntimeException();
}
if (combobreaktime != -1) {
break;
}
}
if (combobreaktime == -1) {
combobreaktime = lasttime;
}
if (combobreaktime == -1) {
throw new RuntimeException("nodata");
}
Entrypoint.sout(String.format(
"%s combobreak at %d, lastcombo %d lastacc %f",
file.getName(),
combobreaktime,
lastcombo,
acc.getLast().acc
));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void spread(LinkedList<Integer> list, int time, int d) {
if (list.isEmpty() || d <= 1) {
list.add(time);
return;
}
int dtime = time - list.getLast();
int inc = dtime / d;
int ttime = list.getLast();
for (int i = 0; i < d; i++) {
ttime += inc;
if (i == d - 1) {
ttime = time;
}
list.add(ttime);
}
}
}
public static class AccData {
public int time;
public float acc;
public AccData(int time, float acc) {
this.time = time;
this.acc = acc;
}
}
}