// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.resource.are.viewer;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.awt.image.VolatileImage;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.infinity.datatype.Bitmap;
import org.infinity.datatype.DecNumber;
import org.infinity.datatype.Flag;
import org.infinity.datatype.HexNumber;
import org.infinity.datatype.ResourceRef;
import org.infinity.datatype.SectionCount;
import org.infinity.datatype.SectionOffset;
import org.infinity.datatype.TextString;
import org.infinity.gui.RenderCanvas;
import org.infinity.resource.Profile;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.graphics.GraphicsResource;
import org.infinity.resource.graphics.ColorConvert;
import org.infinity.resource.graphics.TisDecoder;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.resource.wed.Door;
import org.infinity.resource.wed.Overlay;
import org.infinity.resource.wed.Tilemap;
import org.infinity.resource.wed.WedResource;
/**
* Specialized renderer for drawing tileset-based graphics data.
*/
public class TilesetRenderer extends RenderCanvas
{
public static final String[] LabelVisualStates = {"Day", "Twilight", "Night"};
// Rendering modes for tiles (affects how to render overlayed tiles)
public static final int MODE_AUTO = 0; // mode based on current game id
public static final int MODE_BG1 = 1; // forces BG1 rendering mode
public static final int MODE_BG2 = 2; // forces BG2 rendering mode
private static final int MaxOverlays = 8; // max. supported overlay entries
private static final double MinZoomFactor = 1.0/64.0; // lower zoom factor limit
private static final double MaxZoomFactor = 16.0; // upper zoom factor limit
// Lighting adjustment for day/twilight/night times (multiplied by 10.24 for faster calculations)
// Formula:
// red = (red * LightingAdjustment[lighting][0]) >>> LightingAdjustmentShift;
// green = (green * LightingAdjustment[lighting][1]) >>> LightingAdjustmentShift;
// blue = (blue * LightingAdjustment[lighting][2]) >>> LightingAdjustmentShift;
public static final int[][] LightingAdjustment = new int[][]
// (100%, 100%, 100%), (100%, 85%, 80%), (45%, 45%, 85%)
{ {0x400, 0x400, 0x400}, {0x400, 0x366, 0x333}, {0x1cd, 0x1cd, 0x366} };
public static final int LightingAdjustmentShift = 10; // use in place of division
// keeps track of registered listener objects
private final List<TilesetChangeListener> listChangeListener = new ArrayList<TilesetChangeListener>();
// graphics data for all tiles of each overlay
private final List<Tileset> listTilesets = new ArrayList<Tileset>(MaxOverlays);
// array of tile indices used for closed door states for each door structure
private final List<DoorInfo> listDoorTileIndices = new ArrayList<DoorInfo>();
private final BufferedImage workingTile = ColorConvert.createCompatibleImage(64, 64, true); // internally used for drawing tile graphics
private WedResource wed; // current wed resource
private int renderingMode = MODE_AUTO; // the rendering mode to use for processing overlayed tiles
private boolean overlaysEnabled = true; // indicates whether to draw overlays
private boolean blendedOverlays; // indicates whether to blend overlays with tile graphics
private boolean hasChangedMap, hasChangedAppearance, hasChangedOverlays, hasChangedDoorState;
private boolean isClosed = false; // opened/closed state of door tiles
private boolean showGrid = false; // indicates whether to draw a grid on the tiles
private boolean forcedInterpolation = false; // indicates whether to use a pre-defined interpolation type or set one based on zoom factor
private double zoomFactor = 1.0; // zoom factor for drawing the map
private int lighting = ViewerConstants.LIGHTING_DAY; // the lighting condition to be used (day/twilight/night)
private int miniMapType = ViewerConstants.MAP_NONE; // the currently overlayed mini map (one of the MAP_XXX constants)
private int miniMapAlpha = 128; // alpha transparency for overlayed mini maps
private GraphicsResource miniMap = null; // the current mini map resource
/**
* Returns the number of supported lighting modes.
*/
public static int getLightingModesCount()
{
return LabelVisualStates.length;
}
public TilesetRenderer()
{
this(null);
}
public TilesetRenderer(WedResource wed)
{
super();
init(wed);
}
/**
* Adds a ChangeListener to the component. A change event will be triggered on changing map dimensions
* or setting up a new map.
* @param listener The listener to add.
*/
public void addChangeListener(TilesetChangeListener listener)
{
if (listener != null) {
if (listChangeListener.indexOf(listener) < 0) {
listChangeListener.add(listener);
}
}
}
/**
* Returns an array of all the ChangeListeners added to this component.
* @return All ChangeListeners added or an empty array.
*/
public TilesetChangeListener[] getChangeListeners()
{
return (TilesetChangeListener[])listChangeListener.toArray();
}
/**
* Removes a ChangeListener from the component.
* @param listener The listener to remove.
*/
public void removeChangeListener(TilesetChangeListener listener)
{
if (listener != null) {
listChangeListener.remove(listener);
}
}
/**
* Initializes and displays the specified map. The current map will be discarded.
* Triggers a change event.
* @param wed WED resource structure used to construct a map.
* @return true if map has been initialized successfully, false otherwise.
*/
public boolean loadMap(WedResource wed)
{
if (this.wed != wed) {
if (init(wed)) {
return true;
} else {
return false;
}
} else {
return true;
}
}
/**
* Returns whether a map has been loaded.
*/
public boolean isMapLoaded()
{
return isInitialized();
}
/**
* Returns the currently loaded WED resources.
*/
public WedResource getWed()
{
return wed;
}
/**
* Removes the current map and all associated data from memory.
*/
public void clear()
{
release(true);
}
/**
* Returns the current mode for processing overlays.
*/
public int getRenderingMode()
{
return renderingMode;
}
/**
* Specify how to draw overlayed tiles. Possible choices are MODE_AUTO, MODE_BG1 and MODE_BG2.
* @param mode The new rendering mode
*/
public void setRenderingMode(int mode)
{
if (mode < MODE_AUTO) mode = MODE_AUTO; else if (mode > MODE_BG2) mode = MODE_BG2;
if (mode != renderingMode) {
renderingMode = mode;
hasChangedOverlays = true;
updateDisplay();
}
}
/**
* Returns whether overlays are drawn.
*/
public boolean isOverlaysEnabled()
{
return overlaysEnabled;
}
/**
* Enable or disable the display of overlays.
*/
public void setOverlaysEnabled(boolean enable)
{
if (overlaysEnabled != enable) {
overlaysEnabled = enable;
hasChangedOverlays = true;
updateDisplay();
}
}
/**
* Returns whether the current map contains overlays
*/
public boolean hasOverlays()
{
if (isInitialized()) {
return !listTilesets.get(0).listOverlayTiles.isEmpty();
}
return false;
}
/**
* Returns the current zoom factor.
* @return The currently used zoom factor.
*/
public double getZoomFactor()
{
return zoomFactor;
}
/**
* Sets a new zoom factor for display. Clamped to range [0.25, 4.0].
* Triggers a change event if the zoom factor changes.
* @param factor The new zoom factor to use.
*/
public void setZoomFactor(double factor)
{
if (factor < MinZoomFactor) factor = MinZoomFactor; else if (factor > MaxZoomFactor) factor = MaxZoomFactor;
if (factor != zoomFactor) {
zoomFactor = factor;
hasChangedMap = true;
updateDisplay();
}
}
/**
* Returns whether the renderer is forced to use the predefined interpolation type on scaling.
*/
public boolean isForcedInterpolation()
{
return forcedInterpolation;
}
/**
* Specifies whether the renderer uses the best interpolation type based on the current zoom factor
* or uses a predefined interpolation type only.
* @param set If {@code true}, uses a predefined interpolation type only.
* If {@code false}, chooses an interpolation type automatically.
*/
public void setForcedInterpolation(boolean set)
{
if (set != forcedInterpolation) {
forcedInterpolation = set;
hasChangedAppearance = true;
updateDisplay();
}
}
/**
* Returns the opened/closed state of door tiles.
* @return The opened/closed state of door tiles.
*/
public boolean isDoorsClosed()
{
return isClosed;
}
/**
* Sets the opened/closed state of door tiles. Triggers a change event if the state changes.
* @param isClosed The new opened/closed state of door tiles.
*/
public void setDoorsClosed(boolean isClosed)
{
if (this.isClosed != isClosed) {
this.isClosed = isClosed;
hasChangedDoorState = true;
updateDisplay();
}
}
/**
* Returns whether the current map contains closeable doors.
*/
public boolean hasDoors()
{
if (isInitialized()) {
return !listDoorTileIndices.isEmpty();
}
return false;
}
/**
* Returns the currently used lighting condition.
* @return The currently used lighting condition.
*/
public int getLighting()
{
return lighting;
}
/**
* Sets a new lighting condition to be used to draw the map. Only meaningful for day maps.
* @param lighting The lighting condition to use. (One of the constants {@code LIGHTING_DAY},
* {@code LIGHTING_DUSK} or {@code LIGHTING_NIGHT})
*/
public void setLighting(int lighting)
{
if (lighting < ViewerConstants.LIGHTING_DAY) lighting = ViewerConstants.LIGHTING_DAY;
else if (lighting > ViewerConstants.LIGHTING_NIGHT) lighting = ViewerConstants.LIGHTING_NIGHT;
if (lighting != this.lighting) {
this.lighting = lighting;
hasChangedAppearance = true;
updateDisplay();
}
}
public boolean isGridEnabled()
{
return showGrid;
}
public void setGridEnabled(boolean enable)
{
if (enable != showGrid) {
showGrid = enable;
hasChangedAppearance = true;
updateDisplay();
}
}
/**
* Returns the width of the current map in pixels. Zoom factor is not taken into account.
* @return Map width in pixels.
*/
public int getMapWidth(boolean scaled)
{
if (isInitialized()) {
int w = listTilesets.get(0).tilesX * 64;
if (scaled) {
return (int)Math.ceil((double)w * zoomFactor);
} else {
return w;
}
} else {
return 0;
}
}
/**
* Returns the height of the current map in pixels. Zoom factor is not taken into account.
* @return Map height in pixels.
*/
public int getMapHeight(boolean scaled)
{
if (isInitialized()) {
int h = listTilesets.get(0).tilesY * 64;
if (scaled) {
return (int)Math.ceil((double)h * zoomFactor);
} else {
return h;
}
} else {
return 0;
}
}
/**
* Advances the frame index by one for animated overlays.
*/
public void advanceTileFrame()
{
for (int i = 1, size = listTilesets.size(); i < size; i++) {
listTilesets.get(i).advanceTileFrame();
hasChangedOverlays = true;
}
if (hasChangedOverlays) {
updateDisplay();
}
}
/**
* Sets the frame index for animated overlay tiles.
* @param index The frame index to set.
*/
public void setTileFrame(int index)
{
for (int i = 1, size = listTilesets.size(); i < size; i++) {
listTilesets.get(i).setTileFrame(index);
hasChangedOverlays = true;
}
if (hasChangedOverlays) {
updateDisplay();
}
}
/**
* Returns the type of the current mini map.
* @return One of the MAP_XXX constants.
*/
public int getMiniMapType()
{
return miniMapType;
}
/**
* Returns the BmpResource instance of the current mini map.
* @return BmpResource instance of the current mini map, or {@code null} if not available.
*/
public GraphicsResource getMiniMap()
{
return miniMap;
}
/**
* Specify a new mini map to be overlayed.
* @param mapType The type of the mini map.
* @param bmp The mini map resource.
*/
public void setMiniMap(int mapType, GraphicsResource bmp)
{
if (mapType != miniMapType || bmp != miniMap) {
switch (mapType) {
case ViewerConstants.MAP_SEARCH:
case ViewerConstants.MAP_HEIGHT:
case ViewerConstants.MAP_LIGHT:
miniMap = (bmp.getImage() != null) ? bmp : null;
miniMapType = (miniMap != null) ? mapType : ViewerConstants.MAP_NONE;
break;
default:
miniMap = null;
miniMapType = ViewerConstants.MAP_NONE;
}
hasChangedAppearance = true;
updateDisplay();
}
}
/**
* Returns the currently set transparency for overlayed mini maps.
* @return The alpha transparency of mini maps. Range: [0..255]
*/
public int getMiniMapTransparency()
{
return miniMapAlpha;
}
/**
* Specify the alpha transparency for overlayed mini maps.
* @param alpha Alpha transparency in range [0..255] for overlayed mini maps.
*/
public void setMiniMapTransparency(int alpha)
{
alpha = Math.min(Math.max(alpha, 0), 255);
if (miniMapAlpha != alpha) {
miniMapAlpha = alpha;
hasChangedAppearance = true;
updateDisplay();
}
}
/**
* Redraw all tiles of the current map if needed.
* @param force If {@code true}, the map will be redrawn regardless of the current map state.
*/
public void reload(boolean force)
{
boolean b = false;
if (getImage() != null && getImage() instanceof VolatileImage) {
b = ((VolatileImage)getImage()).contentsLost();
}
updateDisplay(b || force);
}
@Override
public void paint(Graphics g)
{
// checking whether VolatileImage instance needs to be updated
if (getImage() != null && getImage() instanceof VolatileImage) {
VolatileImage image = (VolatileImage)getImage();
int valCode;
do {
valCode = image.validate(getGraphicsConfiguration());
if (valCode == VolatileImage.IMAGE_INCOMPATIBLE) {
// recreate the image object
int w = image.getWidth();
int h = image.getHeight();
image = createVolatileImage(w, h);
setImage(image);
}
if (valCode != VolatileImage.IMAGE_OK) {
updateDisplay(true);
}
} while (image.contentsLost());
}
super.paint(g);
}
protected void updateSize()
{
if (isInitialized()) {
int w = getMapWidth(true);
int h = getMapHeight(true);
Dimension newDim = new Dimension(w, h);
setScalingEnabled(zoomFactor != 1.0);
if (!forcedInterpolation) {
setInterpolationType((zoomFactor < 1.0) ? TYPE_BILINEAR : TYPE_NEAREST_NEIGHBOR);
}
setSize(newDim);
setPreferredSize(newDim);
setMinimumSize(newDim);
} else {
super.updateSize();
}
}
@Override
protected void paintCanvas(Graphics g)
{
super.paintCanvas(g);
if (showGrid) {
double tileWidth = 64.0 * zoomFactor;
double tileHeight = 64.0 * zoomFactor;
double mapWidth = (double)getMapWidth(true);
double mapHeight = (double)getMapHeight(true);
g.setColor(Color.GRAY);
for (double curY = 0.0; curY < mapHeight; curY += tileHeight) {
for (double curX = 0.0; curX < mapWidth; curX += tileWidth) {
g.drawLine((int)Math.ceil(curX), (int)Math.ceil(curY + tileHeight),
(int)Math.ceil(curX + tileWidth), (int)Math.ceil(curY + tileHeight));
g.drawLine((int)Math.ceil(curX + tileWidth), (int)Math.ceil(curY),
(int)Math.ceil(curX + tileWidth), (int)Math.ceil(curY + tileHeight));
}
}
}
}
// Resizes the current image or creates a new one if needed
private boolean updateImageSize()
{
if (isInitialized()) {
if (getImage() == null || getImage().getWidth(null) != getMapWidth(false) ||
getImage().getHeight(null) != getMapHeight(false)) {
setImage(ColorConvert.createVolatileImage(getMapWidth(false), getMapHeight(false), false));
}
updateSize();
return true;
}
return false;
}
// Initializes a new map
private boolean init(WedResource wed)
{
release(false);
// resetting states
blendedOverlays = Profile.getProperty(Profile.Key.IS_TILESET_STENCILED);
lighting = ViewerConstants.LIGHTING_DAY;
// loading map data
if (wed != null) {
if (initWed(wed)) {
this.wed = wed;
if (!updateImageSize()) {
return false;
}
} else {
return false;
}
}
hasChangedMap = true;
// drawing map data
updateDisplay();
return true;
}
// Removes all map-related data from memory
private void release(boolean forceUpdate)
{
if (isInitialized()) {
wed = null;
listTilesets.clear();
listDoorTileIndices.clear();
Image img = getImage();
if (img != null) {
if (forceUpdate) {
Graphics2D g = (Graphics2D)img.getGraphics();
g.setBackground(new Color(0, true));
g.clearRect(0, 0, img.getWidth(null), img.getHeight(null));
g.dispose();
repaint();
}
}
hasChangedMap = false;
hasChangedAppearance = false;
hasChangedOverlays = false;
hasChangedDoorState = false;
}
}
// Simply returns whether a map has been loaded
private boolean isInitialized()
{
return (wed != null) && (!listTilesets.isEmpty());
}
private boolean initWed(WedResource wed)
{
if (wed != null) {
// loading overlay structures
SectionOffset so = (SectionOffset)wed.getAttribute(WedResource.WED_OFFSET_OVERLAYS);
SectionCount sc = (SectionCount)wed.getAttribute(WedResource.WED_NUM_OVERLAYS);
if (so != null && sc != null) {
for (int i = 0, count = sc.getValue(), curOfs = so.getValue(); i < count; i++) {
Overlay ovl = (Overlay)wed.getAttribute(curOfs, false);
if (ovl != null) {
listTilesets.add(new Tileset(wed, ovl));
curOfs += ovl.getSize();
} else {
release(true);
return false;
}
}
} else {
release(true);
return false;
}
// loading door structures
so = (SectionOffset)wed.getAttribute(WedResource.WED_OFFSET_DOORS);
sc = (SectionCount)wed.getAttribute(WedResource.WED_NUM_DOORS);
HexNumber lookupOfs = (HexNumber)wed.getAttribute(WedResource.WED_OFFSET_DOOR_TILEMAP_LOOKUP);
if (so != null && sc != null && lookupOfs != null) {
for (int i = 0, count = sc.getValue(), curOfs = so.getValue(); i < count; i++) {
Door door = (Door)wed.getAttribute(curOfs, false);
if (door != null) {
String name = ((TextString)door.getAttribute(Door.WED_DOOR_NAME)).toString();
boolean isClosed = ((Bitmap)door.getAttribute(Door.WED_DOOR_IS_DOOR)).getValue() == 1;
final int tileSize = 2;
int tileIdx = ((DecNumber)door.getAttribute(Door.WED_DOOR_TILEMAP_LOOKUP_INDEX)).getValue();
int tileCount = ((SectionCount)door.getAttribute(Door.WED_DOOR_NUM_TILEMAP_INDICES)).getValue();
if (tileCount < 0) tileCount = 0;
int[] indices = new int[tileCount];
for (int j = 0; j < tileCount; j++) {
indices[j] = ((DecNumber)door.getAttribute(lookupOfs.getValue() + (tileIdx+j)*tileSize, false)).getValue();
}
listDoorTileIndices.add(new DoorInfo(name, isClosed, indices));
curOfs += door.getSize();
} else {
listDoorTileIndices.add(new DoorInfo("", true, new int[]{})); // needed as placeholder
}
}
} else {
release(true);
return false;
}
return true;
} else {
return false;
}
}
// For compatibility reasons only
private void updateDisplay()
{
updateDisplay(false);
}
// (Re-)draw display, resize if needed
private void updateDisplay(boolean forced)
{
if (isInitialized()) {
updateImageSize();
// VolatileImage objects may lose their content under specific circumstances
if (!forced && getImage() != null && getImage() instanceof VolatileImage) {
forced |= ((VolatileImage)getImage()).contentsLost();
}
if (hasChangedMap || hasChangedAppearance || forced) {
// redraw each tile
drawAllTiles();
} else {
if (hasChangedOverlays) {
// redraw overlayed tiles only
drawOverlayTiles();
}
if (hasChangedDoorState) {
// redraw door tiles only
drawDoorTiles();
}
}
repaint();
notifyChangeListeners();
hasChangedMap = false;
hasChangedAppearance = false;
hasChangedOverlays = false;
hasChangedDoorState = false;
}
}
// Returns if the specified overlay index points to valid overlay data
private boolean hasOverlay(int ovlIdx)
{
if (ovlIdx > 0 && ovlIdx < listTilesets.size()) {
return !listTilesets.get(ovlIdx).listTiles.isEmpty();
}
return false;
}
// draws all tiles of the map
private void drawAllTiles()
{
Tileset ts = listTilesets.get(0);
for (int i = 0, size = ts.listTiles.size(); i < size; i++) {
Tile tile = ts.listTiles.get(i);
drawTile(tile, isDoorTile(tile));
}
}
// draws overlayed tiles only
private void drawOverlayTiles()
{
Tileset ts = listTilesets.get(0);
for (int i = 0, size = ts.listOverlayTiles.size(); i < size; i++) {
Tile tile = ts.listOverlayTiles.get(i);
drawTile(tile, isDoorTile(tile));
}
}
// draws door tiles only
private void drawDoorTiles()
{
for (int i = 0, size = listDoorTileIndices.size(); i < size; i++) {
DoorInfo di = listDoorTileIndices.get(i);
for (int j = 0, iCount = di.getIndicesCount(); j < iCount; j++) {
Tile tile = listTilesets.get(0).listTiles.get(di.getIndex(j));
drawTile(tile, isDoorTile(tile));
}
}
}
// draws the specified tile into the target graphics buffer
private synchronized void drawTile(Tile tile, boolean isDoorTile)
{
if (tile != null) {
boolean isDoorClosed = (Profile.getEngine() == Profile.Engine.PST) ? !isClosed : isClosed;
int[] target = ((DataBufferInt)workingTile.getRaster().getDataBuffer()).getData();
int fa = 255, fr = 0, fg = 0, fb = 0;
if (overlaysEnabled && tile.hasOverlay() && hasOverlay(tile.getOverlayIndex())) { // overlayed tile
// preparing graphics data
int overlay = tile.getOverlayIndex();
if (overlay < listTilesets.size() && !listTilesets.get(overlay).listTiles.isEmpty()) {
int tileIdx = listTilesets.get(overlay).listTiles.get(0).getPrimaryIndex();
int[] srcOvl = null;
if (tileIdx >= 0) {
srcOvl = listTilesets.get(overlay).listTileData.get(tileIdx);
}
int[] srcPri = null;
tileIdx = tile.getPrimaryIndex();
if (tileIdx >= 0) {
srcPri = listTilesets.get(0).listTileData.get(tileIdx);
}
int[] srcSec = null;
tileIdx = tile.getSecondaryIndex();
if (tileIdx >= 0) {
srcSec = listTilesets.get(0).listTileData.get(tileIdx);
}
// determining correct rendering mode
boolean blended = (renderingMode == MODE_AUTO && blendedOverlays) || (renderingMode == MODE_BG2);
// drawing tile graphics
boolean pa, sa;
int pr, pg, pb, sr, sg, sb, or, og, ob;
for (int ofs = 0; ofs < 4096; ofs++) {
if (blended) { // BG2/BGEE mode overlays
// extracting color components
if (srcPri != null) {
pa = (srcPri[ofs] & 0xff000000) != 0;
pr = (srcPri[ofs] >>> 16) & 0xff;
pg = (srcPri[ofs] >>> 8) & 0xff;
pb = srcPri[ofs] & 0xff;
} else {
pa = false;
pr = pg = pb = 0;
}
if (srcSec != null) {
sa = (srcSec[ofs] & 0xff000000) != 0;
sr = (srcSec[ofs] >>> 16) & 0xff;
sg = (srcSec[ofs] >>> 8) & 0xff;
sb = srcSec[ofs] & 0xff;
} else {
sa = false;
sr = sg = sb = 0;
}
if (srcOvl != null) {
or = (srcOvl[ofs] >>> 16) & 0xff;
og = (srcOvl[ofs] >>> 8) & 0xff;
ob = srcOvl[ofs] & 0xff;
} else {
or = og = ob = 0;
}
// blending modes depend on transparency states of primary and secondary pixels
if (pa && !sa) {
if (tile.isTisV1()) {
fr = (pr + or) >>> 1;
fg = (pg + og) >>> 1;
fb = (pb + ob) >>> 1;
} else {
if (srcSec != null) {
fr = pr;
fg = pg;
fb = pb;
} else {
fr = (pr + or) >>> 1;
fg = (pg + og) >>> 1;
fb = (pb + ob) >>> 1;
}
}
} else if (pa && sa) {
fr = (pr + sr) >>> 1;
fg = (pg + sg) >>> 1;
fb = (pb + sb) >>> 1;
} else if (!pa && !sa) {
fr = or;
fg = og;
fb = ob;
} else if (!pa && sa) {
fr = (sr + or) >>> 1;
fg = (sg + og) >>> 1;
fb = (sb + ob) >>> 1;
}
} else { // BG1 mode overlays
int[] src = (isDoorTile && isDoorClosed) ? srcSec : srcPri;
if (src != null) {
if ((src[ofs] & 0xff000000) != 0 && src != null) {
fr = (src[ofs] >>> 16) & 0xff;
fg = (src[ofs] >>> 8) & 0xff;
fb = src[ofs] & 0xff;
} else if (srcOvl != null) {
fr = (srcOvl[ofs] >>> 16) & 0xff;
fg = (srcOvl[ofs] >>> 8) & 0xff;
fb = srcOvl[ofs] & 0xff;
}
} else {
fa = fr = fg = fb = 0;
}
}
// applying lighting conditions
fr = (fr * LightingAdjustment[lighting][0]) >>> LightingAdjustmentShift;
fg = (fg * LightingAdjustment[lighting][1]) >>> LightingAdjustmentShift;
fb = (fb * LightingAdjustment[lighting][2]) >>> LightingAdjustmentShift;
target[ofs] = (fa << 24) | (fr << 16) | (fg << 8) | fb;
}
srcOvl = null;
srcPri = null;
srcSec = null;
}
} else { // no overlay or disabled overlay
// preparing tile graphics
int[] srcTile = null;
int tileIdx = (!isDoorClosed || !isDoorTile) ? tile.getPrimaryIndex() : tile.getSecondaryIndex();
if (tileIdx < 0) { tileIdx = tile.getPrimaryIndex(); } // XXX: hackish work-around for faulty tile definitions
if (tileIdx >= 0) {
srcTile = listTilesets.get(0).listTileData.get(tileIdx);
}
// drawing tile graphics
if (srcTile != null) {
for (int ofs = 0; ofs < 4096; ofs++) {
fr = (srcTile[ofs] >>> 16) & 0xff;
fg = (srcTile[ofs] >>> 8) & 0xff;
fb = srcTile[ofs] & 0xff;
fr = (fr * LightingAdjustment[lighting][0]) >>> LightingAdjustmentShift;
fg = (fg * LightingAdjustment[lighting][1]) >>> LightingAdjustmentShift;
fb = (fb * LightingAdjustment[lighting][2]) >>> LightingAdjustmentShift;
target[ofs] = 0xff000000 | (fr << 16) | (fg << 8) | fb;
}
} else {
// no tile = transparent pixel data (work-around for faulty tiles in BG1's WEDs)
for (int ofs = 0; ofs < 4096; ofs++) {
target[ofs] = 0;
}
}
srcTile = null;
}
// drawing mini map if available
if (miniMap != null && miniMapType != -1) {
BufferedImage miniMapImage = miniMap.getImage();
int miniMapWidth = miniMapImage.getWidth();
int miniMapHeight = miniMapImage.getHeight();
int[] map = ((DataBufferInt)miniMapImage.getRaster().getDataBuffer()).getData();
double scaleX = (double)miniMapWidth / (double)getMapWidth(false);
double scaleY = (double)miniMapHeight / (double)getMapHeight(false);
double curX = (double)tile.getX() * scaleX;
double nextX = Math.floor(curX) + 1.0;
double curY = (double)tile.getY() * scaleY;
double nextY = Math.floor(curY) + 1.0;
int startPixelX = (int)Math.floor(curX);
int curPixelX = startPixelX;
int curPixelY = (int)Math.floor(curY);
int srcAlpha = miniMapAlpha;
int dstAlpha = 256 - srcAlpha;
int dstOfs = 0;
for (int y = 0; y < 64; y++) {
curPixelX = startPixelX;
int srcOfs = curPixelY*miniMapWidth + curPixelX;
for (int x = 0; x < 64; x++) {
// blending pixels
int sr = (((map[srcOfs] >>> 16) & 0xff) * srcAlpha) >>> 8;
int sg = (((map[srcOfs] >>> 8) & 0xff) * srcAlpha) >>> 8;
int sb = ((map[srcOfs] & 0xff) * srcAlpha) >>> 8;
int dr = (((target[dstOfs] >>> 16) & 0xff) * dstAlpha) >>> 8;
int dg = (((target[dstOfs] >>> 8) & 0xff) * dstAlpha) >>> 8;
int db = ((target[dstOfs] & 0xff) * dstAlpha) >>> 8;
int color = ((sr + dr) << 16) | ((sg + dg) << 8) | (sb + db);
target[dstOfs] = 0xff000000 | color;
curX += scaleX;
if (curX >= nextX) {
nextX += 1.0;
curPixelX++;
srcOfs++;
}
dstOfs++;
}
curY += scaleY;
if (curY >= nextY) {
nextY += 1.0;
curPixelY++;
}
}
}
// drawing tile on canvas
if (getImage() != null) {
Graphics2D g = (Graphics2D)getImage().getGraphics();
g.drawImage(workingTile, tile.getX(), tile.getY(), null);
g.dispose();
}
target = null;
}
}
// Returns whether the specified tile is used as a door tile
private boolean isDoorTile(Tile tile)
{
if (tile != null) {
int tileIdx = tile.getPrimaryIndex();
for (int i = 0, size = listDoorTileIndices.size(); i < size; i++) {
DoorInfo di = listDoorTileIndices.get(i);
for (int j = 0, iCount = di.getIndicesCount(); j < iCount; j++) {
try {
int idx = listTilesets.get(0).listTiles.get(di.getIndex(j)).getPrimaryIndex();
if (idx == tileIdx) {
return true;
}
} catch (Exception e) {
// ignore invalid tile indices
}
}
}
}
return false;
}
// Notify all registered change listeners
private void notifyChangeListeners()
{
if (hasChangedMap || hasChangedAppearance || hasChangedOverlays || hasChangedDoorState) {
for (int i = 0, size = listChangeListener.size(); i < size; i++) {
listChangeListener.get(i).tilesetChanged(new TilesetChangeEvent(this,
hasChangedMap, hasChangedAppearance, hasChangedOverlays, hasChangedDoorState));
}
}
}
//----------------------------- INNER CLASSES -----------------------------
// Stores data of a specific overlay structure
private static class Tileset
{
// graphics data for all tiles of this overlay (as int arrays of 64*64 pixels)
public final List<int[]> listTileData = new ArrayList<int[]>();
// info structures for all tiles of this overlay
public final List<Tile> listTiles = new ArrayList<Tile>();
// lists references to all tiles containing overlays from listTiles
public final List<Tile> listOverlayTiles = new ArrayList<Tile>();
public int tilesX, tilesY; // stores number of tiles per row/column
public Tileset(WedResource wed, Overlay ovl)
{
init(wed, ovl);
}
public void advanceTileFrame()
{
for (int i = 0, size = listTiles.size(); i < size; i++) {
listTiles.get(i).advancePrimaryIndex();
}
}
public void setTileFrame(int index)
{
for (int i = 0, size = listTiles.size(); i < size; i++) {
listTiles.get(i).setCurrentPrimaryIndex(index);
}
}
private void init(WedResource wed, Overlay ovl)
{
if (wed != null && ovl != null) {
// storing tile data
boolean isTilesetV1 = true;
ResourceEntry tisEntry = getTisResource(wed, ovl);
if (tisEntry != null) {
try {
TisDecoder decoder = TisDecoder.loadTis(tisEntry);
isTilesetV1 = decoder.getType() == TisDecoder.Type.PALETTE;
BufferedImage tileImage = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB);
for (int i = 0, tCount = decoder.getTileCount(); i < tCount; i++) {
decoder.getTile(i, tileImage);
int[] srcData = ((DataBufferInt)tileImage.getRaster().getDataBuffer()).getData();
int[] dstData = new int[64*64];
System.arraycopy(srcData, 0, dstData, 0, 64*64);
listTileData.add(dstData);
}
tileImage.flush();
tileImage = null;
decoder.close();
decoder = null;
} catch (Exception e) {
e.printStackTrace();
return;
}
}
// storing tile information
tilesX = ((DecNumber)ovl.getAttribute(Overlay.WED_OVERLAY_WIDTH)).getValue();
tilesY = ((DecNumber)ovl.getAttribute(Overlay.WED_OVERLAY_HEIGHT)).getValue();
int mapOfs = ((SectionOffset)ovl.getAttribute(Overlay.WED_OVERLAY_OFFSET_TILEMAP)).getValue();
int idxOfs = ((DecNumber)ovl.getAttribute(Overlay.WED_OVERLAY_OFFSET_TILEMAP_LOOKUP)).getValue();
int tileCount = tilesX * tilesY;
for (int i = 0, curOfs = mapOfs; i < tileCount; i++) {
Tilemap tile = (Tilemap)ovl.getAttribute(curOfs, false);
// tile coordinates in pixels
int x = (i % tilesX) * 64;
int y = (i / tilesX) * 64;
if (tile != null) {
// initializing list of primary tile indices
final int idxSize = 2;
int index = ((DecNumber)tile.getAttribute(Tilemap.WED_TILEMAP_TILE_INDEX_PRI)).getValue();
int count = ((DecNumber)tile.getAttribute(Tilemap.WED_TILEMAP_TILE_COUNT_PRI)).getValue();
if (count < 0) count = 0;
int[] tileIdx = new int[count];
for (int j = 0; j < count; j++) {
if (index >= 0) {
DecNumber dn = (DecNumber)ovl.getAttribute(idxOfs + (index+j)*idxSize, false);
if (dn != null) {
tileIdx[j] = dn.getValue();
} else {
tileIdx[j] = -1;
}
} else {
tileIdx[j] = -1;
}
}
// initializing secondary tile index
int tileIdx2 = ((DecNumber)tile.getAttribute(Tilemap.WED_TILEMAP_TILE_INDEX_SEC)).getValue();
// initializing overlay flags
Flag drawOverlays = (Flag)tile.getAttribute(Tilemap.WED_TILEMAP_DRAW_OVERLAYS);
int flags = (int)drawOverlays.getValue() & 255;
listTiles.add(new Tile(x, y, count, tileIdx, tileIdx2, flags, isTilesetV1));
curOfs += tile.getSize();
} else {
listTiles.add(new Tile(x, y, 0, new int[]{}, -1, 0, true)); // needed as placeholder
}
}
// grouping overlayed tiles for faster access
for (int i = 0, size = listTiles.size(); i < size; i++) {
Tile tile = listTiles.get(i);
if (tile.getFlags() > 0) {
listOverlayTiles.add(tile);
}
}
} else {
tilesX = tilesY = 0;
}
}
// Returns the TIS file defined in the specified Overlay structure
private ResourceEntry getTisResource(WedResource wed, Overlay ovl)
{
ResourceEntry entry = null;
if (wed != null && ovl != null) {
String tisName = ((ResourceRef)ovl.getAttribute(Overlay.WED_OVERLAY_TILESET)).getResourceName().toUpperCase(Locale.ENGLISH);
if (tisName == null || "None".equalsIgnoreCase(tisName)) {
tisName = "";
}
if (!tisName.isEmpty()) {
// Special: BG1 has a weird way to select extended night tilesets
if (Profile.getEngine() == Profile.Engine.BG1) {
String wedName = wed.getResourceEntry().getResourceName().toUpperCase(Locale.ENGLISH);
if (wedName.lastIndexOf('.') > 0) {
wedName = wedName.substring(0, wedName.lastIndexOf('.'));
}
if (tisName.lastIndexOf('.') > 0) {
tisName = tisName.substring(0, tisName.lastIndexOf('.'));
}
// XXX: not sure whether this check is correct
if (wedName.length() > 6 && wedName.charAt(6) == 'N' && tisName.length() == 6) {
entry = ResourceFactory.getResourceEntry(tisName + "N.TIS");
}
if (entry == null) {
entry = ResourceFactory.getResourceEntry(tisName + ".TIS");
}
} else {
entry = ResourceFactory.getResourceEntry(tisName);
}
}
}
return entry;
}
}
// Stores tilemap information only (no graphics data)
private static class Tile
{
private int tileIdx2; // (start) indices of primary and secondary tiles
private int[] tileIdx; // tile indices for primary and secondary tiles
private int tileCount, curTile; // number of primary tiles, currently selected tile
private int x, y, flags; // (x, y) as pixel coordinates, flags defines overlay usage
private boolean isTisV1;
public Tile(int x, int y, int tileCount, int[] index, int index2, int flags, boolean isTisV1)
{
if (tileCount < 0) tileCount = 0;
this.x = x;
this.y = y;
this.tileCount = tileCount;
this.curTile = 0;
this.tileIdx = index;
this.tileIdx2 = index2;
this.flags = flags;
this.isTisV1 = isTisV1;
}
// Returns whether this tile references a TIS v1 resource
public boolean isTisV1()
{
return isTisV1;
}
// Returns the current primary tile index
public int getPrimaryIndex()
{
if (tileIdx.length > 0) {
return tileIdx[curTile];
} else {
return -1;
}
}
// // Returns the primary tile index of the specified frame (useful for animated tiles)
// public int getPrimaryIndex(int frame)
// {
// if (tileCount > 0) {
// return tileIdx[frame % tileCount];
// } else {
// return -1;
// }
// }
// Sets a new selected primary tile index
public void setCurrentPrimaryIndex(int frame)
{
if (tileCount > 0) {
if (frame < 0) frame = 0; else if (frame >= tileCount) frame = tileCount - 1;
curTile = frame;
}
}
// // Returns the primary tile count
// public int getPrimaryIndexCount()
// {
// return tileCount;
// }
// Advances the primary tile index by 1 for animated tiles, wraps around automatically
public void advancePrimaryIndex()
{
if (tileCount > 0) {
curTile = (curTile + 1) % tileCount;
}
}
// Returns the secondary tile index (or -1 if not available)
public int getSecondaryIndex()
{
return tileIdx2;
}
// Returns the x pixel coordinate of this tile
public int getX()
{
return x;
}
// Returns y pixel coordinate of this tile
public int getY()
{
return y;
}
// Returns the unprocessed flags data
public int getFlags()
{
return flags;
}
// Returns true if overlays have been defined
public boolean hasOverlay()
{
return flags != 0;
}
// Returns the overlay index (or 0 otherwise)
public int getOverlayIndex()
{
if (flags != 0) {
for (int i = 1; i < 8; i++) {
if ((flags & (1 << i)) != 0) {
return i;
}
}
}
return 0;
}
}
// Stores relevant information about door structures
private static class DoorInfo
{
// private String name; // door info structure name
// private boolean isClosed; // indicates the door state for the specified list of tile indices
private int[] indices; // list of tilemap indices used for the door
public DoorInfo(String name, boolean isClosed, int[] indices)
{
// this.name = (name != null) ? name : "";
// this.isClosed = isClosed;
this.indices = (indices != null) ? indices : new int[0];
}
// // Returns the name of the door structure
// public String getName()
// {
// return name;
// }
// // Returns whether the tile indices are used for the closed state of the door
// public boolean isClosed()
// {
// return isClosed;
// }
// Returns number of tiles used in this door structure
public int getIndicesCount()
{
return indices.length;
}
// Returns tilemap index of specified entry
public int getIndex(int entry)
{
if (entry >= 0 && entry < indices.length) {
return indices[entry];
} else {
return -1;
}
}
}
}