From c667cde24de1b3cdbbf36575c88f8d761fb7c78a Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Mon, 20 Mar 2023 16:32:30 +0100 Subject: [PATCH 01/22] Implement stream versions of zlib compression and decompression methods --- .../resource/graphics/Compressor.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/org/infinity/resource/graphics/Compressor.java b/src/org/infinity/resource/graphics/Compressor.java index 7321ae69d..e4c072c28 100644 --- a/src/org/infinity/resource/graphics/Compressor.java +++ b/src/org/infinity/resource/graphics/Compressor.java @@ -5,11 +5,15 @@ package org.infinity.resource.graphics; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.zip.DataFormatException; import java.util.zip.Deflater; +import java.util.zip.DeflaterInputStream; import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; import org.infinity.util.ArrayUtil; import org.infinity.util.DynamicArray; @@ -95,10 +99,56 @@ public static byte[] compress(byte[] data, int ofs, int len, boolean prependSize return result; } + /** + * Reads {@code len} bytes of raw data from the {@code InputStream} and writes the compressed data to the + * {@code OutputStream}. + * + * @param is {@code InputStream} with data to compress. + * @param os {@code OutputStream} to write compressed data to. + * @param len Length of data to compress, in bytes. Specify {@code 0} to compress until no more input data is available. + * @param prependSize If {@code true} then uncompressed size value will be written to the output stream + * before compressed data is written. Ignored if {@code len} is 0. + * @return + * @throws IOException if an I/O error occurs. + */ + public static int compress(InputStream is, OutputStream os, int len, boolean prependSize) throws IOException { + int result = 0; + if (is != null && os != null) { + if (len > 0 && prependSize) { + StreamUtils.writeInt(os, len); + result += 4; + } + + byte[] buffer = new byte[Math.min(65536, len)]; + final DeflaterInputStream dis = new DeflaterInputStream(is); + int bufLen = 0; + while ((bufLen = dis.read(buffer)) > 0) { + os.write(buffer, 0, bufLen); + result += bufLen; + } + } + return result; + } + + /** + * Decompresses the data of the specified ByteBuffer object and returns it as a new ByteBuffer object. + * + * @param buffer {@code ByteBuffer} with data to decompress. + * @return A {@code ByteBuffer} object with decompressed data. + * @throws IOException if an I/O error occurs. + */ public static ByteBuffer decompress(ByteBuffer buffer) throws IOException { return decompress(buffer, 8); } + /** + * Decompresses the data of the specified ByteBuffer object and returns it as a new ByteBuffer object. + * + * @param buffer {@code ByteBuffer} with data to decompress. + * @param offset Start offset of compressed data. + * @return A {@code ByteBuffer} object with decompressed data. + * @throws IOException if an I/O error occurs. + */ public static ByteBuffer decompress(ByteBuffer buffer, int offset) throws IOException { ByteBuffer retVal = null; if (buffer != null && offset < buffer.limit()) { @@ -109,10 +159,26 @@ public static ByteBuffer decompress(ByteBuffer buffer, int offset) throws IOExce return retVal; } + /** + * Decompresses the data of the specified byte array and returns it as a new byte array object. + * + * @param buffer Byte array with data to decompress. + * @param offset Start offset of compressed data. + * @return A byte array with decompressed data. + * @throws IOException + */ public static byte[] decompress(byte buffer[]) throws IOException { return decompress(buffer, 8); } + /** + * Decompresses the data of the specified byte array and returns it as a new byte array object. + * + * @param buffer Byte array with data to decompress. + * @param offset Start offset of compressed data. + * @return A byte array with decompressed data. + * @throws IOException if an I/O error occurs. + */ public static byte[] decompress(byte buffer[], int ofs) throws IOException { Inflater inflater = new Inflater(); byte result[] = new byte[DynamicArray.getInt(buffer, ofs)]; @@ -127,6 +193,43 @@ public static byte[] decompress(byte buffer[], int ofs) throws IOException { return result; } + /** + * Decompresses the data from the specified {@code InputStream} and returns it as a new byte array object. + * + * @param is {@code InputStream} with data to decompress. + * The current stream position should point to the start of the game resource. + * @return A byte array with decompressed data. + * @throws IOException + */ + public static byte[] decompress(InputStream is) throws IOException { + return decompress(is, 8, 0); + } + + /** + * Decompresses the data from the specified {@code InputStream} and returns it as a new byte array object. + * + * @param is {@code InputStream} with data to decompress. + * The current stream position should point to the start of the game resource. + * @param ofs Specifies the number of bytes to skip before reading compressed data from the stream. + * @param size Amount of bytes to return from the uncompressed data. Specify {@code 0} to return all decompressed data. + * @return A byte array with decompressed data. + * @throws IOException if an I/O error occurs. + */ + public static byte[] decompress(InputStream is, int ofs, int size) throws IOException { + is.skip(ofs); + if (size <= 0) { + size = StreamUtils.readInt(is); + } else { + is.skip(4); + } + byte[] result = new byte[size]; + + final InflaterInputStream inflater = new InflaterInputStream(is); + inflater.read(result); + + return result; + } + private Compressor() { } } From b432e07068f9902385ecb4ebeca47ed84f18ce86 Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Mon, 20 Mar 2023 17:42:39 +0100 Subject: [PATCH 02/22] Add methods for quickly getting MOS/TIS header information --- .../resource/graphics/MosDecoder.java | 63 +++++++++++++++++++ .../resource/graphics/MosV1Decoder.java | 42 +++++++++++++ .../resource/graphics/MosV2Decoder.java | 33 ++++++++++ .../resource/graphics/TisDecoder.java | 55 ++++++++++++++++ 4 files changed, 193 insertions(+) diff --git a/src/org/infinity/resource/graphics/MosDecoder.java b/src/org/infinity/resource/graphics/MosDecoder.java index 4087b8f65..db6bd0b8a 100644 --- a/src/org/infinity/resource/graphics/MosDecoder.java +++ b/src/org/infinity/resource/graphics/MosDecoder.java @@ -59,6 +59,31 @@ public static Type getType(ResourceEntry mosEntry) { return retVal; } + /** + * Returns information about the specified MOS resource. + * + * @param mosEntry The MOS resource entry. + * @return A {@link MosInfo} structure with information about the specified MOS resource, + * {@code null} if information is not available. + */ + public static MosInfo getInfo(ResourceEntry mosEntry) { + MosInfo retVal = null; + + switch (getType(mosEntry)) { + case MOSC: + case MOSV1: + retVal = MosV1Decoder.getInfo(mosEntry); + break; + case MOSV2: + retVal = MosV2Decoder.getInfo(mosEntry); + break; + default: + break; + } + + return retVal; + } + /** * Returns a new MosDecoder object based on the specified MOS resource entry. * @@ -150,4 +175,42 @@ protected MosDecoder(ResourceEntry mosEntry) { protected void setType(Type type) { this.type = type; } + + // -------------------------- INNER CLASSES -------------------------- + + /** A class for providing parsed MOS header information. */ + public static class MosInfo { + /** Type of the MOS resource. */ + public final Type type; + /** MOS width, in pixels. */ + public final int width; + /** MOS height, in pixels. */ + public final int height; + /** Number of MOS V1 data block columns. */ + public final int columns; + /** Number of MOS V1 data block rows. */ + public final int rows; + /** Dimension of a MOS V1 data block, in pixels. */ + public final int blockSize; + /** Number of MOS V2 data blocks. */ + public final int numBlocks; + + public MosInfo(boolean compressed, int width, int height, int columns, int rows, int blockSize) { + this.type = compressed ? Type.MOSC : Type.MOSV1; + this.width = width; + this.height = height; + this.columns = columns; + this.rows = rows; + this.blockSize= blockSize; + this.numBlocks = 0; + } + + public MosInfo(int width, int height, int numBlocks) { + this.type = Type.MOSV2; + this.width = width; + this.height = height; + this.numBlocks = numBlocks; + this.columns = this.rows = this.blockSize = 0; + } + } } diff --git a/src/org/infinity/resource/graphics/MosV1Decoder.java b/src/org/infinity/resource/graphics/MosV1Decoder.java index 57b0e4c3d..d8a049aea 100644 --- a/src/org/infinity/resource/graphics/MosV1Decoder.java +++ b/src/org/infinity/resource/graphics/MosV1Decoder.java @@ -9,10 +9,12 @@ import java.awt.Image; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; +import java.io.InputStream; import java.nio.ByteBuffer; import java.util.Arrays; import org.infinity.resource.key.ResourceEntry; +import org.infinity.util.DynamicArray; import org.infinity.util.io.StreamUtils; public class MosV1Decoder extends MosDecoder { @@ -32,6 +34,46 @@ public class MosV1Decoder extends MosDecoder { private BufferedImage workingCanvas; // storage space big enough for a single block private int lastBlockIndex; // hold the index of the last data block drawn onto workingCanvas + /** + * Returns information about the specified MOS resource. + * + * @param mosEntry The MOS resource entry. + * @return A {@link MosInfo} structure with information about the specified MOS resource, + * {@code null} if information is not available. + */ + public static MosDecoder.MosInfo getInfo(ResourceEntry mosEntry) { + MosDecoder.MosInfo retVal = null; + + try (InputStream is = mosEntry.getResourceDataAsStream()) { + String signature = StreamUtils.readString(is, 4); + String version = StreamUtils.readString(is, 4); + + byte[] header = null; + boolean compressed = false; + if ("MOSC".equals(signature)) { + compressed = true; + header = Compressor.decompress(is, 0, 0x18); + } else if ("MOS ".equals(signature) && "V1 ".equals(version)) { + header = new byte[0x18]; + DynamicArray.putString(header, 0, 8, "MOS V1 "); + is.read(header, 8, header.length - 8); + } + + if (header != null) { + int width = DynamicArray.getShort(header, 0x08); + int height = DynamicArray.getShort(header, 0x0a); + int columns = DynamicArray.getShort(header, 0x0c); + int rows = DynamicArray.getShort(header, 0x0e); + int blockSize = DynamicArray.getInt(header, 0x10); + retVal = new MosDecoder.MosInfo(compressed, width, height, columns, rows, blockSize); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return retVal; + } + public MosV1Decoder(ResourceEntry mosEntry) { super(mosEntry); init(); diff --git a/src/org/infinity/resource/graphics/MosV2Decoder.java b/src/org/infinity/resource/graphics/MosV2Decoder.java index 1603fd3d7..21c155a2d 100644 --- a/src/org/infinity/resource/graphics/MosV2Decoder.java +++ b/src/org/infinity/resource/graphics/MosV2Decoder.java @@ -11,6 +11,7 @@ import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; +import java.io.InputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -19,6 +20,7 @@ import org.infinity.resource.ResourceFactory; import org.infinity.resource.key.ResourceEntry; +import org.infinity.util.DynamicArray; import org.infinity.util.io.StreamUtils; public class MosV2Decoder extends MosDecoder { @@ -32,6 +34,37 @@ public class MosV2Decoder extends MosDecoder { private int blockCount; private int ofsData; + /** + * Returns information about the specified MOS resource. + * + * @param mosEntry The MOS resource entry. + * @return A {@link MosInfo} structure with information about the specified MOS resource, + * {@code null} if information is not available. + */ + public static MosDecoder.MosInfo getInfo(ResourceEntry mosEntry) { + MosDecoder.MosInfo retVal = null; + + try (InputStream is = mosEntry.getResourceDataAsStream()) { + String signature = StreamUtils.readString(is, 4); + String version = StreamUtils.readString(is, 4); + + if ("MOS ".equals(signature) && "V2 ".equals(version)) { + final byte[] header = new byte[0x18]; + DynamicArray.putString(header, 0, 8, "MOS V2 "); + is.read(header, 8, header.length - 8); + + int width = DynamicArray.getInt(header, 0x08); + int height = DynamicArray.getInt(header, 0x0c); + int numBlocks = DynamicArray.getInt(header, 0x10); + retVal = new MosDecoder.MosInfo(width, height, numBlocks); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return retVal; + } + public MosV2Decoder(ResourceEntry mosEntry) { super(mosEntry); init(); diff --git a/src/org/infinity/resource/graphics/TisDecoder.java b/src/org/infinity/resource/graphics/TisDecoder.java index 3e86c9413..8ec3c23bc 100644 --- a/src/org/infinity/resource/graphics/TisDecoder.java +++ b/src/org/infinity/resource/graphics/TisDecoder.java @@ -5,9 +5,11 @@ package org.infinity.resource.graphics; import java.awt.Image; +import java.io.InputStream; import java.nio.ByteBuffer; import org.infinity.resource.key.ResourceEntry; +import org.infinity.util.io.StreamUtils; /** * Common base class for handling TIS resources. @@ -58,6 +60,36 @@ public static Type getType(ResourceEntry tisEntry) { return retVal; } + /** + * Returns information about the specified TIS resource. + * + * @param tisEntry The TIS resource entry. + * @return A {@link TisInfo} structure with information about the specified TIS resource, + * {@code null} if information is not available. + */ + public static TisInfo getInfo(ResourceEntry tisEntry) { + TisInfo retVal = null; + + if (tisEntry != null) { + try (InputStream is = tisEntry.getResourceDataAsStream()) { + String signature = StreamUtils.readString(is, 4); + String version = StreamUtils.readString(is, 4); + + if ("TIS ".equals(signature) && "V1 ".equals(version)) { + int numTiles = StreamUtils.readInt(is); + int tileSize = StreamUtils.readInt(is); + is.skip(4); // tile data offset + int tileDim = StreamUtils.readInt(is); + retVal = new TisInfo(numTiles, tileSize, tileDim); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + return retVal; + } + /** * Returns a new TisDecoder object based on the type of the specified resource entry. * @@ -130,4 +162,27 @@ protected TisDecoder(ResourceEntry tisEntry) { protected void setType(Type type) { this.type = type; } + + // -------------------------- INNER CLASSES -------------------------- + + /** A class for providing parsed TIS header information. */ + public static class TisInfo { + /** Type of the TIS resource. */ + public final Type type; + /** Number of tiles stored in the TIS file. */ + public final int numTiles; + /** Dimension of a TIS tile, in pixels (always 64). */ + public final int tileDimension; + + public TisInfo(int numTiles, int tileSize, int tileDim) { + this.type = tileSize == 0x0c ? Type.PVRZ : Type.PALETTE; + this.numTiles = numTiles; + this.tileDimension = tileDim; + } + + /** Returns the tile size, in bytes, for the current TIS file. */ + public int getTileSize() { + return type == Type.PVRZ ? 0x0c : 0x1400; + } + } } From 546871cf99b876dcdbb69bf915f830726aab2125 Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Tue, 21 Mar 2023 12:22:07 +0100 Subject: [PATCH 03/22] Update PSTEE-specific opcodes Added definitions for opcodes 348, 349, 350, 351 and 377 --- .../infinity/resource/effects/Opcode348.java | 51 ++++++++++++++++++ .../infinity/resource/effects/Opcode349.java | 51 ++++++++++++++++++ .../infinity/resource/effects/Opcode350.java | 52 ++++++++++++++++++ .../infinity/resource/effects/Opcode351.java | 53 +++++++++++++++++++ .../infinity/resource/effects/Opcode377.java | 47 ++++++++++++++++ 5 files changed, 254 insertions(+) create mode 100644 src/org/infinity/resource/effects/Opcode348.java create mode 100644 src/org/infinity/resource/effects/Opcode349.java create mode 100644 src/org/infinity/resource/effects/Opcode350.java create mode 100644 src/org/infinity/resource/effects/Opcode351.java create mode 100644 src/org/infinity/resource/effects/Opcode377.java diff --git a/src/org/infinity/resource/effects/Opcode348.java b/src/org/infinity/resource/effects/Opcode348.java new file mode 100644 index 000000000..9bb0310ea --- /dev/null +++ b/src/org/infinity/resource/effects/Opcode348.java @@ -0,0 +1,51 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2023 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.effects; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.infinity.datatype.Datatype; +import org.infinity.datatype.DecNumber; +import org.infinity.resource.Profile; +import org.infinity.resource.StructEntry; + +/** + * Implemention of opcode 348. + */ +public class Opcode348 extends BaseOpcode { + private static final String EFFECT_BASE_AMOUNT = "Base amount"; + private static final String EFFECT_AMOUNT_PER_LEVEL = "Amount per level"; + + private static final String RES_TYPE = "BAM:VVC"; + + /** Returns the opcode name for the current game variant. */ + private static String getOpcodeName() { + switch (Profile.getEngine()) { + case EE: + if (Profile.getGame() == Profile.Game.PSTEE) { + return "Cloak of Warding"; + } + default: + return null; + } + } + + public Opcode348() { + super(348, getOpcodeName()); + } + + @Override + protected String makeEffectParamsEE(Datatype parent, ByteBuffer buffer, int offset, List list, + boolean isVersion1) { + if (Profile.getGame() == Profile.Game.PSTEE) { + list.add(new DecNumber(buffer, offset, 4, EFFECT_BASE_AMOUNT)); + list.add(new DecNumber(buffer, offset + 4, 4, EFFECT_AMOUNT_PER_LEVEL)); + return RES_TYPE; + } else { + return super.makeEffectParamsEE(parent, buffer, offset, list, isVersion1); + } + } +} diff --git a/src/org/infinity/resource/effects/Opcode349.java b/src/org/infinity/resource/effects/Opcode349.java new file mode 100644 index 000000000..b9e559f42 --- /dev/null +++ b/src/org/infinity/resource/effects/Opcode349.java @@ -0,0 +1,51 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2023 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.effects; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.infinity.datatype.Datatype; +import org.infinity.datatype.DecNumber; +import org.infinity.resource.AbstractStruct; +import org.infinity.resource.Profile; +import org.infinity.resource.StructEntry; + +/** + * Implemention of opcode 349. + */ +public class Opcode349 extends BaseOpcode { + private static final String EFFECT_DAMAGE_EFFECTS_TO_REFLECT = "# damage effects to reflect"; + + private static final String RES_TYPE = "BAM:VVC"; + + /** Returns the opcode name for the current game variant. */ + private static String getOpcodeName() { + switch (Profile.getEngine()) { + case EE: + if (Profile.getGame() == Profile.Game.PSTEE) { + return "Pain Mirror"; + } + default: + return null; + } + } + + public Opcode349() { + super(349, getOpcodeName()); + } + + @Override + protected String makeEffectParamsEE(Datatype parent, ByteBuffer buffer, int offset, List list, + boolean isVersion1) { + if (Profile.getGame() == Profile.Game.PSTEE) { + list.add(new DecNumber(buffer, offset, 4, EFFECT_DAMAGE_EFFECTS_TO_REFLECT)); + list.add(new DecNumber(buffer, offset + 4, 4, AbstractStruct.COMMON_UNUSED)); + return RES_TYPE; + } else { + return super.makeEffectParamsEE(parent, buffer, offset, list, isVersion1); + } + } +} diff --git a/src/org/infinity/resource/effects/Opcode350.java b/src/org/infinity/resource/effects/Opcode350.java new file mode 100644 index 000000000..48a0673d2 --- /dev/null +++ b/src/org/infinity/resource/effects/Opcode350.java @@ -0,0 +1,52 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2023 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.effects; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.infinity.datatype.Bitmap; +import org.infinity.datatype.Datatype; +import org.infinity.datatype.DecNumber; +import org.infinity.resource.AbstractStruct; +import org.infinity.resource.Profile; +import org.infinity.resource.StructEntry; + +/** + * Implemention of opcode 350. + */ +public class Opcode350 extends BaseOpcode { + private static final String EFFECT_ENABLED = "Enabled?"; + + private static final String RES_TYPE = "BAM:VVC"; + + /** Returns the opcode name for the current game variant. */ + private static String getOpcodeName() { + switch (Profile.getEngine()) { + case EE: + if (Profile.getGame() == Profile.Game.PSTEE) { + return "Guardian Mantle"; + } + default: + return null; + } + } + + public Opcode350() { + super(350, getOpcodeName()); + } + + @Override + protected String makeEffectParamsEE(Datatype parent, ByteBuffer buffer, int offset, List list, + boolean isVersion1) { + if (Profile.getGame() == Profile.Game.PSTEE) { + list.add(new Bitmap(buffer, offset, 4, EFFECT_ENABLED, AbstractStruct.OPTION_NOYES)); + list.add(new DecNumber(buffer, offset + 4, 4, AbstractStruct.COMMON_UNUSED)); + return RES_TYPE; + } else { + return super.makeEffectParamsEE(parent, buffer, offset, list, isVersion1); + } + } +} diff --git a/src/org/infinity/resource/effects/Opcode351.java b/src/org/infinity/resource/effects/Opcode351.java new file mode 100644 index 000000000..22e60c68e --- /dev/null +++ b/src/org/infinity/resource/effects/Opcode351.java @@ -0,0 +1,53 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2023 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.effects; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.infinity.datatype.Bitmap; +import org.infinity.datatype.Datatype; +import org.infinity.datatype.DecNumber; +import org.infinity.resource.AbstractStruct; +import org.infinity.resource.Profile; +import org.infinity.resource.StructEntry; + +/** + * Implemention of opcode 351. + */ +public class Opcode351 extends BaseOpcode { + private static final String EFFECT_AMOUNT = "Amount"; + private static final String EFFECT_ADD_CASTER_LEVEL_BONUS = "Add caster level bonus"; + + private static final String RES_TYPE = "BAM:VVC"; + + /** Returns the opcode name for the current game variant. */ + private static String getOpcodeName() { + switch (Profile.getEngine()) { + case EE: + if (Profile.getGame() == Profile.Game.PSTEE) { + return "Armor"; + } + default: + return null; + } + } + + public Opcode351() { + super(351, getOpcodeName()); + } + + @Override + protected String makeEffectParamsEE(Datatype parent, ByteBuffer buffer, int offset, List list, + boolean isVersion1) { + if (Profile.getGame() == Profile.Game.PSTEE) { + list.add(new DecNumber(buffer, offset, 4, EFFECT_AMOUNT)); + list.add(new Bitmap(buffer, offset + 4, 4, EFFECT_ADD_CASTER_LEVEL_BONUS, AbstractStruct.OPTION_NOYES)); + return RES_TYPE; + } else { + return super.makeEffectParamsEE(parent, buffer, offset, list, isVersion1); + } + } +} diff --git a/src/org/infinity/resource/effects/Opcode377.java b/src/org/infinity/resource/effects/Opcode377.java new file mode 100644 index 000000000..97c09912e --- /dev/null +++ b/src/org/infinity/resource/effects/Opcode377.java @@ -0,0 +1,47 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2023 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.effects; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.infinity.datatype.Datatype; +import org.infinity.datatype.DecNumber; +import org.infinity.resource.AbstractStruct; +import org.infinity.resource.Profile; +import org.infinity.resource.StructEntry; + +/** + * Implemention of opcode 377. + */ +public class Opcode377 extends BaseOpcode { + /** Returns the opcode name for the current game variant. */ + private static String getOpcodeName() { + switch (Profile.getEngine()) { + case EE: + if (Profile.getGame() == Profile.Game.PSTEE) { + return "Speak with Dead"; + } + default: + return null; + } + } + + public Opcode377() { + super(377, getOpcodeName()); + } + + @Override + protected String makeEffectParamsEE(Datatype parent, ByteBuffer buffer, int offset, List list, + boolean isVersion1) { + if (Profile.getGame() == Profile.Game.PSTEE) { + list.add(new DecNumber(buffer, offset, 4, AbstractStruct.COMMON_UNUSED)); + list.add(new DecNumber(buffer, offset + 4, 4, AbstractStruct.COMMON_UNUSED)); + return null; + } else { + return super.makeEffectParamsEE(parent, buffer, offset, list, isVersion1); + } + } +} From f52a171cfe258d9fa3eef9b206817f0474bb9578 Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Sun, 26 Mar 2023 17:27:18 +0200 Subject: [PATCH 04/22] Mass Exporter: Add option to filter resource names; improved type list - Filter supports literal strings and regular expressions - List of available resource types is dynamically generated based on current game engine --- src/org/infinity/resource/Profile.java | 3 + src/org/infinity/util/MassExporter.java | 145 ++++++++++++++++++++++-- 2 files changed, 138 insertions(+), 10 deletions(-) diff --git a/src/org/infinity/resource/Profile.java b/src/org/infinity/resource/Profile.java index 10735f4bc..0857d1027 100644 --- a/src/org/infinity/resource/Profile.java +++ b/src/org/infinity/resource/Profile.java @@ -1088,6 +1088,9 @@ public static String[] getAvailableResourceTypes(boolean ignoreGame) { if (ignoreGame || (Boolean) getProperty(Key.IS_SUPPORTED_LUA)) { list.add("LUA"); } + if (ignoreGame || (Boolean) getProperty(Key.IS_SUPPORTED_MAZE)) { + list.add("MAZE"); + } if (ignoreGame || (Boolean) getProperty(Key.IS_SUPPORTED_MENU)) { list.add("MENU"); } diff --git a/src/org/infinity/util/MassExporter.java b/src/org/infinity/util/MassExporter.java index d142cb819..7f790feae 100644 --- a/src/org/infinity/util/MassExporter.java +++ b/src/org/infinity/util/MassExporter.java @@ -5,6 +5,7 @@ package org.infinity.util; import java.awt.BorderLayout; +import java.awt.Color; import java.awt.FlowLayout; import java.awt.Graphics2D; import java.awt.GridBagConstraints; @@ -24,9 +25,15 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.concurrent.ThreadPoolExecutor; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; import javax.imageio.ImageIO; import javax.swing.JButton; @@ -40,6 +47,9 @@ import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.ProgressMonitor; +import javax.swing.UIManager; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; @@ -74,16 +84,15 @@ import org.infinity.util.io.FileManager; import org.infinity.util.io.StreamUtils; -public final class MassExporter extends ChildFrame implements ActionListener, ListSelectionListener, Runnable { +public final class MassExporter extends ChildFrame implements ActionListener, ListSelectionListener, DocumentListener, Runnable { private static final String FMT_PROGRESS = "Processing resource %d/%d"; - private static final String[] TYPES = { "2DA", "ARE", "BAM", "BCS", "BS", "BIO", "BMP", "CHU", "CHR", "CRE", "DLG", - "EFF", "FNT", "GAM", "GLSL", "GUI", "IDS", "INI", "ITM", "LUA", "MENU", "MOS", "MVE", "PLT", "PNG", "PRO", "PVRZ", - "RES", "SPL", "SQL", "SRC", "STO", "TIS", "TOH", "TOT", "TTF", "VEF", "VVC", "WAV", "WBM", "WED", "WFX", "WMP" }; + private static final Set TYPES_BLACKLIST = new HashSet<>(Arrays.asList(new String[] {"BIK", "LOG", "SAV"})); private final JButton bExport = new JButton("Export", Icons.ICON_EXPORT_16.getIcon()); private final JButton bCancel = new JButton("Cancel", Icons.ICON_DELETE_16.getIcon()); private final JButton bDirectory = new JButton(Icons.ICON_OPEN_16.getIcon()); + private final JCheckBox cbPattern = new JCheckBox("Use regular expressions", false); private final JCheckBox cbIncludeExtraDirs = new JCheckBox("Include extra folders", true); private final JCheckBox cbDecompile = new JCheckBox("Decompile scripts", true); private final JCheckBox cbDecrypt = new JCheckBox("Decrypt text files", true); @@ -101,14 +110,16 @@ public final class MassExporter extends ChildFrame implements ActionListener, Li private final JCheckBox cbOverwrite = new JCheckBox("Overwrite existing files", false); private final JFileChooser fc = new JFileChooser(Profile.getGameRoot().toFile()); private final JComboBox cbExtractFramesBAMFormat = new JComboBox<>(new String[] { "PNG", "BMP" }); - private final JList listTypes = new JList<>(TYPES); - private final JTextField tfDirectory = new JTextField(20); + private final JList listTypes = new JList<>(getAvailableResourceTypes()); + private final JTextField tfDirectory = new JTextField(16); + private final JTextField tfPattern = new JTextField(16); private Path outputPath; private List selectedTypes; private ProgressMonitor progress; private int progressIndex; private List selectedFiles; + private Pattern pattern; public MassExporter() { super("Mass Exporter", true); @@ -118,6 +129,8 @@ public MassExporter() { bDirectory.addActionListener(this); bExport.setEnabled(false); tfDirectory.setEditable(false); + tfPattern.getDocument().addDocumentListener(this); + updateTextFieldColor(tfPattern); listTypes.addListSelectionListener(this); fc.setDialogTitle("Mass export: Select directory"); fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); @@ -140,6 +153,12 @@ public MassExporter() { topRightPanel.add(tfDirectory, BorderLayout.CENTER); topRightPanel.add(bDirectory, BorderLayout.EAST); + JPanel patternPanel = new JPanel(new BorderLayout()); + JLabel lPattern = new JLabel("Resource name filter:"); + patternPanel.add(lPattern, BorderLayout.NORTH); + patternPanel.add(tfPattern, BorderLayout.CENTER); + patternPanel.add(cbPattern, BorderLayout.SOUTH); + GridBagConstraints gbc = new GridBagConstraints(); JPanel bottomRightPanel = new JPanel(new GridBagLayout()); @@ -229,7 +248,7 @@ public MassExporter() { gbc.weightx = 0.0; gbc.weighty = 1.0; - gbc.gridheight = 2; + gbc.gridheight = 3; gbc.fill = GridBagConstraints.BOTH; gbc.insets = new Insets(6, 6, 6, 6); gbl.setConstraints(leftPanel, gbc); @@ -242,6 +261,9 @@ public MassExporter() { gbl.setConstraints(topRightPanel, gbc); pane.add(topRightPanel); + gbl.setConstraints(patternPanel, gbc); + pane.add(patternPanel); + gbc.weighty = 1.0; gbl.setConstraints(bottomRightPanel, gbc); pane.add(bottomRightPanel); @@ -262,9 +284,34 @@ public MassExporter() { @Override public void actionPerformed(ActionEvent event) { + if (event.getSource() == tfPattern) { + if (tfPattern.getText().isEmpty()) { + final Color bg = UIManager.getColor("TextField.background"); + tfPattern.setBackground(bg.darker()); + } else { + tfPattern.setBackground(UIManager.getColor("TextField.background")); + } + } else if (event.getSource() == bExport) { selectedTypes = listTypes.getSelectedValuesList(); outputPath = FileManager.resolve(tfDirectory.getText()); + try { + pattern = getPattern(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, e.getMessage(), "Pattern syntax error", JOptionPane.ERROR_MESSAGE); + if (e instanceof PatternSyntaxException) { + final int index = ((PatternSyntaxException)e).getIndex(); + if (index >= 0) { + tfPattern.setCaretPosition(index); + } else { + tfPattern.setCaretPosition(tfPattern.getText().length()); + } + } + tfPattern.requestFocusInWindow(); + return; + } + try { Files.createDirectories(outputPath); } catch (IOException e) { @@ -295,6 +342,25 @@ public void valueChanged(ListSelectionEvent event) { // --------------------- End Interface ListSelectionListener --------------------- + // --------------------- Begin Interface DocumentListener --------------------- + + @Override + public void insertUpdate(DocumentEvent e) { + updateTextFieldColor(tfPattern); + } + + @Override + public void removeUpdate(DocumentEvent e) { + updateTextFieldColor(tfPattern); + } + + @Override + public void changedUpdate(DocumentEvent e) { + updateTextFieldColor(tfPattern); + } + + // --------------------- End Interface DocumentListener --------------------- + // --------------------- Begin Interface Runnable --------------------- @Override @@ -317,7 +383,22 @@ public void run() { selectedFiles = new ArrayList<>(1000); for (final String newVar : selectedTypes) { - selectedFiles.addAll(ResourceFactory.getResources(newVar, extraDirs)); + if (pattern != null) { + selectedFiles.addAll( + ResourceFactory.getResources(newVar, extraDirs) + .stream() + .filter(e -> pattern.matcher(e.getResourceRef()).find()) + .collect(Collectors.toList()) + ); + } else { + selectedFiles.addAll(ResourceFactory.getResources(newVar, extraDirs)); + } + } + + if (selectedFiles.isEmpty()) { + JOptionPane.showMessageDialog(NearInfinity.getInstance(), "No files to export.", "Info", + JOptionPane.INFORMATION_MESSAGE); + return; } // executing multithreaded search @@ -359,10 +440,11 @@ public void run() { } if (isCancelled) { - JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Mass export aborted", "Info", + JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Mass export aborted.", "Info", JOptionPane.INFORMATION_MESSAGE); } else { - JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Mass export completed", "Info", + JOptionPane.showMessageDialog(NearInfinity.getInstance(), + String.format("Mass export completed.\n%d file(s) exported.", selectedFiles.size()), "Info", JOptionPane.INFORMATION_MESSAGE); } } finally { @@ -377,10 +459,53 @@ public void run() { // --------------------- End Interface Runnable --------------------- + /** + * Returns an array with all resource types available for the current game. + */ + private static String[] getAvailableResourceTypes() { + List types = Arrays.asList(Profile.getAvailableResourceTypes()) + .stream() + .filter(s -> !TYPES_BLACKLIST.contains(s)) + .collect(Collectors.toList()); + return types.toArray(new String[types.size()]); + } + private int getResourceCount() { return (selectedFiles != null) ? selectedFiles.size() : 0; } + /** Returns {@link Pattern} object from the current regular expression pattern. */ + private Pattern getPattern() throws IllegalArgumentException { + if (!tfPattern.getText().isEmpty()) { + final String pattern = cbPattern.isSelected() ? tfPattern.getText() : Pattern.quote(tfPattern.getText()); + return Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + } + return null; + } + + /** + * Updates the background color of the specified {@link JTextField} component based on whether it has content. + * + * @param tf {@link JTextField} component to update. + */ + private void updateTextFieldColor(JTextField tf) { + if (tf != null) { + Color bg = UIManager.getColor("TextField.background"); + if (tf.getText().isEmpty()) { + // shaded background if text field is empty + int lo = 240; + int hi = 256; + int bright = (bg.getRed() + bg.getGreen() + bg.getBlue()) / 3; + if (bright >= 64) { + bg = new Color(bg.getRed() * lo / hi, bg.getGreen() * lo / hi, bg.getBlue() * lo / hi); + } else { + bg = new Color(bg.getRed() * hi / lo, bg.getGreen() * hi / lo, bg.getBlue() * hi / lo); + } + } + tfPattern.setBackground(bg); + } + } + private synchronized void advanceProgress(boolean finished) { if (progress != null) { if (finished) { From 393d7487a6ee1a827abf1b22d8be11d772045342 Mon Sep 17 00:00:00 2001 From: Bubb13 <36863623+Bubb13@users.noreply.github.com> Date: Sat, 1 Apr 2023 19:01:19 -0700 Subject: [PATCH 05/22] BAM Converter: Transparency fix when reducing palettes to 256 colors --- .../infinity/gui/converter/BamPaletteDialog.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/org/infinity/gui/converter/BamPaletteDialog.java b/src/org/infinity/gui/converter/BamPaletteDialog.java index 0a7a1bcc8..df8459b6e 100644 --- a/src/org/infinity/gui/converter/BamPaletteDialog.java +++ b/src/org/infinity/gui/converter/BamPaletteDialog.java @@ -315,6 +315,8 @@ public void setPaletteModified() { * Calculates a new palette based on the currently registered colors and stores it in the PaletteDialog instance. */ public void updateGeneratedPalette() { + final int Green = 0xff00ff00; + if (isPaletteModified() && !lockedPalette) { if (colorMap.size() <= 256) { // checking whether all frames share the same palette @@ -360,6 +362,12 @@ public void updateGeneratedPalette() { } } else { // reducing color count to max. 256 + + // medianCut() should not consider the special transparent green color, workaround to keep it preserved + Integer savedGreen = colorMap.get(Green); + if (savedGreen != null) { + colorMap.remove(Green); + } int[] pixels = new int[colorMap.size()]; Iterator iter = colorMap.keySet().iterator(); int idx = 0; @@ -367,7 +375,12 @@ public void updateGeneratedPalette() { pixels[idx] = iter.next(); idx++; } - ColorConvert.medianCut(pixels, 256, palettes[TYPE_GENERATED], false); + final int desiredColors = savedGreen != null ? 255 : 256; + ColorConvert.medianCut(pixels, desiredColors, palettes[TYPE_GENERATED], false); + if (savedGreen != null) { + colorMap.put(Green, savedGreen); + palettes[TYPE_GENERATED][255] = Green; + } } // moving special "green" to the first index From c5dc82e9150de756a24c5f40a00feeea0608de27 Mon Sep 17 00:00:00 2001 From: Bubb13 <36863623+Bubb13@users.noreply.github.com> Date: Sat, 1 Apr 2023 20:32:32 -0700 Subject: [PATCH 06/22] BAM Converter: getNearestColor() shouldn't select transparent green - Causes non-transparent pixels to become transparent for BAM V1. --- src/org/infinity/gui/converter/ConvertToBam.java | 4 ++-- .../infinity/resource/graphics/ColorConvert.java | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/org/infinity/gui/converter/ConvertToBam.java b/src/org/infinity/gui/converter/ConvertToBam.java index 52ff6dfa3..9dbc74ab1 100644 --- a/src/org/infinity/gui/converter/ConvertToBam.java +++ b/src/org/infinity/gui/converter/ConvertToBam.java @@ -4153,7 +4153,7 @@ private void updateFinalBamDecoder(int bamVersion) throws Exception { dstBuf[ofs] = colIdx;// (byte)ci; } else { double weight = getUseAlpha() ? 1.0 : 0.0; - byte color = (byte) ColorConvert.getNearestColor(srcBuf[ofs], palette, weight, null); + byte color = (byte) ColorConvert.getNearestColor(srcBuf[ofs], palette, weight, null, true); dstBuf[ofs] = color;// (byte)ci; colorCache.put(c, color); } @@ -4246,7 +4246,7 @@ private void updateFinalBamFrame(int bamVersion, int frameIdx) { dstBuf[ofs] = colIdx; } else { double weight = getUseAlpha() ? 1.0 : 0.0; - byte color = (byte) ColorConvert.getNearestColor(srcBuf[ofs], palette, weight, null); + byte color = (byte) ColorConvert.getNearestColor(srcBuf[ofs], palette, weight, null, true); dstBuf[ofs] = color;// (byte)ci; colorCache.put(c, color); } diff --git a/src/org/infinity/resource/graphics/ColorConvert.java b/src/org/infinity/resource/graphics/ColorConvert.java index e3f7d8500..657760ea9 100644 --- a/src/org/infinity/resource/graphics/ColorConvert.java +++ b/src/org/infinity/resource/graphics/ColorConvert.java @@ -342,7 +342,9 @@ public static Dimension getImageDimension(Path fileName) { * calculation. * @return Palette index pointing to the nearest color value. Returns -1 if color entry could not be determined. */ - public static int getNearestColor(int argb, int[] palette, double alphaWeight, ColorDistanceFunc calculator) { + public static int getNearestColor(int argb, int[] palette, double alphaWeight, ColorDistanceFunc calculator, boolean skipGreen) { + final int Green = 0x0000ff00; + int retVal = -1; if (palette == null) { return retVal; @@ -354,7 +356,13 @@ public static int getNearestColor(int argb, int[] palette, double alphaWeight, C alphaWeight = Math.max(0.0, Math.min(2.0, alphaWeight)); double minDist = Double.MAX_VALUE; for (int i = 0; i < palette.length; i++) { - double dist = calculator.calculate(argb, palette[i], alphaWeight); + final int argb2 = palette[i]; + // BAM V1: It is a bad idea to use the transparent green color as the "nearest" color, + // the input most likely did not intend for adjacent colors to be transparent. + if (skipGreen && (argb2 & 0x00ffffff) == Green) { + continue; + } + double dist = calculator.calculate(argb, argb2, alphaWeight); if (dist < minDist) { minDist = dist; retVal = i; @@ -364,6 +372,10 @@ public static int getNearestColor(int argb, int[] palette, double alphaWeight, C return retVal; } + public static int getNearestColor(int argb, int[] palette, double alphaWeight, ColorDistanceFunc calculator) { + return getNearestColor(argb, palette, alphaWeight, calculator, false); + } + /** * Sorts the given palette in-place by the specified sort type. * From fbda1db1109f9bd45c1d5136dfa8e7d92708061a Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Sun, 2 Apr 2023 10:24:08 +0200 Subject: [PATCH 07/22] Documentation and optimization --- .../gui/converter/BamPaletteDialog.java | 9 +++------ .../resource/graphics/ColorConvert.java | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/org/infinity/gui/converter/BamPaletteDialog.java b/src/org/infinity/gui/converter/BamPaletteDialog.java index df8459b6e..f3f50de7e 100644 --- a/src/org/infinity/gui/converter/BamPaletteDialog.java +++ b/src/org/infinity/gui/converter/BamPaletteDialog.java @@ -364,12 +364,9 @@ public void updateGeneratedPalette() { // reducing color count to max. 256 // medianCut() should not consider the special transparent green color, workaround to keep it preserved - Integer savedGreen = colorMap.get(Green); - if (savedGreen != null) { - colorMap.remove(Green); - } - int[] pixels = new int[colorMap.size()]; - Iterator iter = colorMap.keySet().iterator(); + final Integer savedGreen = colorMap.remove(Green); + final int[] pixels = new int[colorMap.size()]; + final Iterator iter = colorMap.keySet().iterator(); int idx = 0; while (idx < pixels.length && iter.hasNext()) { pixels[idx] = iter.next(); diff --git a/src/org/infinity/resource/graphics/ColorConvert.java b/src/org/infinity/resource/graphics/ColorConvert.java index 657760ea9..0f2ad9d83 100644 --- a/src/org/infinity/resource/graphics/ColorConvert.java +++ b/src/org/infinity/resource/graphics/ColorConvert.java @@ -340,9 +340,11 @@ public static Dimension getImageDimension(Path fileName) { * @param calculator the function for distance calculation. Choose one of the predefined functions or specify a * custom instance. Specify {@code null} to use the fastest (but slightly inaccurate) distance * calculation. + * @param skipGreen indicates whether the special color "Green" should be ignored by the color calculation. * @return Palette index pointing to the nearest color value. Returns -1 if color entry could not be determined. */ - public static int getNearestColor(int argb, int[] palette, double alphaWeight, ColorDistanceFunc calculator, boolean skipGreen) { + public static int getNearestColor(int argb, int[] palette, double alphaWeight, ColorDistanceFunc calculator, + boolean skipGreen) { final int Green = 0x0000ff00; int retVal = -1; @@ -372,6 +374,20 @@ public static int getNearestColor(int argb, int[] palette, double alphaWeight, C return retVal; } + /** + * Calculates the nearest color available in the given palette using the specified color distance function. + * + * @param argb the reference ARGB color. + * @param palette palette with ARGB colors to search. + * @param alphaWeight Weight factor of the alpha component. Supported range: [0.0, 2.0]. A value < 1.0 makes alpha + * less important for the distance calculation. A value > 1.0 makes alpha more important for the + * distance calculation. Specify 1.0 to use the unmodified alpha compomponent for the calculation. + * Specify 0.0 to ignore the alpha part in the calculation. + * @param calculator the function for distance calculation. Choose one of the predefined functions or specify a + * custom instance. Specify {@code null} to use the fastest (but slightly inaccurate) distance + * calculation. + * @return Palette index pointing to the nearest color value. Returns -1 if color entry could not be determined. + */ public static int getNearestColor(int argb, int[] palette, double alphaWeight, ColorDistanceFunc calculator) { return getNearestColor(argb, palette, alphaWeight, calculator, false); } From 6a3e649abc650c81e3717a2115729cc00f7978d0 Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Sun, 2 Apr 2023 20:53:37 +0200 Subject: [PATCH 08/22] Fix PRO resources dynamically change size depending on projectile type Fixes a regression from a85dc4f4b0e7924fe8c5760d604a72be0c62ab6e --- src/org/infinity/resource/pro/ProResource.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/org/infinity/resource/pro/ProResource.java b/src/org/infinity/resource/pro/ProResource.java index dbc0a45ae..c5fc5bd5e 100644 --- a/src/org/infinity/resource/pro/ProResource.java +++ b/src/org/infinity/resource/pro/ProResource.java @@ -52,7 +52,7 @@ * @see * https://gibberlings3.github.io/iesdp/file_formats/ie_formats/pro_v1.htm */ -public final class ProResource extends AbstractStruct implements Resource, HasViewerTabs, UpdateListener { +public final class ProResource extends AbstractStruct implements Resource, HasChildStructs, HasViewerTabs, UpdateListener { // PRO-specific field labels public static final String PRO_TYPE = "Projectile type"; public static final String PRO_SPEED = "Speed"; @@ -195,6 +195,16 @@ public boolean valueUpdated(UpdateEvent event) { return false; } + @Override + public AddRemovable[] getPrototypes() throws Exception { + return new AddRemovable[] {}; + } + + @Override + public AddRemovable confirmAddEntry(AddRemovable entry) throws Exception { + return entry; + } + @Override public int getViewerTabCount() { return 1; From 04a63a400b345ff194c67d60423e2118a99ddc1e Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Fri, 14 Apr 2023 11:29:43 +0200 Subject: [PATCH 09/22] Fixed hardcoded charset encoding for script comments in Mass Exporter ...and streamlined charset selection. --- src/org/infinity/datatype/TextString.java | 2 +- .../infinity/resource/bcs/BafResource.java | 6 ++-- .../infinity/resource/bcs/BcsResource.java | 5 ++- .../resource/cre/decoder/util/ItemInfo.java | 3 +- .../resource/text/PlainTextResource.java | 7 +--- .../resource/text/modes/GLSLTokenMaker.java | 4 +-- src/org/infinity/util/MassExporter.java | 4 ++- src/org/infinity/util/Misc.java | 36 +++++++++++++++++++ 8 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/org/infinity/datatype/TextString.java b/src/org/infinity/datatype/TextString.java index a5b5f6e14..852eca395 100644 --- a/src/org/infinity/datatype/TextString.java +++ b/src/org/infinity/datatype/TextString.java @@ -35,7 +35,7 @@ public final class TextString extends Datatype implements InlineEditable, IsText public TextString(ByteBuffer buffer, int offset, int length, String name) { super(offset, length, name); this.buffer = StreamUtils.getByteBuffer(length); - this.charset = Charset.forName(BrowserMenuBar.getInstance().getSelectedCharset()); + this.charset = Misc.getCharsetFrom(BrowserMenuBar.getInstance().getSelectedCharset()); read(buffer, offset); } diff --git a/src/org/infinity/resource/bcs/BafResource.java b/src/org/infinity/resource/bcs/BafResource.java index 34bd8e21e..f01756734 100644 --- a/src/org/infinity/resource/bcs/BafResource.java +++ b/src/org/infinity/resource/bcs/BafResource.java @@ -16,7 +16,6 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.nio.file.Files; import java.util.ArrayList; import java.util.Locale; @@ -55,6 +54,7 @@ import org.infinity.resource.Writeable; import org.infinity.resource.key.ResourceEntry; import org.infinity.search.TextResourceSearcher; +import org.infinity.util.Misc; import org.infinity.util.StaticSimpleXorDecryptor; import org.infinity.util.io.StreamUtils; @@ -92,7 +92,7 @@ public BafResource(ResourceEntry entry) throws Exception { buffer = StaticSimpleXorDecryptor.decrypt(buffer, 2); } text = StreamUtils.readString(buffer, buffer.limit(), - Charset.forName(BrowserMenuBar.getInstance().getSelectedCharset())); + Misc.getCharsetFrom(BrowserMenuBar.getInstance().getSelectedCharset())); } // --------------------- Begin Interface ActionListener --------------------- @@ -478,7 +478,7 @@ public String getDescription() { int returnval = chooser.showSaveDialog(panel.getTopLevelAncestor()); if (returnval == JFileChooser.APPROVE_OPTION) { try (BufferedWriter bw = Files.newBufferedWriter(chooser.getSelectedFile().toPath(), - Charset.forName(BrowserMenuBar.getInstance().getSelectedCharset()))) { + Misc.getCharsetFrom(BrowserMenuBar.getInstance().getSelectedCharset()))) { bw.write(codeText.getText()); JOptionPane.showMessageDialog(panel, "File saved to \"" + chooser.getSelectedFile().toString() + '\"', "Save completed", JOptionPane.INFORMATION_MESSAGE); diff --git a/src/org/infinity/resource/bcs/BcsResource.java b/src/org/infinity/resource/bcs/BcsResource.java index a6cfa8d10..dc94f54e2 100644 --- a/src/org/infinity/resource/bcs/BcsResource.java +++ b/src/org/infinity/resource/bcs/BcsResource.java @@ -16,7 +16,6 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; @@ -328,7 +327,7 @@ public BcsResource(ResourceEntry entry) throws Exception { buffer = StaticSimpleXorDecryptor.decrypt(buffer, 2); } text = StreamUtils.readString(buffer, buffer.limit(), - Charset.forName(BrowserMenuBar.getInstance().getSelectedCharset())); + Misc.getCharsetFrom(BrowserMenuBar.getInstance().getSelectedCharset())); } // --------------------- Begin Interface ActionListener --------------------- @@ -476,7 +475,7 @@ public String getDescription() { int returnval = chooser.showSaveDialog(panel.getTopLevelAncestor()); if (returnval == JFileChooser.APPROVE_OPTION) { try (BufferedWriter bw = Files.newBufferedWriter(chooser.getSelectedFile().toPath(), - Charset.forName(BrowserMenuBar.getInstance().getSelectedCharset()))) { + Misc.getCharsetFrom(BrowserMenuBar.getInstance().getSelectedCharset()))) { bw.write(sourceText.getText().replaceAll("\r?\n", Misc.LINE_SEPARATOR)); JOptionPane.showMessageDialog(panel, "File saved to \"" + chooser.getSelectedFile().toString() + '\"', "Export complete", JOptionPane.INFORMATION_MESSAGE); diff --git a/src/org/infinity/resource/cre/decoder/util/ItemInfo.java b/src/org/infinity/resource/cre/decoder/util/ItemInfo.java index f7c5988cd..25111bd3a 100644 --- a/src/org/infinity/resource/cre/decoder/util/ItemInfo.java +++ b/src/org/infinity/resource/cre/decoder/util/ItemInfo.java @@ -7,7 +7,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -1044,7 +1043,7 @@ private EffectEntry(byte[] effect) { this.timing = buf.getByte(0xc); this.dispelResist = buf.getByte(0xd); this.duration = buf.getByte(0xe); - this.resource = DynamicArray.getString(effect, 0x14, 8, Charset.forName("windows-1252")); + this.resource = DynamicArray.getString(effect, 0x14, 8, Misc.CHARSET_DEFAULT); this.savingThrowFlags = buf.getInt(0x24); this.savingThrow = buf.getInt(0x28); this.special = buf.getInt(0x2c); diff --git a/src/org/infinity/resource/text/PlainTextResource.java b/src/org/infinity/resource/text/PlainTextResource.java index 97223cf99..27496a7cd 100644 --- a/src/org/infinity/resource/text/PlainTextResource.java +++ b/src/org/infinity/resource/text/PlainTextResource.java @@ -328,12 +328,7 @@ public PlainTextResource(ResourceEntry entry) throws Exception { if (buffer.limit() > 1 && buffer.getShort(0) == -1) { buffer = StaticSimpleXorDecryptor.decrypt(buffer, 2); } - final Charset cs; - if (BrowserMenuBar.getInstance() != null) { - cs = Charset.forName(BrowserMenuBar.getInstance().getSelectedCharset()); - } else { - cs = Misc.CHARSET_DEFAULT; - } + final Charset cs = Misc.getCharsetFrom(BrowserMenuBar.getInstance().getSelectedCharset()); text = StreamUtils.readString(buffer, buffer.limit(), cs); } diff --git a/src/org/infinity/resource/text/modes/GLSLTokenMaker.java b/src/org/infinity/resource/text/modes/GLSLTokenMaker.java index c7023d22b..22b237fee 100644 --- a/src/org/infinity/resource/text/modes/GLSLTokenMaker.java +++ b/src/org/infinity/resource/text/modes/GLSLTokenMaker.java @@ -24,6 +24,7 @@ import org.fife.ui.rsyntaxtextarea.AbstractJFlexCTokenMaker; import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rsyntaxtextarea.TokenImpl; +import org.infinity.util.Misc; /** @@ -3872,8 +3873,7 @@ public GLSLTokenMaker(java.io.Reader in) { * @param in the java.io.Inputstream to read input from. */ public GLSLTokenMaker(java.io.InputStream in) { - this(new java.io.InputStreamReader - (in, java.nio.charset.Charset.forName("UTF-8"))); + this(new java.io.InputStreamReader(in, Misc.CHARSET_UTF8)); } /** diff --git a/src/org/infinity/util/MassExporter.java b/src/org/infinity/util/MassExporter.java index 7f790feae..e267304b6 100644 --- a/src/org/infinity/util/MassExporter.java +++ b/src/org/infinity/util/MassExporter.java @@ -22,6 +22,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -794,7 +795,8 @@ private ByteBuffer decompileScript(ResourceEntry entry, ByteBuffer inBuffer) thr final Decompiler decompiler = new Decompiler(StreamUtils.readString(inBuffer, inBuffer.limit()), false); decompiler.setGenerateComments(BrowserMenuBar.getInstance().autogenBCSComments()); String script = decompiler.getSource().replaceAll("\r?\n", Misc.LINE_SEPARATOR); - return ByteBuffer.wrap(script.getBytes(Misc.CHARSET_DEFAULT)); + final Charset cs = Misc.getCharsetFrom(BrowserMenuBar.getInstance().getSelectedCharset()); + return ByteBuffer.wrap(script.getBytes(cs)); } return inBuffer; } diff --git a/src/org/infinity/util/Misc.java b/src/org/infinity/util/Misc.java index e2840d078..c315b99a0 100644 --- a/src/org/infinity/util/Misc.java +++ b/src/org/infinity/util/Misc.java @@ -18,6 +18,7 @@ import javax.swing.JComponent; import org.infinity.NearInfinity; +import org.infinity.resource.Profile; /** * A general-purpose class containing useful function not fitting elsewhere. @@ -61,6 +62,41 @@ public boolean equals(Object obj) { }; } + /** + * Returns a default charset depending on the current game type. + * + * @return {@link #CHARSET_UTF8} for Enhanced Edition games, {@link #CHARSET_DEFAULT} otherwise. + */ + public static Charset getDefaultCharset() { + return Profile.isEnhancedEdition() ? CHARSET_UTF8 : CHARSET_DEFAULT; + } + + /** + * A convenience method that attempts to return the charset specified by the given name or the next best match + * depending on the current game type. + * + * @param charsetName Name of the desired charset as {@code String}. + * @return The desired charset if successful, a game-specific default charset otherwise. + */ + public static Charset getCharsetFrom(String charsetName) { + return getCharsetFrom(charsetName, getDefaultCharset()); + } + + /** + * A convenience method that attempts to return the charset specified by the given name. + * + * @param charsetName Name of the desired charset as {@code String}. + * @param defaultCharset Fallback solution if the desired charset doesn't exist. + * @return the desired charset if successful, {@code defaultCharset} otherwise. + */ + public static Charset getCharsetFrom(String charsetName, Charset defaultCharset) { + try { + return Charset.forName(charsetName); + } catch (Exception e) { + return defaultCharset; + } + } + /** * Attempts to detect the character set of the text data in the specified byte buffer. * From 394d747666d12c723cc21ea4a5107f16e02ba97f Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Fri, 26 May 2023 11:48:47 +0200 Subject: [PATCH 10/22] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index cd76010d9..7c3d15237 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,11 @@ A file browser and editor for the Infinity Engine. You can find out more in the [Near Infinity Wiki](https://github.com/NearInfinityBrowser/NearInfinity/wiki), or download it directly from the [Releases section](https://github.com/Argent77/NearInfinity/releases). +**Discuss Near Infinity on:** +- [GitHub Discussions](https://github.com/NearInfinityBrowser/NearInfinity/discussions) +- [Beamdog Forums](https://forums.beamdog.com/discussion/30593/new-versions-of-nearinfinity-available/) +- [Spellhold Studios Forums](http://www.shsforums.net/topic/45358-nearinfinity/) + ## How to build Near Infinity **Required tools:** From 54de11e7408aafdee71f2da5491e1e0416a45825 Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Wed, 31 May 2023 11:00:16 +0200 Subject: [PATCH 11/22] Fix opcode 319, param2=11 initialization Fixed resource field not using the correct datatype when an effect structure with opcode=319, param2=11 is loaded. --- src/org/infinity/resource/effects/Opcode319.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/org/infinity/resource/effects/Opcode319.java b/src/org/infinity/resource/effects/Opcode319.java index 3fb00d8fe..3ac0f8011 100644 --- a/src/org/infinity/resource/effects/Opcode319.java +++ b/src/org/infinity/resource/effects/Opcode319.java @@ -51,6 +51,7 @@ protected String makeEffectParamsEE(Datatype parent, ByteBuffer buffer, int offs list.set(1, power); } + String resType = null; byte power = buffer.get(offset - 1); if (isEEEx() && (power == 2 || power == 3)) { final SpellProtType param2 = new SpellProtType(buffer, offset + 4, 4); @@ -65,9 +66,12 @@ protected String makeEffectParamsEE(Datatype parent, ByteBuffer buffer, int offs param2.addUpdateListener((UpdateListener)parent); list.add(param2.createIdsValueFromType(buffer)); list.add(param2); + if (param2.getValue() == 11) { + resType = EFFECT_STRING; + } } - return null; + return resType; } @Override From 42175e1af6bca9c4a08058eddd092b3eedac9044 Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Mon, 5 Jun 2023 16:01:52 +0200 Subject: [PATCH 12/22] Improve Near Infinity application icons - Added 256x256 and 512x512 pixels icon variants - Improved icon content - Made icon loading function more robust --- src/org/infinity/NearInfinity.java | 9 +++++++-- src/org/infinity/icon/App128.png | Bin 11072 -> 13411 bytes src/org/infinity/icon/App16.png | Bin 589 -> 666 bytes src/org/infinity/icon/App256.png | Bin 0 -> 40377 bytes src/org/infinity/icon/App32.png | Bin 1376 -> 1667 bytes src/org/infinity/icon/App512.png | Bin 0 -> 116503 bytes src/org/infinity/icon/App64.png | Bin 3815 -> 4660 bytes src/org/infinity/icon/Icons.java | 12 +++++++++--- 8 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 src/org/infinity/icon/App256.png create mode 100644 src/org/infinity/icon/App512.png diff --git a/src/org/infinity/NearInfinity.java b/src/org/infinity/NearInfinity.java index dd969799a..ec51b5e0a 100644 --- a/src/org/infinity/NearInfinity.java +++ b/src/org/infinity/NearInfinity.java @@ -1122,8 +1122,13 @@ private void storePreferences() { private void setAppIcon() { List list = new ArrayList<>(); - for (int i = 4; i < 8; i++) { - list.add(Icons.getImage(null, String.format("App%d.png", 1 << i))); + for (int i = 4; true; i++) { + final Image icon = Icons.getImage(null, String.format("App%d.png", 1 << i)); + if (icon != null) { + list.add(icon); + } else { + break; + } } setIconImages(list); } diff --git a/src/org/infinity/icon/App128.png b/src/org/infinity/icon/App128.png index a4111c8613b81c447825787e46116db4a3a8b338..43c3c7b90517339ee7873eda779b6d7513bc741c 100644 GIT binary patch literal 13411 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 literal 11072 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 Hu0mjfkVGx) delta 544 zcmV+*0^j|b1cpDIp_aBN<_#;8d)l90P{z%0v(;v_VQ5M(ad*yB7Yr+ivyQVLB1=kb{1f_ z!o|e1*Mo0Bk%|q11iux18B~`oC~_!is3ZB{P@Fx zGu=&nJuSC^q{kQJ>*vWZ5CYz&e%6#@i|F=EGjp4CZ|kIXSTJ4r0Ml2Qx*k6`jH_XS zupH}{IgRv1llfz_d*mHWq~IrjKQcYn{#_cF3pYD%aC`UzYX>!F$N zjVJE*HXMH{4tM#J1vUaG%;)Rsx4(F}=;=tvfGhj;!5Tgm9^2(kvDL+8e9yRaN@iP-^iMF9py|6R?1hQ&d#2s$Me; iR0c#uQW! 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