package org.erikaredmark.monkeyshines.encoder; import java.awt.Color; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Logger; import org.erikaredmark.monkeyshines.AnimationSpeed; import org.erikaredmark.monkeyshines.AnimationType; import org.erikaredmark.monkeyshines.Conveyer; import org.erikaredmark.monkeyshines.DeathAnimation; import org.erikaredmark.monkeyshines.GameConstants; import org.erikaredmark.monkeyshines.Goodie; import org.erikaredmark.monkeyshines.Hazard; import org.erikaredmark.monkeyshines.ImmutablePoint2D; import org.erikaredmark.monkeyshines.ImmutableRectangle; import org.erikaredmark.monkeyshines.LevelScreen; import org.erikaredmark.monkeyshines.Sprite; import org.erikaredmark.monkeyshines.Sprite.TwoWayFacing; import org.erikaredmark.monkeyshines.TileMap; import org.erikaredmark.monkeyshines.World; import org.erikaredmark.monkeyshines.Conveyer.Rotation; import org.erikaredmark.monkeyshines.Sprite.ForcedDirection; import org.erikaredmark.monkeyshines.Sprite.SpriteType; import org.erikaredmark.monkeyshines.WorldCoordinate; import org.erikaredmark.monkeyshines.background.Background; import org.erikaredmark.monkeyshines.background.FullBackground; import org.erikaredmark.monkeyshines.background.SingleColorBackground; import org.erikaredmark.monkeyshines.bounds.Boundable; import org.erikaredmark.monkeyshines.bounds.IPoint2D; import org.erikaredmark.monkeyshines.encoder.exception.WorldRestoreException; import org.erikaredmark.monkeyshines.encoder.exception.WorldSaveException; import org.erikaredmark.monkeyshines.encoder.proto.WorldFormatProtos; import org.erikaredmark.monkeyshines.encoder.proto.WorldFormatProtos.World.BackgroundType; import org.erikaredmark.monkeyshines.resource.WorldResource; import org.erikaredmark.monkeyshines.tiles.CollapsibleTile; import org.erikaredmark.monkeyshines.tiles.CommonTile; import org.erikaredmark.monkeyshines.tiles.ConveyerTile; import org.erikaredmark.monkeyshines.tiles.HazardTile; import org.erikaredmark.monkeyshines.tiles.TileType; import org.erikaredmark.monkeyshines.tiles.CommonTile.StatelessTileType; /** * * A facade class that provides a mapping between the in-memory world of running the game and * the level editor, vs the persistent world of the google protocol buffer format. Instances * of this class are initialised either from the Java object model (starting with {@link World}) * or a {@code .world} file. Once initialised, it is possible to either create new instances of * {@code World} or save to/over instance of .world files on disk. * <p/> * Instances of this class are immutable. This class is designed to be created from one form, so it * may produce a world of the counter-form. * * @author Erika Redmark * */ public final class EncodedWorld { private static final String CLASS_NAME = "org.erikaredmark.monkeyshines.encoder.EncodedWorld"; private static final Logger LOGGER = Logger.getLogger(CLASS_NAME); private final WorldFormatProtos.World world; private EncodedWorld(final WorldFormatProtos.World world) { this.world = world; } /** * * Creates the encoded version from the world. The encoded version will maintain only * the immutable state of the world, and can be easily written to disk. * * @param world * world to encode * * @return * new instance of this object * */ public static EncodedWorld fromMemory(final World world) { WorldFormatProtos.World.Builder diskWorld = WorldFormatProtos.World.newBuilder(); diskWorld.setAuthor(world.getAuthor() ); diskWorld.setName(world.getWorldName() ); diskWorld.setBonusScreen(world.getBonusScreen() ); // Intentionally omitted; return screen auto-calculated from the first bonus door bonzo touches. //diskWorld.setReturnScreen(world.getReturnScreen() ); diskWorld.addAllHazards(hazardsToProto(world.getHazards() ) ); diskWorld.addAllGoodies(goodiesToProto(world.getGoodies() ) ); diskWorld.addAllLevels(levelsToProto(world.getLevelScreens() ) ); return new EncodedWorld(diskWorld.build() ); } /** * * Creates an encoded version from the given .world file. Once created, a new instance * of a {@code World} can be easily generated from this encoded form. If any changes are * made, a new instance of this object will need to be created via {@code fromMemory(World)} * in order to save it. * <p/> * This method does not close the stream after using it. * * @param stream * properly encoded .world stream, typically from a file, to load * * @return * new instance of this object * */ public static EncodedWorld fromStream(final InputStream stream) throws WorldRestoreException { try { WorldFormatProtos.World temp = WorldFormatProtos.World.parseFrom(stream); return new EncodedWorld(temp); } catch (IOException e) { throw new WorldRestoreException(e); } } /** * * Saves the given world to the given stream. Stream is left open after method ends. * * @param out * output stream to write to * */ public void save(OutputStream out) throws WorldSaveException { try { world.writeTo(out); } catch (IOException e) { throw new WorldSaveException(e); } } /** * * Creates a new world instance for use with the game or level editor that duplicates the * immutable state stored in the encoded form. Each call to this method returns a new object. * * @param rsrc * graphics resource, required to inflate objects that require access to graphics * and sound resources * * @return * new world instance from encoded form * */ public World newWorldInstance(final WorldResource rsrc) { final String worldName = world.getName(); final Map<WorldCoordinate, Goodie> goodiesInWorld = protoToGoodies(world.getGoodiesList(), rsrc); final List<Hazard> hazards = protoToHazards(world.getHazardsList() ); // Compare the generated hazards with the number in the resource. If less, add extra. // If more... log a warning, user is losing a hazard if they save. int expectedHazards = rsrc.getHazardCount(); if (hazards.size() > expectedHazards) { LOGGER.warning("Expected " + expectedHazards + " but save file shows " + hazards.size() + ". Hazards after the cutoff point will be auto-deleted on next size. Exit without saving and add hazards to the resource pack to fix."); for (int i = hazards.size() - 1; i >= expectedHazards; --i) { hazards.remove(i); } } else if (hazards.size() < expectedHazards){ List<Hazard> newHazards = Hazard.initialise(hazards.size(), expectedHazards - hazards.size(), rsrc); hazards.addAll(newHazards); } final int bonusScreen = this.world.getBonusScreen(); // Size of conveyers may be added to if the world is skinned with an updated // resource containing new conveyers. // generate as many conveyer instances as graphics resource allows. Decoding of the // actual levels with the auto-generated conveyers will handle conveyer belts. List<Conveyer> conveyers = new ArrayList<>(rsrc.getConveyerCount() * 2 ); World.generateConveyers(conveyers, rsrc.getConveyerCount() ); // Finally, all the different kinds of tiles loaded, we can load the actually tilemap that requires references // to those tiles final Map<Integer, LevelScreen> worldScreens = protoToLevels(world.getLevelsList(), rsrc, hazards, conveyers); World newWorld = new World(worldName, goodiesInWorld, worldScreens, hazards, conveyers, bonusScreen, rsrc); newWorld.resetAllScreens(); return newWorld; } /* ------------------------------ Hazards -------------------------------- */ static List<WorldFormatProtos.World.Hazard> hazardsToProto(List<Hazard> hazards) { List<WorldFormatProtos.World.Hazard> protoHazards = new ArrayList<>(hazards.size() ); for (Hazard h : hazards) { protoHazards.add(hazardToProto(h) ); } return protoHazards; } static WorldFormatProtos.World.Hazard hazardToProto(Hazard hazard) { WorldFormatProtos.World.Hazard.Builder protoHazard = WorldFormatProtos.World.Hazard.newBuilder(); protoHazard.setId(hazard.getId() ); protoHazard.setExplodes(hazard.getExplodes() ); protoHazard.setDeathAnimation(deathAnimationToProto(hazard.getDeathAnimation() ) ); protoHazard.setHarmless(hazard.isHarmless() ); return protoHazard.build(); } static List<Hazard> protoToHazards(List<WorldFormatProtos.World.Hazard> protoHazards) { List<Hazard> hazards = new ArrayList<>(protoHazards.size() ); for (WorldFormatProtos.World.Hazard h : protoHazards) { hazards.add(protoToHazard(h) ); } return hazards; } static Hazard protoToHazard(WorldFormatProtos.World.Hazard protoHazard) { return new Hazard(protoHazard.getId(), protoHazard.getExplodes(), protoToDeathAnimation(protoHazard.getDeathAnimation() ), protoHazard.getHarmless() ); } /* ------------------------------ Death Animation -------------------------------- */ static WorldFormatProtos.World.DeathAnimation deathAnimationToProto(DeathAnimation animation) { switch (animation) { case BEE: return WorldFormatProtos.World.DeathAnimation.BEE; case BURN: return WorldFormatProtos.World.DeathAnimation.BURN; case NORMAL: return WorldFormatProtos.World.DeathAnimation.NORMAL_DEATH; case ELECTRIC: return WorldFormatProtos.World.DeathAnimation.ELECTRIC; default: throw new RuntimeException("Death Animation " + animation + " has no version in Proto format!"); } } static DeathAnimation protoToDeathAnimation(WorldFormatProtos.World.DeathAnimation protoAnimation) { switch (protoAnimation) { case BEE: return DeathAnimation.BEE; case BURN: return DeathAnimation.BURN; case NORMAL_DEATH: return DeathAnimation.NORMAL; case ELECTRIC: return DeathAnimation.ELECTRIC; default: throw new RuntimeException("Death Animation proto " + protoAnimation + " has no Java object!"); } } /* ------------------------------ Goodies -------------------------------- */ static List<WorldFormatProtos.World.StringToGoodieTuple> goodiesToProto(Map<WorldCoordinate, Goodie> goodies) { List<WorldFormatProtos.World.StringToGoodieTuple> protoGoodies = new ArrayList<>(goodies.values().size() ); for (Entry<WorldCoordinate, Goodie> entry : goodies.entrySet() ) { WorldFormatProtos.World.Goodie goodie = goodieToProto(entry.getValue() ); protoGoodies.add(WorldFormatProtos.World.StringToGoodieTuple.newBuilder() .setOne(entry.getKey().createSavedStringForm() ) .setTwo(goodie) .build() ); } return protoGoodies; } static WorldFormatProtos.World.Goodie goodieToProto(Goodie goodie) { WorldFormatProtos.World.Goodie.Builder protoGoodie = WorldFormatProtos.World.Goodie.newBuilder(); protoGoodie.setId(goodie.getGoodieType().id() ); protoGoodie.setScreenId(goodie.getScreenID() ); protoGoodie.setLocation(pointToProto(goodie.getLocation() ) ); return protoGoodie.build(); } static Map<WorldCoordinate, Goodie> protoToGoodies(List<WorldFormatProtos.World.StringToGoodieTuple> protoGoodies, WorldResource rsrc) { Map<WorldCoordinate, Goodie> goodies = new HashMap<>(protoGoodies.size() ); for (WorldFormatProtos.World.StringToGoodieTuple tuple : protoGoodies) { goodies.put(WorldCoordinate.fromSavedStringForm(tuple.getOne() ), protoToGoodie(tuple.getTwo(), rsrc) ); } return goodies; } static Goodie protoToGoodie(WorldFormatProtos.World.Goodie protoGoodie, WorldResource rsrc) { return Goodie.newGoodie(Goodie.Type.byValue(protoGoodie.getId() ), protoToPoint(protoGoodie.getLocation() ), protoGoodie.getScreenId(), rsrc); } /* ------------------------------ Point -------------------------------- */ static WorldFormatProtos.World.Point pointToProto(IPoint2D point) { WorldFormatProtos.World.Point.Builder protoPoint = WorldFormatProtos.World.Point.newBuilder(); protoPoint.setX(point.x() ); protoPoint.setY(point.y() ); return protoPoint.build(); } static ImmutablePoint2D protoToPoint(WorldFormatProtos.World.Point protoPoint) { return ImmutablePoint2D.of(protoPoint.getX(), protoPoint.getY() ); } /* ------------------------------ Levels -------------------------------- */ static List<WorldFormatProtos.World.IntegerToLevelTuple> levelsToProto(Map<Integer, LevelScreen> levels) { List<WorldFormatProtos.World.IntegerToLevelTuple> protoLevels = new ArrayList<>(levels.size() ); for (Entry<Integer, LevelScreen> entry : levels.entrySet() ) { WorldFormatProtos.World.LevelScreen level = levelToProto(entry.getValue() ); protoLevels.add(WorldFormatProtos.World.IntegerToLevelTuple.newBuilder() .setOne(entry.getKey() ) .setTwo(level) .build() ); } return protoLevels; } static WorldFormatProtos.World.LevelScreen levelToProto(LevelScreen level) { WorldFormatProtos.World.LevelScreen.Builder protoLevel = WorldFormatProtos.World.LevelScreen.newBuilder(); protoLevel.setId(level.getId() ); protoLevel.setBonzoLocation(pointToProto(level.getBonzoStartingLocation() ) ); protoLevel.setBackground(backgroundToProto(level.getBackground() ) ); protoLevel.addAllSprites(spritesToProto(level.getSpritesOnScreen() ) ); protoLevel.addAllTiles(tilesToProto(level.getMap() ) ); return protoLevel.build(); } static Map<Integer, LevelScreen> protoToLevels(List<WorldFormatProtos.World.IntegerToLevelTuple> protoLevels, WorldResource rsrc, List<Hazard> hazards, List<Conveyer> conveyers) { Map<Integer, LevelScreen> levels = new HashMap<>(protoLevels.size() ); for (WorldFormatProtos.World.IntegerToLevelTuple tuple : protoLevels) { levels.put(tuple.getOne(), protoToLevel(tuple.getTwo(), rsrc, hazards, conveyers) ); } return levels; } static LevelScreen protoToLevel(WorldFormatProtos.World.LevelScreen protoLevel, WorldResource rsrc, List<Hazard> hazards, List<Conveyer> conveyers) { return new LevelScreen(protoLevel.getId(), protoToBackground(protoLevel.getBackground(), rsrc), protoToTiles(protoLevel.getTilesList(), rsrc, hazards, conveyers), protoToPoint(protoLevel.getBonzoLocation() ), protoToSprites(protoLevel.getSpritesList(), rsrc ), rsrc); } /* ---------------------------- Backgrounds ------------------------------ */ static WorldFormatProtos.World.Background backgroundToProto(Background b) { WorldFormatProtos.World.Background.Builder protoBackground = WorldFormatProtos.World.Background.newBuilder(); if (b instanceof FullBackground) { FullBackground full = (FullBackground)b; protoBackground.setId(full.getId() ); BackgroundType backgroundType = full.isPattern() ? BackgroundType.PATTERN : BackgroundType.FULL; protoBackground.setType(backgroundType); } else { // Solid color Color color = ((SingleColorBackground)b).getColor(); // Store as ARGB. Just in case the colour model ISN'T ARGB, we manually create // the integer. Alpha will be lost anyway, for now, but store it in case we ever decide not // to lose it during decoding. int argb = (color.getAlpha() << 24) | (color.getRed() << 16) | (color.getGreen() << 8) | (color.getBlue() ); protoBackground.setId(argb); protoBackground.setType(BackgroundType.SOLID_COLOR); } return protoBackground.build(); } static Background protoToBackground(WorldFormatProtos.World.Background protoBackground, WorldResource rsrc) { int id = protoBackground.getId(); switch (protoBackground.getType() ) { case FULL: if (id >= rsrc.getBackgroundCount() ) throw new RuntimeException("Requested full background id " + id + " does not exist in resource pack"); return rsrc.getBackground(id); case PATTERN: if (id >= rsrc.getPatternCount() ) throw new RuntimeException("Requested pattern id " + id + " does not exist in resource pack"); return rsrc.getPattern(id); case SOLID_COLOR: // Id is an ARGB encoded Color object. // Alpha is lost in this constructor. However, backgrounds should not // HAVE an alpha to begin with. For now, just be content with losing it. Color color = new Color(id); return new SingleColorBackground(color); default: throw new RuntimeException("Decoder cannot handle background type " + protoBackground.getType() ); } } /* ------------------------------ Sprites -------------------------------- */ static List<WorldFormatProtos.World.Sprite> spritesToProto(List<Sprite> sprites) { List<WorldFormatProtos.World.Sprite> protoSprites = new ArrayList<>(sprites.size() ); for (Sprite s : sprites) { protoSprites.add(spriteToProto(s) ); } return protoSprites; } static WorldFormatProtos.World.Sprite spriteToProto(Sprite sprite) { WorldFormatProtos.World.Sprite.Builder protoSprite = WorldFormatProtos.World.Sprite.newBuilder(); protoSprite.setId(sprite.getId() ); protoSprite.setStartLocation(pointToProto(sprite.getStaringLocation() ) ); protoSprite.setBoundingBox(boxToProto(sprite.getBoundingBox() ) ); protoSprite.setAnimation(animationTypeToProto(sprite.getAnimationType() ) ); protoSprite.setAnimationSpeed(animationSpeedToProto(sprite.getAnimationSpeed() ) ); protoSprite.setType(spriteTypeToProto(sprite.getType() ) ); protoSprite.setForcedDirection(forcedDirectionToProto(sprite.getForcedDirection() ) ); protoSprite.setTwoSetsDirection(twoWayFacingToProto(sprite.getTwoWayFacing() ) ); // Build a point for storage WorldFormatProtos.World.Point.Builder initialSpeed = WorldFormatProtos.World.Point.newBuilder(); initialSpeed.setX(sprite.getInitialSpeedX() ); initialSpeed.setY(sprite.getInitialSpeedY() ); protoSprite.setInitialSpeed(initialSpeed.build() ); return protoSprite.build(); } static List<Sprite> protoToSprites(List<WorldFormatProtos.World.Sprite> protoSprites, WorldResource rsrc) { List<Sprite> sprites = new ArrayList<>(protoSprites.size() ); for (WorldFormatProtos.World.Sprite s : protoSprites) { sprites.add(protoToSprite(s, rsrc) ); } return sprites; } static Sprite protoToSprite(WorldFormatProtos.World.Sprite protoSprite, WorldResource rsrc) { return Sprite.newSprite(protoSprite.getId(), protoToPoint(protoSprite.getStartLocation() ), protoToBox(protoSprite.getBoundingBox() ), protoToPoint(protoSprite.getInitialSpeed() ), protoToAnimationType(protoSprite.getAnimation() ), protoToAnimationSpeed(protoSprite.getAnimationSpeed() ), protoToSpriteType(protoSprite.getType() ), protoToForcedDirection(protoSprite.getForcedDirection() ), protoToTwoWayFacing(protoSprite.getTwoSetsDirection() ), rsrc); } /* ------------------------- Two Way Facing -------------------------- */ static WorldFormatProtos.World.TwoWayFacing twoWayFacingToProto(TwoWayFacing facing) { switch (facing) { case SINGLE: return WorldFormatProtos.World.TwoWayFacing.SINGLE; case HORIZONTAL: return WorldFormatProtos.World.TwoWayFacing.TWO_WAY_HORIZONTAL; case VERTICAL: return WorldFormatProtos.World.TwoWayFacing.TWO_WAY_VERTICAL; default: throw new RuntimeException("Two Way facing type " + facing + " has no defined proto version"); } } static TwoWayFacing protoToTwoWayFacing(WorldFormatProtos.World.TwoWayFacing protoFacing) { // note: protoFacing may be null and probably is even in the distributed worlds, as two-way facing // was an automatic property based on the graphics resource and assumed horizontal. Whilst it is // still automatic to a degree, it can now be requested vertical (although it won't comply if the // graphics can't handle it). if (protoFacing == null || protoFacing == WorldFormatProtos.World.TwoWayFacing.TWO_WAY_UNUSED) { // Will become SINGLE if the graphics can't take it. return TwoWayFacing.HORIZONTAL; } else { switch (protoFacing) { case SINGLE: return TwoWayFacing.SINGLE; case TWO_WAY_HORIZONTAL: return TwoWayFacing.HORIZONTAL; case TWO_WAY_VERTICAL: return TwoWayFacing.VERTICAL; default: throw new RuntimeException("Two Way facing type " + protoFacing + " has no defined java object"); } } } /* ------------------------ Forced Direction ------------------------- */ static WorldFormatProtos.World.ForcedDirection forcedDirectionToProto(ForcedDirection forced) { switch (forced) { case NONE: return WorldFormatProtos.World.ForcedDirection.FORCED_NONE; case RIGHT_UP: return WorldFormatProtos.World.ForcedDirection.FORCED_RIGHT; case LEFT_DOWN: return WorldFormatProtos.World.ForcedDirection.FORCED_LEFT; default: throw new RuntimeException("Forced Direction type " + forced + " has no defined proto version"); } } static ForcedDirection protoToForcedDirection(WorldFormatProtos.World.ForcedDirection protoForced) { switch (protoForced) { case FORCED_NONE: return ForcedDirection.NONE; case FORCED_RIGHT: return ForcedDirection.RIGHT_UP; case FORCED_LEFT: return ForcedDirection.LEFT_DOWN; default: throw new RuntimeException("Forced Direction type " + protoForced + " has no defined java object"); } } /* --------------------------- Sprite Type --------------------------- */ static WorldFormatProtos.World.SpriteType spriteTypeToProto(SpriteType spriteType) { switch (spriteType) { case NORMAL: return WorldFormatProtos.World.SpriteType.NORMAL; case HEALTH_DRAIN: return WorldFormatProtos.World.SpriteType.HEALTH_DRAIN; case BONUS_DOOR: return WorldFormatProtos.World.SpriteType.BONUS_DOOR; case EXIT_DOOR: return WorldFormatProtos.World.SpriteType.EXIT_DOOR; case SCENERY: return WorldFormatProtos.World.SpriteType.SCENERY_SPRITE; default: throw new RuntimeException("Sprite type " + spriteType + " has no defined proto version"); } } static SpriteType protoToSpriteType(WorldFormatProtos.World.SpriteType spriteTypeProto) { switch (spriteTypeProto) { case NORMAL: return SpriteType.NORMAL; case HEALTH_DRAIN: return SpriteType.HEALTH_DRAIN; case BONUS_DOOR: return SpriteType.BONUS_DOOR; case EXIT_DOOR: return SpriteType.EXIT_DOOR; case SCENERY_SPRITE: return SpriteType.SCENERY; default: throw new RuntimeException("Proto sprite type " + spriteTypeProto + " has no defined java object"); } } /* ------------------------------ Box -------------------------------- */ static WorldFormatProtos.World.Box boxToProto(Boundable box) { WorldFormatProtos.World.Box.Builder protoBox = WorldFormatProtos.World.Box.newBuilder(); protoBox.setTopLeft(pointToProto(box.getLocation() ) ); protoBox.setSize(pointToProto(box.getSize() ) ); return protoBox.build(); } // We stick with immutable types for decoding static ImmutableRectangle protoToBox(WorldFormatProtos.World.Box protoBox) { IPoint2D start = protoToPoint(protoBox.getTopLeft() ); IPoint2D size = protoToPoint(protoBox.getSize() ); return ImmutableRectangle.of(start.x(), start.y(), size.x(), size.y() ); } /* ------------------------------ Animation Type -------------------------------- */ static WorldFormatProtos.World.AnimationType animationTypeToProto(AnimationType type) { switch (type) { case INCREASING_FRAMES: return WorldFormatProtos.World.AnimationType.INCREASING_FRAMES; case CYCLING_FRAMES: return WorldFormatProtos.World.AnimationType.CYLCING_FRAMES; default: throw new RuntimeException("Animation type " + type + " has no defined proto version!"); } } static AnimationType protoToAnimationType(WorldFormatProtos.World.AnimationType type) { switch (type) { case INCREASING_FRAMES: return AnimationType.INCREASING_FRAMES; case CYLCING_FRAMES: return AnimationType.CYCLING_FRAMES; default: throw new RuntimeException("Animation type proto " + type + " has no defined Java object!"); } } /* ------------------------------ Animation Speed -------------------------------- */ static WorldFormatProtos.World.AnimationSpeed animationSpeedToProto(AnimationSpeed speed) { switch (speed) { case NORMAL: return WorldFormatProtos.World.AnimationSpeed.NORMAL_SPEED; case SLOW: return WorldFormatProtos.World.AnimationSpeed.SLOW; default: throw new RuntimeException("Animation speed " + speed + " has no defined proto version!"); } } static AnimationSpeed protoToAnimationSpeed(WorldFormatProtos.World.AnimationSpeed speed) { switch (speed) { case NORMAL_SPEED: return AnimationSpeed.NORMAL; case SLOW: return AnimationSpeed.SLOW; default: throw new RuntimeException("Animation speed proto " + speed + " has no defined Java object!"); } } /* ------------------------------ Tiles -------------------------------- */ // Saves the tiles themselves as well as proper dimensions of the tilemap. // NOTICE: This is intended as part of LevelScreen. Only tilemaps of 20 rows by 32 columns are supported. static List<WorldFormatProtos.World.Tile> tilesToProto(TileMap map) { List<WorldFormatProtos.World.Tile> protoTiles = new ArrayList<>(GameConstants.LEVEL_ROWS * GameConstants.LEVEL_COLS); for (TileType t : map.internalMap() ) { WorldFormatProtos.World.Tile.Builder protoTile = WorldFormatProtos.World.Tile.newBuilder(); protoTile.setId(t.getId() ); protoTile.setType(tileTypeToProto(t) ); if (t instanceof ConveyerTile) { protoTile.setRotation(rotationToProto(((ConveyerTile) t).getConveyer().getRotation() ) ); } protoTiles.add(protoTile.build() ); } return protoTiles; } // Loads the tiles themselves as well as proper dimensions of the tilemap. // NOTICE: This is intended as part of LevelScreen. Only tilemaps of 20 rows by 32 columns are supported. static TileMap protoToTiles(List<WorldFormatProtos.World.Tile> protoTiles, WorldResource rsrc, List<Hazard> hazards, List<Conveyer> conveyers) { TileMap map = new TileMap(GameConstants.LEVEL_ROWS, GameConstants.LEVEL_COLS); TileType[] internalMap = map.internalMap(); Iterator<WorldFormatProtos.World.Tile> it = protoTiles.iterator(); for (int i = 0; i < GameConstants.TOTAL_TILES; ++i) { assert it.hasNext(); WorldFormatProtos.World.Tile encodedTile = it.next(); internalMap[i] = protoToTileType(encodedTile.getType(), encodedTile, rsrc, hazards, conveyers); } return map; } /* ------------------------------ Tile Types -------------------------------- */ // This is not enough to convert ALL tile type information to proto form. One must // determine which exact type it is and if there is need for additional information extraction // for the proto form of Tile static WorldFormatProtos.World.TileType tileTypeToProto(TileType type) { if (type instanceof CommonTile) { switch (((CommonTile)type).getUnderlyingType() ) { case NONE: return WorldFormatProtos.World.TileType.NONE; case SOLID: return WorldFormatProtos.World.TileType.SOLID; case THRU: return WorldFormatProtos.World.TileType.THRU; case SCENE: return WorldFormatProtos.World.TileType.SCENERY; default: throw new RuntimeException("Stateless Tile Type " + type + " has no defined proto version!"); } } else if (type instanceof HazardTile) { return WorldFormatProtos.World.TileType.HAZARD; } else if (type instanceof ConveyerTile) { return WorldFormatProtos.World.TileType.CONVEYER; } else if (type instanceof CollapsibleTile) { return WorldFormatProtos.World.TileType.BREAKING; } else { throw new RuntimeException("Tile type " + type + " has no defined proto version!"); } } // Extra parameters are required for setting up some more complicated tiles. static TileType protoToTileType(WorldFormatProtos.World.TileType type, WorldFormatProtos.World.Tile tile, WorldResource rsrc, List<Hazard> hazards, List<Conveyer> conveyers) { switch (type) { case NONE: return CommonTile.NONE; case SOLID: return CommonTile.of(tile.getId(), StatelessTileType.SOLID, rsrc); case THRU: return CommonTile.of(tile.getId(), StatelessTileType.THRU, rsrc); case SCENERY: return CommonTile.of(tile.getId(), StatelessTileType.SCENE, rsrc); case HAZARD: return HazardTile.forHazard(hazards.get(tile.getId() ) ); case CONVEYER: return new ConveyerTile(conveyers.get(tile.getId() * 2 + getConveyerIdOffset(tile.getRotation() ) ) ); case BREAKING: return new CollapsibleTile(tile.getId() ); default: throw new RuntimeException("Proto tiletype " + type + " has no defined java object!"); } } // Returns 0 for clockwise rotation and 1 for anti-clockwise rotation. // Used to get offset for mapping encoded conveyer to proper real conveyer private static int getConveyerIdOffset(WorldFormatProtos.World.Rotation rotation) { if (rotation == WorldFormatProtos.World.Rotation.CLOCKWISE) return 0; else return 1; } /* ------------------------------ Rotation -------------------------------- */ // specific to conveyer belts static WorldFormatProtos.World.Rotation rotationToProto(Rotation rotation) { switch (rotation) { case CLOCKWISE: return WorldFormatProtos.World.Rotation.CLOCKWISE; case ANTI_CLOCKWISE: return WorldFormatProtos.World.Rotation.ANTI_CLOCKWISE; case NONE: throw new RuntimeException("No rotation is not valid for save file format: API error"); default: throw new RuntimeException("Rotation " + rotation + " has no defined proto version!"); } } static Rotation protoToRotation(WorldFormatProtos.World.Rotation rotation) { switch (rotation) { case CLOCKWISE: return Rotation.CLOCKWISE; case ANTI_CLOCKWISE: return Rotation.ANTI_CLOCKWISE; default: throw new RuntimeException("Proto Rotation " + rotation + " has no defined java object!"); } } /** * * Creates a new world that is empty. The newly created world will have the given name and a single screen of id 1000. * * @param name * the name of the new world * * @return * an empty encoded world * */ public static EncodedWorld fresh(String name) { // Set up all defaults WorldFormatProtos.World.Builder newWorld = WorldFormatProtos.World.newBuilder(); newWorld.setAuthor("Unknown"); newWorld.setName(name); newWorld.setBonusScreen(10000); // Goodies and hazards have no entries. WorldFormatProtos.World.LevelScreen.Builder emptyLevel = WorldFormatProtos.World.LevelScreen.newBuilder(); emptyLevel.setId(1000); // Use a solid colour, because barring additional resource pack information, solid colors will always // be present emptyLevel.setBackground(WorldFormatProtos.World.Background.newBuilder() .setType(WorldFormatProtos.World.BackgroundType.SOLID_COLOR) .setId(Color.BLACK.getRGB() ) .build() ); emptyLevel.setBonzoLocation(WorldFormatProtos.World.Point.newBuilder() .setX(0) .setY(0) .build() ); // MUST set up tiles to all empty List<WorldFormatProtos.World.Tile> tiles = new ArrayList<>(GameConstants.TOTAL_TILES); for (int i = 0; i < GameConstants.TOTAL_TILES; ++i) { tiles.add(WorldFormatProtos.World.Tile.newBuilder() .setId(0) .setType(WorldFormatProtos.World.TileType.NONE) .build() ); } emptyLevel.addAllTiles(tiles); newWorld.addLevels(WorldFormatProtos.World.IntegerToLevelTuple.newBuilder() .setOne(1000) .setTwo(emptyLevel) .build() ); return new EncodedWorld(newWorld.build() ); } }