package org.open2jam.render;
import org.open2jam.sound.SoundInstance;
import org.open2jam.game.TimingData;
import org.open2jam.game.Latency;
import com.github.dtinth.partytime.Client;
import com.github.dtinth.partytime.server.Connection;
import com.github.dtinth.partytime.server.Server;
import org.open2jam.sound.FmodExSoundSystem;
import java.awt.Font;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Map.Entry;
import java.util.*;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.DisplayMode;
import org.open2jam.Config;
import org.open2jam.GameOptions;
import org.open2jam.game.speed.SpeedMultiplier;
import org.open2jam.game.speed.Speed;
import org.open2jam.game.position.WSpeed;
import org.open2jam.parsers.Chart;
import org.open2jam.parsers.Event;
import org.open2jam.parsers.EventList;
import org.open2jam.parsers.utils.SampleData;
import org.open2jam.render.entities.*;
import org.open2jam.game.judgment.JudgmentResult;
import org.open2jam.game.judgment.JudgmentStrategy;
import org.open2jam.game.position.HiSpeed;
import org.open2jam.game.position.NoteDistanceCalculator;
import org.open2jam.game.position.RegulSpeed;
import org.open2jam.game.position.XRSpeed;
import org.open2jam.render.lwjgl.TrueTypeFont;
import org.open2jam.sound.Sound;
import org.open2jam.sound.SoundChannel;
import org.open2jam.sound.SoundSystem;
import org.open2jam.sound.SoundSystemException;
import org.open2jam.util.*;
/**
*
* @author fox
*/
public class Render implements GameWindowCallback
{
private String localMatchingServer = "";
private int rank;
private final boolean normalizeSpeed;
private Server server = null;
public void setServer(Server lastServer) {
server = lastServer;
}
public interface AutosyncCallback {
void autosyncFinished(double displayLag);
}
/** the config xml */
private static final URL resources_xml = Render.class.getResource("/resources/resources.xml");
/** 4 beats per minute, 4 * 60 beats per second, 4*60*1000 per millisecond */
private static final int BEATS_PER_MSEC = 4 * 60 * 1000;
private static final double DELAY_TIME = 1500;
/** player options */
private final GameOptions opt;
private static final double AUTOPLAY_THRESHOLD = 0;
/** skin info and entities */
Skin skin;
/** the mapping of note channels to KeyEvent keys */
final EnumMap<Event.Channel, Integer> keyboard_map;
/** the mapping of note channels to KeyEvent keys */
final EnumMap<Config.MiscEvent, Integer> keyboard_misc;
/** The window that is being used to render the game */
final GameWindow window;
/** The sound system to use */
final SoundSystem soundSystem;
/** The judge to judge the notes */
private JudgmentStrategy judge;
/** the chart being rendered */
private final Chart chart;
/** The recorded fps */
int fps;
/** the current "calculated" speed */
double speed;
/** the base speed multiplier */
private Speed speedObj;
/** the note distance calculator */
private NoteDistanceCalculator distance;
private boolean gameStarted = true;
/** the layer of the notes */
private int note_layer;
/** the bpm at which the entities are falling */
private double bpm;
/** maps the Event value to Sound objects */
private Map<Integer, Sound> sounds;
/** The time at which the last rendering looped started from the point of view of the game logic */
double lastLoopTime;
/** The time since the last record of fps */
double lastFpsTime = 0;
/** The cumulative time in this game */
double gameTime = 0;
int gameMeasure = 0;
Runnable increaseMeasureRunnable = new Runnable() {
@Override
public void run() {
gameMeasure += 1;
}
};
/** the time it started rendering */
double start_time;
/** a list of list of entities.
** basically, each list is a layer of entities
** the layers are rendered in order
** so entities at layer X will always be rendered before layer X+1 */
final EntityMatrix entities_matrix;
/** this iterator is used by the update_note_buffer
* to go through the events on the chart */
Iterator<Event> buffer_iterator;
/** this is used by the update_note_buffer
* to remember the "opened" long-notes */
private EnumMap<Event.Channel, LongNoteEntity> ln_buffer;
/** this holds the actual state of the keyboard,
* whether each is being pressed or not */
EnumMap<Event.Channel,Boolean> keyboard_key_pressed;
EnumMap<Event.Channel,Entity> longflare;
EnumMap<JudgmentResult,NumberEntity> note_counter;
/** these are the same notes from the entity_matrix
* but divided in channels for ease to pull */
private EnumMap<Event.Channel,LinkedList<NoteEntity>> note_channels;
/** entities for the key pressed events
* need to keep track of then to kill
* when the key is released */
EnumMap<Event.Channel,Entity> key_pressed_entity;
/** keep track of the long note the player may be
* holding with the key */
EnumMap<Event.Channel,LongNoteEntity> longnote_holded;
/** keep trap of the last sound of each channel
* so that the player can re-play the sound when the key is pressed */
EnumMap<Event.Channel,SampleEntity> last_sound;
/** number to display the fps, and note counters on the screen */
NumberEntity fps_entity;
NumberEntity score_entity;
/** JamCombo variables */
ComboCounterEntity jamcombo_entity;
/**
* Cools: +2
* Goods: +1
* Everything else: reset to 0
* >=50 to add a jam
*/
BarEntity jambar_entity;
BarEntity lifebar_entity;
LinkedList<Entity> pills_draw;
Map<Integer, Sprite> bga_sprites;
BgaEntity bgaEntity;
int consecutive_cools = 0;
NumberEntity minute_entity;
NumberEntity second_entity;
Entity judgment_entity;
/** the combo counter */
ComboCounterEntity combo_entity;
/** the maxcombo counter */
NumberEntity maxcombo_entity;
protected Entity judgment_line;
TrueTypeFont trueTypeFont;
/** statistics variable */
double total_notes = 0;
/** display and audio latency */
private Latency displayLatency;
private Latency audioLatency;
/** points to a latency that's currenly syncing:
* either displayLatency, audioLatency, or null.
*/
private Latency syncingLatency;
/** what to do after autosync? */
AutosyncCallback autosyncCallback;
/** local matching */
private Client localMatching;
/** song finish time [leave 10 seconds] */
long finish_time = -1;
protected CompositeEntity visibility_entity;
private final static float VOLUME_FACTOR = 0.05f;
/** timing data */
private TimingData timing = new TimingData();
/** status list */
private StatusList statusList = new StatusList();
/** haste mode: effective pitch */
private int pitchShift;
private double gameSpeed = 1;
private double effectiveSpeed; /* set by updatePitch */
private double effectiveJudgmentFactor; /* set by updatePitch */
private boolean haste = false;
/** adjust the final speed */
private double speedFactor = 1.0;
private class AdjustDistance implements NoteDistanceCalculator {
private final NoteDistanceCalculator distance;
public AdjustDistance(NoteDistanceCalculator distance) {
this.distance = distance;
}
@Override
public void update(double now, double delta) {
distance.update(now, delta);
}
@Override
public double calculate(double now, double target, double speed, NoteEntity noteEntity) {
return distance.calculate(now, target, speed, noteEntity) * speedFactor;
}
@Override
public String toString() {
return distance.toString();
}
}
static {
ResourceFactory.get().setRenderingType(ResourceFactory.OPENGL_LWJGL);
}
protected final boolean AUTOSOUND;
boolean disableAutoSound = false;
public Render(Chart chart, GameOptions opt, DisplayMode dm) throws SoundSystemException
{
keyboard_map = Config.getKeyboardMap(Config.KeyboardType.K7);
keyboard_misc = Config.getKeyboardMisc();
window = ResourceFactory.get().getGameWindow();
soundSystem = new FmodExSoundSystem(opt.getBufferSize());
soundSystem.setMasterVolume(opt.getMasterVolume());
soundSystem.setBGMVolume(opt.getBGMVolume());
soundSystem.setKeyVolume(opt.getKeyVolume());
entities_matrix = new EntityMatrix();
this.chart = chart;
this.opt = opt;
// speed multiplier
speed = opt.getSpeedMultiplier();
speedObj = new SpeedMultiplier(speed);
distance = new HiSpeed(timing, 385);
// TODO: refactor this
switch(opt.getSpeedType())
{
case xRSpeed:
distance = new XRSpeed(distance);
break;
case WSpeed:
distance = new WSpeed(distance, speedObj);
break;
case RegulSpeed:
distance = new RegulSpeed(385);
break;
}
distance = new AdjustDistance(distance);
AUTOSOUND = opt.isAutosound();
//TODO Should get values from gameoptions, but i'm lazy as hell
if(opt.isAutoplay())
{
for(Event.Channel c : Event.Channel.values())
{
if(c.toString().startsWith(("NOTE_")))
c.enableAutoplay();
}
// Event.Channel.NOTE_4.enableAutoplay();
// Event.Channel.NOTE_1.enableAutoplay();
} else {
for(Event.Channel c : Event.Channel.values())
{
if(c.toString().startsWith(("NOTE_")))
c.disableAutoplay();
}
}
displayLatency = new Latency(opt.getDisplayLag());
audioLatency = new Latency(opt.getAudioLatency());
statusList.add(new StatusItem() {
@Override
public String getText() {
return distance + ": " + speedObj;
}
@Override
public boolean isVisible() { return true; }
});
statusList.add(new StatusItem() {
@Override
public String getText() {
return "Current Measure: " + gameMeasure;
}
@Override
public boolean isVisible() { return true; }
});
statusList.add(new StatusItem() {
@Override
public String getText() {
return "Game Speed: " + String.format("%+d", pitchShift);
}
@Override
public boolean isVisible() { return true; }
});
haste = opt.isHasteMode();
normalizeSpeed = opt.isHasteModeNormalizeSpeed();
window.setDisplay(dm,opt.isDisplayVsync(),opt.isDisplayFullscreen());
}
public void setAutosyncCallback(AutosyncCallback autosyncDelegate) {
this.autosyncCallback = autosyncDelegate;
}
public void setJudge(JudgmentStrategy judge) {
this.judge = judge;
}
public void setAutosyncDisplay() {
this.syncingLatency = displayLatency;
}
public void setAutosyncAudio() {
this.syncingLatency = audioLatency;
}
public void setStartPaused() {
this.gameStarted = false;
}
public void setLocalMatchingServer(String text) {
this.localMatchingServer = text;
}
public void setRank(int rank) {
this.rank = rank;
}
/**
* initialize the common elements for the game.
* this is called by the window render
*/
@Override
public void initialise()
{
lastLoopTime = SystemTimer.getTime();
// skin load
try {
SkinParser sb = new SkinParser(window.getResolutionWidth(), window.getResolutionHeight());
SAXParserFactory.newInstance().newSAXParser().parse(resources_xml.openStream(), sb);
if((skin = sb.getResult("o2jam")) == null){
Logger.global.log(Level.SEVERE, "Skin load error There is no o2jam skin");
}
} catch (ParserConfigurationException ex) {
Logger.global.log(Level.SEVERE, "Skin load error {0}", ex);
} catch (org.xml.sax.SAXException ex) {
Logger.global.log(Level.SEVERE, "Skin load error {0}", ex);
} catch (java.io.IOException ex) {
Logger.global.log(Level.SEVERE, "Skin load error {0}", ex);
}
// cover image load
try{
BufferedImage img = chart.getCover();
Sprite s = ResourceFactory.get().getSprite(img);
s.setScale(skin.getScreenScaleX(), skin.getScreenScaleY());
s.draw(0, 0);
window.update();
} catch (NullPointerException e){
Logger.global.log(Level.INFO, "No cover image on file: {0}", chart.getSource().getName());
}
changeSpeed(0);
bpm = chart.getBPM();
note_layer = skin.getEntityMap().get("NOTE_1").getLayer();
// build long note buffer
ln_buffer = new EnumMap<Event.Channel,LongNoteEntity>(Event.Channel.class);
// the notes pressed buffer
keyboard_key_pressed = new EnumMap<Event.Channel,Boolean>(Event.Channel.class);
// reference to the notes in the buffer, separated by the channel
note_channels = new EnumMap<Event.Channel,LinkedList<NoteEntity>>(Event.Channel.class);
// entity for key pressed events
key_pressed_entity = new EnumMap<Event.Channel,Entity>(Event.Channel.class);
// reference to long notes being holded
longnote_holded = new EnumMap<Event.Channel,LongNoteEntity>(Event.Channel.class);
longflare = new EnumMap<Event.Channel, Entity> (Event.Channel.class);
last_sound = new EnumMap<Event.Channel,SampleEntity>(Event.Channel.class);
fps_entity = (NumberEntity) skin.getEntityMap().get("FPS_COUNTER");
entities_matrix.add(fps_entity);
score_entity = (NumberEntity) skin.getEntityMap().get("SCORE_COUNTER");
entities_matrix.add(score_entity);
jamcombo_entity = (ComboCounterEntity) skin.getEntityMap().get("JAM_COUNTER");
jamcombo_entity.setThreshold(1);
entities_matrix.add(jamcombo_entity);
jambar_entity = (BarEntity) skin.getEntityMap().get("JAM_BAR");
jambar_entity.setLimit(50);
entities_matrix.add(jambar_entity);
lifebar_entity = (BarEntity) skin.getEntityMap().get("LIFE_BAR");
entities_matrix.add(lifebar_entity);
combo_entity = (ComboCounterEntity) skin.getEntityMap().get("COMBO_COUNTER");
combo_entity.setThreshold(2);
entities_matrix.add(combo_entity);
maxcombo_entity = (NumberEntity) skin.getEntityMap().get("MAXCOMBO_COUNTER");
entities_matrix.add(maxcombo_entity);
minute_entity = (NumberEntity) skin.getEntityMap().get("MINUTE_COUNTER");
entities_matrix.add(minute_entity);
second_entity = (NumberEntity) skin.getEntityMap().get("SECOND_COUNTER");
second_entity.showDigits(2);//show 2 digits
entities_matrix.add(second_entity);
pills_draw = new LinkedList<Entity>();
visibility_entity = new CompositeEntity();
if(opt.getVisibilityModifier() != GameOptions.VisibilityMod.None)
visibility(opt.getVisibilityModifier());
judgment_line = skin.getEntityMap().get("JUDGMENT_LINE");
entities_matrix.add(judgment_line);
initLifeBar();
for(Event.Channel c : keyboard_map.keySet())
{
keyboard_key_pressed.put(c, Boolean.FALSE);
note_channels.put(c, new LinkedList<NoteEntity>());
}
note_counter = new EnumMap<JudgmentResult,NumberEntity>(JudgmentResult.class);
for(JudgmentResult s : JudgmentResult.values()){
NumberEntity e = (NumberEntity)skin.getEntityMap().get("COUNTER_"+s).copy();
note_counter.put(s, e);
entities_matrix.add(note_counter.get(s));
}
start_time = lastLoopTime = SystemTimer.getTime();
EventList event_list = construct_velocity_tree(chart.getEvents());
event_list.fixEventList(EventList.FixMethod.OPEN2JAM, true);
judge.setTiming(timing);
//Let's randomize "-"
switch(opt.getChannelModifier())
{
case Mirror:
event_list.channelMirror();
break;
case Shuffle:
event_list.channelShuffle();
break;
case Random:
event_list.channelRandom();
break;
}
bgaEntity = (BgaEntity) skin.getEntityMap().get("BGA");
entities_matrix.add(bgaEntity);
bga_sprites = new HashMap<Integer, Sprite>();
if(chart.hasVideo()) {
bgaEntity.isVideo = true;
bgaEntity.videoFile = chart.getVideo();
bgaEntity.initVideo();
} else if(!chart.getBgaIndex().isEmpty()) {
// get all the bgaEntity sprites
for(Entry<Integer, File> entry: chart.getImages().entrySet()) {
BufferedImage img;
try {
img = ImageIO.read(entry.getValue());
Sprite s = ResourceFactory.get().getSprite(img);
bga_sprites.put(entry.getKey(), s);
} catch (IOException ex) {
java.util.logging.Logger.getLogger(Render.class.getName()).log(Level.SEVERE, "{0}", ex);
}
}
}
// adding static entities
for(Entity e : skin.getEntityList()){
entities_matrix.add(e);
}
// get a new iterator
buffer_iterator = event_list.iterator();
// load up initial buffer
update_note_buffer(0, 0);
// get the chart sound samples
sounds = new HashMap<Integer, Sound>();
for(Entry<Integer, SampleData> entry : chart.getSamples().entrySet())
{
SampleData sampleData = entry.getValue();
try {
Sound sound = soundSystem.load(sampleData);
sounds.put(entry.getKey(), sound);
} catch (SoundSystemException ex) {
java.util.logging.Logger.getLogger(Render.class.getName()).log(Level.SEVERE, "{0}", ex);
}
try {
entry.getValue().dispose();
} catch (IOException ex) {
java.util.logging.Logger.getLogger(Render.class.getName()).log(Level.SEVERE, "{0}", ex);
}
}
trueTypeFont = new TrueTypeFont(new Font("Tahoma", Font.BOLD, 14), false);
//clean up
System.gc();
// wait a bit.. 5 seconds at min
SystemTimer.sleep((int) (5000 - (SystemTimer.getTime() - lastLoopTime)));
lastLoopTime = SystemTimer.getTime();
start_time = lastLoopTime + DELAY_TIME;
try {
String[] data = localMatchingServer.trim().split(":");
if (data.length == 2) {
String host = data[0];
int port = Integer.parseInt(data[1]);
localMatching = new Client(host, port, (long)audioLatency.getLatency());
}
} catch (Exception ex) {
ex.printStackTrace();
}
if (localMatching != null) {
gameStarted = false;
new Thread(localMatching).start();
statusList.add(new StatusItem() {
@Override
public String getText() {
return "" + localMatching.getStatus();
}
@Override
public boolean isVisible() { return true; }
});
} else if (!gameStarted) {
statusList.add(new StatusItem() {
@Override
public String getText() {
return "Press any note button to start the game.";
}
@Override
public boolean isVisible() { return !gameStarted; }
});
}
}
/**
* Initializes the life bar based on rank
*/
private void initLifeBar() {
int base = 12000; // base health bar size
int multiplier;
if (rank >= 2) {
multiplier = 4; // hard coefficient
} else if (rank >= 1) {
multiplier = 3; // normal coefficient
} else {
multiplier = 2; // easy coefficient
}
int maxLife = base * multiplier;
lifebar_entity.setLimit(maxLife);
lifebar_entity.setNumber(maxLife);
}
/* make the rendering start */
public void startRendering()
{
window.setGameWindowCallback(this);
window.setTitle(chart.getArtist()+" - "+chart.getTitle());
try{
window.startRendering();
}catch(OutOfMemoryError e) {
System.gc();
Logger.global.log(Level.SEVERE, "System out of memory ! baillin out !!{0}", e.getMessage());
JOptionPane.showMessageDialog(null, "Fatal Error", "System out of memory ! baillin out !!",JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
}
/**
* Notification that a frame is being rendered. Responsible for
* running game logic and rendering the scene.
*/
@Override
public void frameRendering()
{
// work out how long its been since the last update, this
// will be used to calculate how far the entities should
// move this loop
double now = SystemTimer.getTime();
double delta = now - lastLoopTime;
lastLoopTime = now;
lastFpsTime += delta;
fps++;
update_fps_counter();
check_misc_keyboard();
changeSpeed(delta); // TODO: is everything here really needed every frame ?
updateGameSpeed(delta);
if (!gameStarted && localMatching != null) {
if (localMatching.isReady()) gameStarted = true;
}
if (gameStarted) {
gameTime += delta * effectiveSpeed;
}
now = gameTime;
if (AUTOSOUND) now -= audioLatency.getLatency();
double now_display = now + displayLatency.getLatency();
update_note_buffer(now, now_display);
distance.update(now_display, delta);
soundSystem.update();
do_autoplay(now);
Keyboard.poll();
check_keyboard(now);
for(LinkedList<Entity> layer : entities_matrix) // loop over layers
{
// get entity iterator from layer
// need to use iterator here because we remove() below
Iterator<Entity> j = layer.iterator();
while(j.hasNext()) // loop over entities
{
Entity e = j.next();
e.move(delta); // move the entity
if(e instanceof TimeEntity)
{
TimeEntity te = (TimeEntity) e;
//autoplays sounds play
double timeToJudge = now;
if (e instanceof SoundEntity && AUTOSOUND) {
timeToJudge += audioLatency.getLatency();
}
if(te.getTime() - timeToJudge <= 0 && gameStarted) te.judgment();
NoteEntity ne = e instanceof NoteEntity ? (NoteEntity)e : null;
double y = getViewport() - distance.calculate(now_display, te.getTime(), speed, ne);
//TODO Fix this, maybe an option in the skin
//o2jam overlaps 1 px of the note with the measure and, because of this
//our skin should do it too xD
if(e instanceof MeasureEntity) y -= 1;
if(!(e instanceof BgaEntity))
e.setPos(e.getX(), y);
if(e instanceof LongNoteEntity) {
LongNoteEntity lne = (LongNoteEntity)e;
double ey = getViewport() - distance.calculate(now_display, lne.getEndTime(), speed, ne);
lne.setEndDistance(Math.abs(ey - y));
}
if(e instanceof NoteEntity) check_judgment((NoteEntity)e, now);
}
if(e.isDead())j.remove();
else e.draw();
}
}
int y = 300;
for (String s : statusList) {
trueTypeFont.drawString(780, y, s, 1, -1, TrueTypeFont.ALIGN_RIGHT);
y += 30;
}
// TODO: THIS IS SPAGHETTI. IMPROVE SOON.
y = 64;
if (server != null) {
trueTypeFont.drawString(780, y, "Server: " + server.getStatus(), 1, -1, TrueTypeFont.ALIGN_RIGHT);
y += 24;
for (Connection conn : server.getConnections()) {
String s = conn.toString() + ": " + conn.getStatus();
trueTypeFont.drawString(780, y, s, 1, -1, TrueTypeFont.ALIGN_RIGHT);
y += 18;
}
}
if(!buffer_iterator.hasNext() && entities_matrix.isEmpty(note_layer)){
if (finish_time == -1) {
finish_time = System.currentTimeMillis() + 10000;
} else if (System.currentTimeMillis() > finish_time) {
soundSystem.release();
window.destroy();
}
}
}
private void update_fps_counter()
{
// update our FPS counter if a second has passed
if (lastFpsTime >= 1000) {
Logger.global.log(Level.FINEST, "FPS: {0}", fps);
fps_entity.setNumber(fps);
lastFpsTime = lastFpsTime-1000;
fps = 0;
//the timer counter
if(second_entity.getNumber() >= 59)
{
second_entity.setNumber(0);
minute_entity.incNumber();
}
else
second_entity.incNumber();
}
}
int lastSpeedChangeMeasure = 0;
int lastUpdateMeasure = 0;
double lastSpeedChangeTime = 0;
void updateGameSpeed(double delta) {
int pitch = (int)Math.round(12.0 * Math.log(gameSpeed) / Math.log(2));
effectiveSpeed = Math.pow(2, pitch / 12.0);
effectiveJudgmentFactor = effectiveSpeed;
if (pitchShift != pitch) {
pitchShift = pitch;
soundSystem.setSpeed((float)effectiveSpeed);
}
if (haste) {
double maxSpeed = Math.min(2, Math.max(0.5, 3 * (double)lifebar_entity.getNumber() / lifebar_entity.getLimit()));
if (gameMeasure > lastUpdateMeasure) {
int measureDelta = gameMeasure - lastSpeedChangeMeasure;
boolean increase = false;
if (gameTime - lastSpeedChangeTime >= 5333 * Math.pow(Math.min(gameSpeed, 1.0), 4) && gameMeasure >= 6) {
if ((measureDelta & (measureDelta - 1)) == 0) increase = true;
if (measureDelta >= 8) increase = true;
if (lastSpeedChangeMeasure == 0) increase = true;
}
if (increase) {
gameSpeed = gameSpeed * Math.pow(2, 1 / 12.0);
lastSpeedChangeMeasure = gameMeasure;
lastSpeedChangeTime = gameTime;
}
lastUpdateMeasure = gameMeasure;
}
if (gameSpeed > maxSpeed) gameSpeed = maxSpeed;
if (normalizeSpeed) {
double target = 1 / gameSpeed;
speedFactor += (target - speedFactor) * 0.1;
}
}
}
void do_autoplay(double now)
{
for(Event.Channel c : keyboard_map.keySet())
{
if(!c.isAutoplay()) continue;
NoteEntity ne = nextNoteKey(c);
if(ne == null)continue;
double hit = ne.testTimeHit(now);
if(hit > AUTOPLAY_THRESHOLD)continue;
ne.updateHit(now, effectiveJudgmentFactor);
if(ne instanceof LongNoteEntity)
{
if(ne.getState() == NoteEntity.State.NOT_JUDGED)
{
disableAutoSound = false;
ne.keysound();
ne.setState(NoteEntity.State.LN_HEAD_JUDGE);
Entity ee = skin.getEntityMap().get("PRESSED_"+ne.getChannel()).copy();
entities_matrix.add(ee);
Entity to_kill = key_pressed_entity.put(ne.getChannel(), ee);
if(to_kill != null)to_kill.setDead(true);
}
else if(ne.getState() == NoteEntity.State.LN_HOLD)
{
ne.setState(NoteEntity.State.JUDGE);
longflare.get(ne.getChannel()).setDead(true); //let's kill the longflare effect
key_pressed_entity.get(ne.getChannel()).setDead(true);
}
}
else
{
disableAutoSound = false;
ne.keysound();
ne.setState(NoteEntity.State.JUDGE);
}
}
}
public boolean isDisableAutoSound() {
return disableAutoSound;
}
public void check_keyboard(double now)
{
if (window.isKeyDown(Keyboard.KEY_RETURN) && server != null) {
server.startGame();
server = null;
}
for(Map.Entry<Event.Channel,Integer> entry : keyboard_map.entrySet())
{
Event.Channel c = entry.getKey();
if(c.isAutoplay()) continue;
boolean keyDown = window.isKeyDown(entry.getValue());
boolean keyWasDown = keyboard_key_pressed.get(c);
if(keyDown && !keyWasDown){ // started holding now
if (!gameStarted && localMatching == null) gameStarted = true;
keyboard_key_pressed.put(c, true);
Entity baseEntity = skin.getEntityMap().get("PRESSED_"+c);
Entity to_kill = null;
if (baseEntity != null) {
Entity ee = baseEntity.copy();
entities_matrix.add(ee);
to_kill = key_pressed_entity.put(c, ee);
}
if(to_kill != null)to_kill.setDead(true);
NoteEntity e = nextNoteKey(c);
if(e == null){
SampleEntity i = last_sound.get(c);
if(i != null) i.extrasound();
continue;
}
e.updateHit(now, effectiveJudgmentFactor);
// don't continue if the note is too far
if(judge.accept(e)) {
disableAutoSound = false;
e.keysound();
if(e instanceof LongNoteEntity) {
longnote_holded.put(c, (LongNoteEntity) e);
if(e.getState() == NoteEntity.State.NOT_JUDGED)
e.setState(NoteEntity.State.LN_HEAD_JUDGE);
} else {
e.setState(NoteEntity.State.JUDGE);
}
} else {
e.getSampleEntity().extrasound();
}
}else if(!keyDown && keyWasDown) { // key released now
keyboard_key_pressed.put(c, false);
Entity to_kill = key_pressed_entity.get(c);
if(to_kill != null)to_kill.setDead(true);
Entity lf = longflare.remove(c);
if(lf !=null)lf.setDead(true);
LongNoteEntity e = longnote_holded.remove(c);
if(e == null || e.getState() != NoteEntity.State.LN_HOLD)continue;
e.updateHit(now, effectiveJudgmentFactor);
e.setState(NoteEntity.State.JUDGE);
}
}
}
private void autosync(double hit) {
if (syncingLatency == null) return;
syncingLatency.autosync(hit);
}
public void check_judgment(NoteEntity ne, double now)
{
JudgmentResult result;
switch (ne.getState())
{
case NOT_JUDGED: // you missed it (no keyboard input)
ne.updateHit(now, effectiveJudgmentFactor);
if (judge.missed(ne)) {
disableAutoSound = true;
setNoteJudgment(ne, JudgmentResult.MISS);
}
break;
case JUDGE: //LN & normal ones: has finished with good result
result = judge.judge(ne);
setNoteJudgment(ne, result);
if (!(ne instanceof LongNoteEntity)) {
autosync(ne.getHitTime());
}
break;
case LN_HOLD: // You kept too much time the note held that it misses
ne.updateHit(now, effectiveJudgmentFactor);
if (judge.missed(ne)) {
setNoteJudgment(ne, JudgmentResult.MISS);
// kill the long flare
Entity lf = longflare.remove(ne.getChannel());
if(lf !=null)lf.setDead(true);
}
break;
case LN_HEAD_JUDGE: //LN: Head has been played
result = judge.judge(ne);
setNoteJudgment(ne, result);
// display the long flare and kill the old one
if (result != JudgmentResult.MISS) {
Entity ee = skin.getEntityMap().get("EFFECT_LONGFLARE").copy();
ee.setPos(ne.getX()+ne.getWidth()/2-ee.getWidth()/2,ee.getY());
entities_matrix.add(ee);
Entity to_kill = longflare.put(ne.getChannel(),ee);
if(to_kill != null)to_kill.setDead(true);
ne.setState(NoteEntity.State.LN_HOLD);
} else {
System.out.println(ne.getTimeToJudge() + " - " + now);
}
break;
case TO_KILL: // this is the "garbage collector", it just removes the notes off window
if(ne.getY() >= window.getResolutionHeight())
{
// kill it
ne.setDead(true);
}
break;
}
}
public void setNoteJudgment(NoteEntity ne, JudgmentResult result) {
result = handleJudgment(result);
// stop the sound if missed
if (result == JudgmentResult.MISS) {
ne.missed();
}
// display the judgment
if(judgment_entity != null)judgment_entity.setDead(true);
judgment_entity = skin.getEntityMap().get("EFFECT_"+result).copy();
entities_matrix.add(judgment_entity);
// add to the statistics
note_counter.get(result).incNumber();
// for cool: display the effect
if (result == JudgmentResult.COOL || result == JudgmentResult.GOOD) {
Entity ee = skin.getEntityMap().get("EFFECT_CLICK").copy();
ee.setPos(ne.getX()+ne.getWidth()/2-ee.getWidth()/2,
getViewport()-ee.getHeight()/2);
entities_matrix.add(ee);
}
// delete the note
if (result == JudgmentResult.MISS || (ne instanceof LongNoteEntity)) {
ne.setState(NoteEntity.State.TO_KILL);
} else {
ne.setDead(true);
}
// update combo
if (shouldIncreaseCombo(result)) {
combo_entity.incNumber();
} else {
combo_entity.resetNumber();
}
}
public boolean shouldIncreaseCombo(JudgmentResult result) {
if (result == null) return false;
switch (result) {
case BAD: case MISS: return false;
}
return true;
}
public JudgmentResult handleJudgment(JudgmentResult result) {
int score_value = 0;
switch(result)
{
case COOL:
jambar_entity.addNumber(2);
consecutive_cools++;
lifebar_entity.addNumber(rank >= 2 ? 48 : 96);
score_value = 200 + (jamcombo_entity.getNumber()*10);
break;
case GOOD:
jambar_entity.addNumber(1);
consecutive_cools = 0;
score_value = 100;
break;
case BAD:
if(pills_draw.size() > 0)
{
result = JudgmentResult.GOOD;
jambar_entity.addNumber(1);
pills_draw.removeLast().setDead(true);
score_value = 100; // TODO: not sure
}
else
{
jambar_entity.setNumber(0);
jamcombo_entity.resetNumber();
lifebar_entity.subtractNumber(240);
score_value = 4;
}
consecutive_cools = 0;
break;
case MISS:
jambar_entity.setNumber(0);
jamcombo_entity.resetNumber();
consecutive_cools = 0;
lifebar_entity.subtractNumber(1440);
if(score_entity.getNumber() >= 10)score_value = -10;
else score_value = -score_entity.getNumber();
break;
}
score_entity.addNumber(score_value);
if(jambar_entity.getNumber() >= jambar_entity.getLimit())
{
jambar_entity.setNumber(0); //reset
jamcombo_entity.incNumber();
}
if(consecutive_cools >= 15 && pills_draw.size() < 5)
{
consecutive_cools -= 15;
Entity ee = skin.getEntityMap().get("PILL_"+(pills_draw.size()+1)).copy();
entities_matrix.add(ee);
pills_draw.add(ee);
}
if(maxcombo_entity.getNumber()<(combo_entity.getNumber()))
{
maxcombo_entity.incNumber();
}
return result;
}
/* play a sample */
public SoundInstance queueSample(Event.SoundSample soundSample)
{
if(soundSample == null) return null;
Sound sound = sounds.get(soundSample.sample_id);
if(sound == null)return null;
try {
return sound.play(soundSample.isBGM() ? SoundChannel.BGM : SoundChannel.KEY,
1.0f, soundSample.pan);
} catch (SoundSystemException ex) {
java.util.logging.Logger.getLogger(Render.class.getName()).log(Level.SEVERE, "{0}", ex);
return null;
}
}
private void change_bgm_volume(float factor)
{
opt.setBGMVolume(opt.getBGMVolume() + factor);
soundSystem.setBGMVolume(opt.getBGMVolume());
}
private void change_key_volume(float factor)
{
opt.setKeyVolume(opt.getKeyVolume() + factor);
soundSystem.setKeyVolume(opt.getKeyVolume());
}
private void changeSpeed(double delta)
{
speedObj.update(delta);
speed = speedObj.getCurrentSpeed();
}
double getViewport() { return skin.getJudgmentLine(); }
/* this returns the next note that needs to be played
** of the defined channel or NULL if there's
** no such note in the moment **/
NoteEntity nextNoteKey(Event.Channel c)
{
if(note_channels.get(c).isEmpty())return null;
NoteEntity ne = note_channels.get(c).getFirst();
while(ne.getState() != NoteEntity.State.NOT_JUDGED &&
ne.getState() != NoteEntity.State.LN_HOLD)
{
note_channels.get(c).removeFirst();
if(note_channels.get(c).isEmpty())return null;
ne = note_channels.get(c).getFirst();
}
last_sound.put(c, ne.getSampleEntity());
return ne;
}
private double buffer_timer = 0;
/* update the note layer of the entities_matrix.
*** note buffering is equally distributed between the frames
**/
void update_note_buffer(double now, double now_display)
{
while(buffer_iterator.hasNext() && getViewport() - distance.calculate(now_display, buffer_timer, speed, null) > -10)
{
Event e = buffer_iterator.next();
buffer_timer = e.getTime();
switch(e.getChannel())
{
case MEASURE:
MeasureEntity m = (MeasureEntity) skin.getEntityMap().get("MEASURE_MARK").copy();
m.setTime(e.getTime());
m.setOnJudge(increaseMeasureRunnable);
entities_matrix.add(m);
break;
case NOTE_1:case NOTE_2:
case NOTE_3:case NOTE_4:
case NOTE_5:case NOTE_6:case NOTE_7:
if(e.getFlag() == Event.Flag.NONE){
if(ln_buffer.containsKey(e.getChannel()))
Logger.global.log(Level.WARNING, "There is a none in the current long {0} @ "+e.getTotalPosition(), e.getChannel());
NoteEntity n = (NoteEntity) skin.getEntityMap().get(e.getChannel().toString()).copy();
n.setTime(e.getTime());
assignSample(n, e);
entities_matrix.add(n);
note_channels.get(n.getChannel()).add(n);
}
else if(e.getFlag() == Event.Flag.HOLD){
if(ln_buffer.containsKey(e.getChannel()))
Logger.global.log(Level.WARNING, "There is a hold in the current long {0} @ "+e.getTotalPosition(), e.getChannel());
LongNoteEntity ln = (LongNoteEntity) skin.getEntityMap().get("LONG_"+e.getChannel()).copy();
ln.setTime(e.getTime());
assignSample(ln, e);
entities_matrix.add(ln);
ln_buffer.put(e.getChannel(),ln);
note_channels.get(ln.getChannel()).add(ln);
}
else if(e.getFlag() == Event.Flag.RELEASE){
LongNoteEntity lne = ln_buffer.remove(e.getChannel());
if(lne == null){
Logger.global.log(Level.WARNING, "Attempted to RELEASE note {0} @ "+e.getTotalPosition(), e.getChannel());
}else{
lne.setEndTime(e.getTime());
}
}
break;
case BGA:
if(!bgaEntity.isVideo) {
Sprite sprite = null;
if(bga_sprites.containsKey((int)e.getValue()))
sprite = bga_sprites.get((int)e.getValue());
if(sprite == null) break;
sprite.setScale(1f, 1f);
bgaEntity.setSprite(sprite);
}
bgaEntity.setTime(e.getTime());
break;
//TODO ADD SUPPORT
case NOTE_SC:
case NOTE_8:case NOTE_9:
case NOTE_10:case NOTE_11:
case NOTE_12:case NOTE_13:case NOTE_14:
case NOTE_SC2:
case AUTO_PLAY:
autoSound(e, true);
break;
}
}
}
private void assignSample(NoteEntity n, Event e) {
SampleEntity sampleEntity = createSampleEntity(e, false);
if(AUTOSOUND) {
autoSound(sampleEntity);
sampleEntity.setNote(true);
}
n.setSampleEntity(sampleEntity);
}
private SampleEntity autoSound(Event e, boolean bgm)
{
return autoSound(createSampleEntity(e, bgm));
}
private SampleEntity autoSound(SampleEntity se) {
entities_matrix.add(se);
return se;
}
private SampleEntity createSampleEntity(Event e, boolean bgm) {
if(bgm) e.getSample().toBGM();
SampleEntity s = new SampleEntity(this,e.getSample(),0);
s.setTime(e.getTime());
return s;
}
private final List<Integer> misc_keys = new LinkedList<Integer>();
void check_misc_keyboard()
{
for(Map.Entry<Config.MiscEvent,Integer> entry : keyboard_misc.entrySet())
{
Config.MiscEvent event = entry.getKey();
if(window.isKeyDown(entry.getValue()) && !misc_keys.contains(entry.getValue())) // this key is being pressed
{
misc_keys.add(entry.getValue());
switch(event)
{
case SPEED_UP:
speedObj.increase();
break;
case SPEED_DOWN:
speedObj.decrease();
break;
case MAIN_VOL_UP:
opt.setMasterVolume(opt.getMasterVolume() + VOLUME_FACTOR);
soundSystem.setMasterVolume(opt.getMasterVolume());
break;
case MAIN_VOL_DOWN:
opt.setMasterVolume(opt.getMasterVolume() - VOLUME_FACTOR);
soundSystem.setMasterVolume(opt.getMasterVolume());
break;
case KEY_VOL_UP:
change_key_volume(VOLUME_FACTOR);
break;
case KEY_VOL_DOWN:
change_key_volume(-VOLUME_FACTOR);
break;
case BGM_VOL_UP:
change_bgm_volume(VOLUME_FACTOR);
break;
case BGM_VOL_DOWN:
change_bgm_volume(-VOLUME_FACTOR);
break;
}
}
else if(!window.isKeyDown(entry.getValue()) && misc_keys.contains(entry.getValue()))
{
misc_keys.remove(entry.getValue());
}
}
}
private EventList construct_velocity_tree(EventList list)
{
int measure = 0;
double timer = DELAY_TIME;
double my_bpm = this.bpm;
double frac_measure = 1;
double measure_pointer = 0;
double measure_size = 0.8 * getViewport();
double my_note_speed = (my_bpm * measure_size) / BEATS_PER_MSEC;
double event_position;
EventList new_list = new EventList();
timing.add(timer, bpm);
//there is always a 1st measure
Event m = new Event(Event.Channel.MEASURE, measure, 0, 0, Event.Flag.NONE);
m.setTime(timer);
new_list.add(m);
for(Event e : list)
{
while(e.getMeasure() > measure)
{
timer += (BEATS_PER_MSEC * (frac_measure-measure_pointer)) / my_bpm;
m = new Event(Event.Channel.MEASURE, measure, 0, 0, Event.Flag.NONE);
m.setTime(timer);
new_list.add(m);
measure++;
frac_measure = 1;
measure_pointer = 0;
}
if(chart.type == Chart.TYPE.OJN) {
event_position = e.getPosition();
} else {
event_position = e.getPosition() * frac_measure;
}
timer += (BEATS_PER_MSEC * (event_position-measure_pointer)) / my_bpm;
measure_pointer = event_position;
switch(e.getChannel())
{
case STOP:
timing.add(timer, 0);
double stop_time = e.getValue();
if(chart.type == Chart.TYPE.BMS) {
stop_time = (e.getValue() / 192) * BEATS_PER_MSEC / my_bpm;
}
timing.add(timer + stop_time, my_bpm);
timer += stop_time;
break;
case BPM_CHANGE:
my_bpm = e.getValue();
timing.add(timer, my_bpm);
break;
case TIME_SIGNATURE:
frac_measure = e.getValue();
break;
case NOTE_1:case NOTE_2:
case NOTE_3:case NOTE_4:
case NOTE_5:case NOTE_6:case NOTE_7:
case NOTE_SC:
case NOTE_8:case NOTE_9:
case NOTE_10:case NOTE_11:
case NOTE_12:case NOTE_13:case NOTE_14:
case NOTE_SC2:
case AUTO_PLAY:
case BGA:
if (!last_sound.containsKey(e.getChannel()) && e.getSample() != null) {
last_sound.put(e.getChannel(), createSampleEntity(e, false));
}
e.setTime(timer + e.getOffset());
if(e.getOffset() != 0) System.out.println("offset: "+e.getOffset()+" timer: "+(timer+e.getOffset()));
break;
case MEASURE:
Logger.global.log(Level.WARNING, "...THE FUCK? Why is a measure event here?");
break;
}
new_list.add(e);
}
timing.finish();
return new_list;
}
private void visibility(GameOptions.VisibilityMod value)
{
int height = 0;
int width = 0;
Sprite rec = null;
// We will make a new entity with the masking rectangle for each note lane
// because we can't know for sure where the notes will be,
// meaning that they may not be together
for(Event.Channel ev : Event.Channel.values())
{
if(ev.toString().startsWith("NOTE_") && skin.getEntityMap().get(ev.toString()) != null)
{
height = (int)Math.round(getViewport());
width = (int)Math.round(skin.getEntityMap().get(ev.toString()).getWidth());
rec = ResourceFactory.get().doRectangle(width, height, value);
visibility_entity.getEntityList().add(new Entity(rec, skin.getEntityMap().get(ev.toString()).getX(), 0));
}
}
int layer = note_layer+1;
for(Entity e : skin.getEntityList())
if(e.getLayer() > layer) layer++;
visibility_entity.setLayer(++layer);
for(Entity e : skin.getAllEntities())
{
int l = e.getLayer();
if(l >= layer)
e.setLayer(++l);
}
// FIXME this is a hack
if(value != GameOptions.VisibilityMod.Sudden)skin.getEntityMap().get("JUDGMENT_LINE").setLayer(layer);
skin.getEntityMap().get("MEASURE_MARK").setLayer(layer);
entities_matrix.add(visibility_entity);
}
/**
* Notification that the game window has been closed
*/
@Override
public void windowClosed() {
bgaEntity.release();
soundSystem.release();
System.gc();
if (syncingLatency != null && autosyncCallback != null) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
autosyncCallback.autosyncFinished(syncingLatency.getLatency());
}
});
}
}
private double clamp(double value, double min, double max)
{
return Math.min(Math.max(value, min), max);
}
}