package rabbitescape.ui.swing; import static rabbitescape.engine.util.Util.*; import rabbitescape.engine.config.Config; import rabbitescape.engine.config.ConfigFile; import rabbitescape.engine.config.ConfigTools; import rabbitescape.engine.config.ConfigSchema; import rabbitescape.engine.util.RealFileSystem; import rabbitescape.engine.util.Util; import rabbitescape.render.*; import rabbitescape.render.Frame; import rabbitescape.render.Renderer; import javax.swing.*; import java.awt.*; import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.image.BufferStrategy; import java.util.*; import java.util.List; import static rabbitescape.engine.i18n.Translation.t; import static rabbitescape.render.AnimationLoader.*; public class AnimationTester extends JFrame { public enum Mode { RUN, STEP, FRAME_DUMP } private static final long serialVersionUID = 1L; private static final String CONFIG_PATH = "~/.rabbitescape/config/animationtester.properties" .replace( "~", System.getProperty( "user.home" ) ); private static final String CFG_AT_WINDOW_LEFT = "animationtester.window.left"; private static final String CFG_AT_WINDOW_TOP = "animationtester.window.top"; private static final String CFG_AT_TILE_SIZE = "animationtester.tile.size"; private static final String CFG_AT_ANIMATIONS = "animationtester.animations"; private static final String CFG_AT_BLOCKS = "animationtester.blocks"; private static final String[][] defaultAnimationNames = new String[][] { new String[] { NONE, NONE, NONE }, new String[] { NONE, NONE, NONE }, new String[] { NONE, NONE, NONE }, new String[] { "rabbit_walking_right", NONE, NONE }, new String[] { NONE, "rabbit_bashing_right", "rabbit_walking_right" }, new String[] { NONE, NONE, NONE }, new String[] { NONE, NONE, NONE }, new String[] { NONE, NONE, NONE }, new String[] { NONE, NONE, NONE } }; private static final String[] defaultBlockNames = new String[] { NONE, NONE, NONE, "land_rising_left_1", NONE, "bridge_rising_right", "land_block_1", "land_block_2", "land_block_3", }; private class Listener extends EmptyListener { @Override public void mouseClicked( MouseEvent mouseEvent ) { int i = screen2index( mouseEvent.getX(), mouseEvent.getY() ); String[] possibilties = animationCache.listAll(); JPanel dropDowns = new JPanel(); JList<String> list0 = addList( dropDowns, possibilties, animationNames[ i ][ 0 ] ); JList<String> list1 = addList( dropDowns, possibilties, animationNames[ i ][ 1 ] ); JList<String> list2 = addList( dropDowns, possibilties, animationNames[ i ][ 2 ] ); JList<String> blocksList = addList( dropDowns, allBlocks, blockNames[ i ] ); JScrollPane scrollPane = new JScrollPane( dropDowns ); scrollPane.setPreferredSize( new Dimension( 800, 500 ) ); int retVal = JOptionPane.showOptionDialog( AnimationTester.this, scrollPane, "Change animation", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null ); canvas.requestFocus(); if ( retVal == JOptionPane.CANCEL_OPTION ) { return; } animationNames[i][0] = noneForNull( list0.getSelectedValue() ); animationNames[i][1] = noneForNull( list1.getSelectedValue() ); animationNames[i][2] = noneForNull( list2.getSelectedValue() ); blockNames[i] = noneForNull( blocksList.getSelectedValue() ); saveSelectionsToConfig(); canvas.requestFocus(); } private void saveSelectionsToConfig() { atConfig.set( CFG_AT_ANIMATIONS, animationsToConfigString( animationNames ) ); atConfig.set( CFG_AT_BLOCKS, blocksToConfigString( blockNames ) ); atConfig.save(); } private String noneForNull( String value ) { if ( value == null ) { return NONE; } else { return value; } } private JList<String> addList( JPanel parent, String[] possibilities, String animation ) { JList<String> list = new JList<>( possibilities ); parent.add( list ); list.setSelectedValue( animation, true ); return list; } @Override public void componentMoved( ComponentEvent arg0 ) { ConfigTools.setInt( atConfig, CFG_AT_WINDOW_LEFT, getX() ); ConfigTools.setInt( atConfig, CFG_AT_WINDOW_TOP, getY() ); atConfig.save(); } @Override public void keyPressed( KeyEvent e ) { switch( e.getKeyCode() ) { case KeyEvent.VK_RIGHT: // Speed up if( msFrameLength > 50 ) { msFrameLength -= 50; } return; case KeyEvent.VK_LEFT: // Slow msFrameLength += 50; return; case KeyEvent.VK_A: backwardStep = true; return; case KeyEvent.VK_D: forwardStep = true; return; case KeyEvent.VK_H: System.out.println( "" ); System.out.println( "Right arrow Speed up" ); System.out.println( "Left arrow Slow down" ); System.out.println( "A In step mode, back one frame" ); System.out.println( "D In step mode, forward one frame" ); System.out.println( "[CTRL]+X Clear all animations" ); System.out.println( "H Print this help" ); System.out.println( "L Toggle printing log of frames" ); System.out.println( "S Toggle step mode" ); System.out.println( "Q Quit" ); System.out.println( "F5 Dump 30 frames to png" ); System.out.println( "F6 After creating pngs, animate them" ); System.out.println( " Requires ImageMagick" ); return; case KeyEvent.VK_L: frameLogging = !frameLogging; return; case KeyEvent.VK_S: switch ( runMode ) { case RUN: runMode = Mode.STEP; return; case STEP: runMode = Mode.RUN; return; default: return; } case KeyEvent.VK_X: if ( e.isControlDown() ) { for ( int i = 0 ; i < 9 ; i++ ) { for ( int j = 0 ; j < 3 ; j++ ) { animationNames[i][j] = NONE; } } saveSelectionsToConfig(); } return; case KeyEvent.VK_F5: runMode = Mode.FRAME_DUMP; return; case KeyEvent.VK_F6: frameDumper.framesToGif(); return; case KeyEvent.VK_Q: System.exit( 0 ); return; // Should not be necessary. Gets rid of intermittent compiler warning default: // Ignore fat fingers } } } private Mode runMode = Mode.RUN; private FrameCounter firstFrameDumped = null; private FrameDumper frameDumper = null ; private boolean forwardStep = false; private boolean backwardStep = false; private boolean frameLogging = false; private int msFrameLength = 100 ; private final int tileSize; private final int numTilesX; boolean running; private final java.awt.Canvas canvas; private final Config atConfig; private final SwingPaint paint; private final BitmapCache<SwingBitmap> bitmapCache; private final AnimationCache animationCache; private final String[] allBlocks = new String[] { NONE, "land_block_1", "land_block_2", "land_block_3", "land_block_4", "land_rising_right_1", "land_rising_right_2", "land_rising_right_3", "land_rising_right_4", "land_rising_left_1", "land_rising_left_2", "land_rising_left_3", "land_rising_left_4", "bridge_rising_right", "bridge_rising_left", }; /** [0-8][0-2] position in 3x3 grid, and triplet of consecutive animations for * that position. */ private final String[][] animationNames; /** * Blocks don't animate, so just [0-8], one for each position. */ private final String[] blockNames; private static class InitUi implements Runnable { public AnimationTester animationTester; @Override public void run() { try { animationTester = new AnimationTester( createConfig() ); synchronized ( this ) { notifyAll(); } } catch ( Throwable t ) { synchronized ( this ) { notifyAll(); } t.printStackTrace(); System.exit( 3 ); } } public synchronized void loopWhenReady() { try { wait(); } catch ( InterruptedException e ) { // Will come when notified } if ( animationTester != null ) { animationTester.loop(); } } } public AnimationTester( Config atConfig ) { this.atConfig = atConfig; this.tileSize = ConfigTools.getInt( atConfig, CFG_AT_TILE_SIZE ); this.numTilesX = 3; this.animationCache = new AnimationCache( new AnimationLoader() ); /** Name of .rea files (changed to caps it's also the name of the state) */ this.animationNames = animationsFromConfig( atConfig.get( CFG_AT_ANIMATIONS ) ); this.blockNames = blocksFromConfig( atConfig.get( CFG_AT_BLOCKS ) ); int numTilesY = 3; setIgnoreRepaint( true ); setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); this.canvas = new java.awt.Canvas(); canvas.setIgnoreRepaint( true ); canvas.setPreferredSize( new java.awt.Dimension( tileSize * numTilesX, tileSize * numTilesY ) ); getContentPane().add( canvas, java.awt.BorderLayout.CENTER ); paint = new SwingPaint( null ); bitmapCache = new BitmapCache<SwingBitmap>( new SwingBitmapLoader(), new SwingBitmapScaler(), SwingMain.cacheSize() ); Listener listener = new Listener(); canvas.addMouseListener( listener ); canvas.addKeyListener( listener ); addComponentListener( listener ); setBoundsFromConfig(); setIcon(); setTitle( t( "Animation Tester" ) ); pack(); setVisible( true ); // Must do this after frame is made visible canvas.createBufferStrategy( 2 ); canvas.requestFocus(); } private void setBoundsFromConfig() { int x = ConfigTools.getInt( atConfig, CFG_AT_WINDOW_LEFT ); int y = ConfigTools.getInt( atConfig, CFG_AT_WINDOW_TOP ); if ( x != Integer.MIN_VALUE && y != Integer.MIN_VALUE ) { setLocation( x, y ); } } public static void main( String[] args ) { try { InitUi initUi = new InitUi(); SwingUtilities.invokeLater( initUi ); initUi.loopWhenReady(); } catch( Throwable t ) { t.printStackTrace(); System.exit( 2 ); } } private class FrameCounter { private int frameSetNum ; private int frameNum ; public FrameCounter( FrameCounter f ) { this.frameSetNum = f.frameSetNum; this.frameNum = f.frameNum; } public FrameCounter() { frameSetNum = 0; frameNum = 0; } @Override public int hashCode() { return 31 * frameSetNum + frameNum; } @Override public boolean equals( Object o ) { if ( null == o ) { return false; } if ( !(o instanceof FrameCounter ) ) { return false; } FrameCounter f = (FrameCounter)o; return this.frameSetNum == f.frameSetNum && this.frameNum == f.frameNum ; } public void inc() { ++frameNum; if ( frameNum == 10 ) { frameNum = 0; ++frameSetNum; if ( frameSetNum == 3 ) { frameSetNum = 0; } } } public void dec() { --frameNum; if( frameNum == 0) { frameNum = 9; --frameSetNum; if( frameSetNum == 0 ) { frameSetNum = 2; } } } public int getFrameNum() { return frameNum; } public int getFrameSetNum() { return frameSetNum; } } private void loop() { BufferStrategy strategy = canvas.getBufferStrategy(); Renderer<SwingBitmap, SwingPaint> renderer = new Renderer<SwingBitmap, SwingPaint>( 0, 0, tileSize, bitmapCache ); SoundPlayer soundPlayer = new SoundPlayer( SwingSound.create( false ) ); FrameCounter counter = new FrameCounter(); running = true; while( running && this.isVisible() ) { DrawFrame drawFrame = new DrawFrame( strategy, renderer, soundPlayer, counter.getFrameSetNum(), counter.getFrameNum() ); drawFrame.run(); switch ( runMode ) { case STEP: counter = keyStep(counter); continue; case RUN: counter = runStep( counter ); continue; case FRAME_DUMP: counter = frameDumpStep( counter, drawFrame ); continue; } } } private FrameCounter keyStep( FrameCounter counter ) { while( true ) { try { Thread.sleep( 50 ); } catch ( InterruptedException e ) { // Ignore } if( forwardStep ) { forwardStep = false; counter.inc(); return counter; } if( backwardStep ) { backwardStep = false; counter.dec(); return counter; } } } private FrameCounter runStep( FrameCounter counter ) { pause(); counter.inc(); return counter; } private FrameCounter frameDumpStep( FrameCounter counter, DrawFrame drawFrame ) { try { if ( counter.equals( firstFrameDumped ) ) { // A whole set has been dumped. Revert to normal runMode = Mode.RUN; firstFrameDumped = null; System.out.println(); counter.inc(); return counter; } if ( null == firstFrameDumped ) { frameDumper = new FrameDumper(); firstFrameDumped = new FrameCounter( counter ); } String frameID = String.format("%02d_%02d", counter.getFrameSetNum(), counter.getFrameNum() ); frameDumper.dump( canvas, drawFrame, frameID ); counter.inc(); return counter; } catch ( Exception e ) { e.printStackTrace(); System.exit( 1 ); return null; // The compiler can be dumb. } } private class DrawFrame extends BufferedDraw { private final int frameSetNum; private final int frameNum; private final Renderer<SwingBitmap, SwingPaint> renderer; private final SoundPlayer soundPlayer; public DrawFrame( BufferStrategy strategy, Renderer<SwingBitmap, SwingPaint> renderer, SoundPlayer soundPlayer, int frameSetNum, int frameNum ) { super( strategy ); this.renderer = renderer; this.soundPlayer = soundPlayer; this.frameSetNum = frameSetNum; this.frameNum = frameNum; } @Override void draw( java.awt.Graphics2D g ) { g.setPaint( java.awt.Color.WHITE ); g.fillRect( 0, 0, canvas.getWidth(), canvas.getHeight() ); List<Sprite> sprites = new ArrayList<>(); int i = 0; for ( String block : blockNames ) { if ( block == null || block.equals( NONE ) ) { ++i; continue; } java.awt.Point loc = int2dim( i ); sprites.add( new Sprite( block, null, loc.x, loc.y, 0, 0 ) ); ++i; } i = 0; for ( String[] animationSet : animationNames ) { String animationName = animationSet[frameSetNum]; if ( animationName != null && !animationName.equals( NONE ) ) { Animation animation = animationCache.get( animationName ); if ( animation != null ) { Frame frame = animation.get( frameNum ); java.awt.Point loc = int2dim( i ); Sprite sprite = new Sprite( frame.name, frame.soundEffect, loc.x, loc.y, frame.offsetX, frame.offsetY ); if( frameLogging && Mode.FRAME_DUMP != runMode ) { System.out.println( frame.name ); } sprites.add( sprite ); } } ++i; } soundPlayer.play( sprites ); renderer.render( new SwingCanvas( g, canvas.getWidth(), canvas.getHeight() ), sprites, paint ); } } private java.awt.Point int2dim( int i ) { return new java.awt.Point( i % 3, i / 3 ); } private int screen2index( int x, int y ) { return ( numTilesX * ( y / tileSize ) ) + ( x / tileSize ); } private void pause() { try { Thread.sleep( msFrameLength ); } catch ( InterruptedException e ) { // Ignore } } private static Config createConfig() { ConfigSchema definition = new ConfigSchema(); definition.set( CFG_AT_WINDOW_LEFT, String.valueOf( Integer.MIN_VALUE ), "The x position of the animation tester window on the screen" ); definition.set( CFG_AT_WINDOW_TOP, String.valueOf( Integer.MIN_VALUE ), "The y position of the animation tester window on the screen" ); definition.set( CFG_AT_TILE_SIZE, String.valueOf( 128 ), "The on-screen size of tiles in the animation tester in pixels" ); definition.set( CFG_AT_ANIMATIONS, "", "The animations selected to play in the animation tester" + " (3 per tile)" ); definition.set( CFG_AT_BLOCKS, "", "The blocks selected to play in the animation tester" ); return new Config( definition, new ConfigFile( new RealFileSystem(), CONFIG_PATH ) ); } private static String animationsToConfigString( String[][] animations ) { StringBuilder ret = new StringBuilder(); for ( String[] arr : animations ) { for ( String anim : arr ) { ret.append( anim ); ret.append( ' ' ); } } return ret.toString(); } private static String blocksToConfigString( String[] blocks ) { return join( " ", blocks ); } private static String[][] animationsFromConfig( String cfgEntry ) { if ( Util.isEmpty( cfgEntry ) ) { return defaultAnimationNames; } String[] items = cfgEntry.split( " " ); if ( items.length % 3 != 0 ) { return defaultAnimationNames; } String[][] ret = new String[items.length / 3][]; for ( int i = 0; i < items.length / 3; ++i ) { ret[i] = new String[3]; ret[i][0] = items[ i * 3 ]; ret[i][1] = items[ i * 3 + 1 ]; ret[i][2] = items[ i * 3 + 2 ]; } return ret; } private static String[] blocksFromConfig( String cfgEntry ) { if ( Util.isEmpty( cfgEntry ) ) { return defaultBlockNames; } return cfgEntry.split( " " ); } private void setIcon() { SwingBitmapLoader l = new SwingBitmapLoader(); int s = l.sizeFor( 128 ); SwingBitmap bmp = l.load( "rabbit_fall_01", s ); this.setIconImage(bmp.image); } }