// 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.gam;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import org.infinity.datatype.Bitmap;
import org.infinity.datatype.DecNumber;
import org.infinity.datatype.Flag;
import org.infinity.datatype.HexNumber;
import org.infinity.datatype.IsNumeric;
import org.infinity.datatype.ResourceRef;
import org.infinity.datatype.SectionCount;
import org.infinity.datatype.SectionOffset;
import org.infinity.datatype.TextString;
import org.infinity.datatype.Unknown;
import org.infinity.gui.StructViewer;
import org.infinity.gui.hexview.BasicColorMap;
import org.infinity.gui.hexview.StructHexViewer;
import org.infinity.resource.AbstractStruct;
import org.infinity.resource.AddRemovable;
import org.infinity.resource.HasAddRemovable;
import org.infinity.resource.HasViewerTabs;
import org.infinity.resource.Profile;
import org.infinity.resource.Resource;
import org.infinity.resource.key.ResourceEntry;
public final class GamResource extends AbstractStruct implements Resource, HasAddRemovable, HasViewerTabs
{
// GAM-specific field labels
public static final String GAM_GAME_TIME = "Game time (game seconds)";
public static final String GAM_SELECTED_FORMATION = "Selected formation";
public static final String GAM_FORMATION_BUTTON_FMT = "Formation button %d";
public static final String GAM_PARTY_GOLD = "Party gold";
public static final String GAM_NUM_NPCS_IN_PARTY = "# NPCs in party";
public static final String GAM_WEATHER = "Weather";
public static final String GAM_OFFSET_PARTY_MEMBERS = "Party members offset";
public static final String GAM_NUM_PARTY_MEMBERS = "# party members";
public static final String GAM_OFFSET_UNUSED = "Unused offset";
public static final String GAM_NUM_UNUSED = "Unused count";
public static final String GAM_OFFSET_NON_PARTY_MEMBERS = "Non-party characters offset";
public static final String GAM_NUM_NON_PARTY_MEMBERS = "# non-party characters";
public static final String GAM_OFFSET_GLOBAL_VARIABLES = "Global variables offset";
public static final String GAM_NUM_GLOBAL_VARIABLES = "# global variables";
public static final String GAM_MASTER_AREA = "Master area";
public static final String GAM_CURRENT_LINK = "Current link";
public static final String GAM_NUM_JOURNAL_ENTRIES = "# journal entries";
public static final String GAM_OFFSET_JOURNAL_ENTRIES = "Journal entries offset";
public static final String GAM_REPUTATION = "Reputation";
public static final String GAM_CURRENT_AREA = "Current area";
public static final String GAM_CURRENT_AREA_2 = "Current area 2";
public static final String GAM_CONFIGURATION = "Configuration";
public static final String GAM_SAVE_VERSION = "Save version";
public static final String GAM_NUM_UNKNOWN = "Unknown section count";
public static final String GAM_OFFSET_UNKNOWN = "Unknown section offset";
public static final String GAM_OFFSET_MODRON_MAZE = "Modron maze offset";
public static final String GAM_OFFSET_KILL_VARIABLES = "Kill variables offset";
public static final String GAM_NUM_KILL_VARIABLES = "# kill variables";
public static final String GAM_OFFSET_BESTIARY = "Bestiary offset";
public static final String GAM_OFFSET_FAMILIAR_INFO = "Familiar info offset";
public static final String GAM_OFFSET_STORED_LOCATIONS = "Stored locations offset";
public static final String GAM_NUM_STORED_LOCATIONS = "# stored locations";
public static final String GAM_REAL_TIME = "Game time (real seconds)";
public static final String GAM_OFFSET_POCKET_PLANE_LOCATIONS = "Pocket plane locations offset";
public static final String GAM_NUM_POCKET_PLANE_LOCATIONS = "# pocket plane locations";
public static final String GAM_ZOOM_LEVEL = "Zoom level";
public static final String GAM_RANDOM_ENCOUNTER_AREA = "Random encounter area";
public static final String GAM_WORLDMAP = "Worldmap";
public static final String GAM_CAMPAIGN = "Campaign";
public static final String GAM_FAMILIAR_OWNER = "Familiar owner";
public static final String GAM_ENCOUNTER_ENTRY = "Encounter entry";
public static final String GAM_BESTIARY = "Bestiary";
public static final String GAM_OFFSET_END_OF_UNKNOWN_STRUCTURE = "End of unknown structure offset";
public static final String GAM_UNKNOWN_STRUCTURE = "Unknown structure";
public static final String GAM_POCKET_PLANE = "Pocket plane";
public static final String[] s_formation = {"Button 1", "Button 2", "Button 3", "Button 4", "Button 5"};
public static final String[] s_weather = {"No weather", "Raining", "Snowing", "Light weather",
"Medium weather", "Light wind", "Medium wind", "Rare lightning",
"Regular lightning", "Storm increasing"};
public static final String[] s_torment = {"Follow", "T", "Gather", "4 and 2", "3 by 2",
"Protect", "2 by 3", "Rank", "V", "Wedge", "S",
"Line", "None"};
public static final String[] s_configuration = {
"Normal windows", "Party AI disabled", "Larger text window", "Largest text window", "",
"Fullscreen mode", "Left pane hidden", "Right pane hidden", "Automap notes hidden"};
public static final String[] s_configuration_bg1 = {
"Normal windows", "Party AI disabled", "Larger text window", "Largest text window"};
public static final String[] s_configuration_iwd = {
"Normal windows", "Party AI disabled", "Larger text window", "Largest text window", "",
"Fullscreen mode", "Left pane hidden", "Right pane hidden", "Unsupported"};
public static final String[] s_configuration_iwd2 = {
"Normal windows", "Party AI disabled", "", "", "", "Fullscreen mode", "",
"Console hidden", "Automap notes hidden"};
public static final String[] s_version_bg1 = {"Restrict XP to BG1 limit", "Restrict XP to TotSC limit"};
public static final String[] s_familiar_owner = {
"Party member 0", "Party member 1", "Party member 2", "Party member 3",
"Party member 4", "Party member 5"};
private StructHexViewer hexViewer;
public GamResource(ResourceEntry entry) throws Exception
{
super(entry);
}
// --------------------- Begin Interface HasAddRemovable ---------------------
@Override
public AddRemovable[] getAddRemovables() throws Exception
{
if (Profile.getEngine() == Profile.Engine.PST) {
// TODO: missing CRE resource when adding PartyNPC structures
return new AddRemovable[]{new Variable(), new JournalEntry(), new KillVariable()};
// return new AddRemovable[]{new Variable(), new JournalEntry(), new KillVariable(),
// new PartyNPC(), new NonPartyNPC()};
} else {
return new AddRemovable[]{new Variable(), new JournalEntry()};
// return new AddRemovable[]{new Variable(), new JournalEntry(), new PartyNPC(),
// new NonPartyNPC()};
}
}
@Override
public AddRemovable confirmAddEntry(AddRemovable entry) throws Exception
{
if (entry instanceof PartyNPC) {
int numPartyMembers = ((IsNumeric)getAttribute(GAM_NUM_PARTY_MEMBERS)).getValue();
if (numPartyMembers >= 6) {
int ret = JOptionPane.showConfirmDialog(getViewer(),
"This game supports only up to 6 active party members. " +
"Do you want to add a new entry?",
"Add new party member", JOptionPane.YES_NO_OPTION);
if (ret != JOptionPane.YES_OPTION) {
entry = null;
}
}
}
return entry;
}
@Override
public boolean confirmRemoveEntry(AddRemovable entry) throws Exception
{
return true;
}
// --------------------- End Interface HasAddRemovable ---------------------
// --------------------- Begin Interface HasViewerTabs ---------------------
@Override
public int getViewerTabCount()
{
return 2;
}
@Override
public String getViewerTabName(int index)
{
switch (index) {
case 0:
return StructViewer.TAB_VIEW;
case 1:
return StructViewer.TAB_RAW;
}
return null;
}
@Override
public JComponent getViewerTab(int index)
{
switch (index) {
case 0:
{
JScrollPane scroll = new JScrollPane(new Viewer(this));
scroll.setBorder(BorderFactory.createEmptyBorder());
return scroll;
}
case 1:
{
if (hexViewer == null) {
hexViewer = new StructHexViewer(this, new BasicColorMap(this, true));
}
return hexViewer;
}
}
return null;
}
@Override
public boolean viewerTabAddedBefore(int index)
{
return (index == 0);
}
// --------------------- End Interface HasViewerTabs ---------------------
// --------------------- Begin Interface Writeable ---------------------
@Override
public void write(OutputStream os) throws IOException
{
super.writeFlatList(os);
}
// --------------------- End Interface Writeable ---------------------
@Override
protected void viewerInitialized(StructViewer viewer)
{
viewer.addTabChangeListener(hexViewer);
}
@Override
protected void datatypeAdded(AddRemovable datatype)
{
updateOffsets();
if (hexViewer != null) {
hexViewer.dataModified();
}
}
@Override
protected void datatypeAddedInChild(AbstractStruct child, AddRemovable datatype)
{
updateOffsets();
if (hexViewer != null) {
hexViewer.dataModified();
}
}
@Override
protected void datatypeRemoved(AddRemovable datatype)
{
updateOffsets();
if (hexViewer != null) {
hexViewer.dataModified();
}
}
@Override
protected void datatypeRemovedInChild(AbstractStruct child, AddRemovable datatype)
{
updateOffsets();
if (hexViewer != null) {
hexViewer.dataModified();
}
}
@Override
public int read(ByteBuffer buffer, int offset) throws Exception
{
addField(new TextString(buffer, offset, 4, COMMON_SIGNATURE));
TextString version = new TextString(buffer, offset + 4, 4, COMMON_VERSION);
addField(version);
addField(new DecNumber(buffer, offset + 8, 4, GAM_GAME_TIME));
if (Profile.getEngine() == Profile.Engine.PST) {
addField(new Bitmap(buffer, offset + 12, 2, GAM_SELECTED_FORMATION, s_torment));
} else {
addField(new Bitmap(buffer, offset + 12, 2, GAM_SELECTED_FORMATION, s_formation));
}
for (int i = 0; i < 5; i++) {
addField(new DecNumber(buffer, offset + 14 + (i * 2), 2, String.format(GAM_FORMATION_BUTTON_FMT, i+1)));
}
addField(new DecNumber(buffer, offset + 24, 4, GAM_PARTY_GOLD));
addField(new DecNumber(buffer, offset + 28, 2, GAM_NUM_NPCS_IN_PARTY));
addField(new Flag(buffer, offset + 30, 2, GAM_WEATHER, s_weather));
SectionOffset offset_partynpc = new SectionOffset(buffer, offset + 32, GAM_OFFSET_PARTY_MEMBERS,
PartyNPC.class);
addField(offset_partynpc);
SectionCount count_partynpc = new SectionCount(buffer, offset + 36, 4, GAM_NUM_PARTY_MEMBERS,
PartyNPC.class);
addField(count_partynpc);
SectionOffset offset_unknown = new SectionOffset(buffer, offset + 40, GAM_OFFSET_UNUSED,
UnknownSection2.class);
addField(offset_unknown);
SectionCount count_unknown = new SectionCount(buffer, offset + 44, 4, GAM_NUM_UNUSED,
UnknownSection2.class);
addField(count_unknown);
SectionOffset offset_nonpartynpc = new SectionOffset(buffer, offset + 48, GAM_OFFSET_NON_PARTY_MEMBERS,
NonPartyNPC.class);
addField(offset_nonpartynpc);
SectionCount count_nonpartynpc = new SectionCount(buffer, offset + 52, 4, GAM_NUM_NON_PARTY_MEMBERS,
NonPartyNPC.class);
addField(count_nonpartynpc);
SectionOffset offset_global = new SectionOffset(buffer, offset + 56, GAM_OFFSET_GLOBAL_VARIABLES,
Variable.class);
addField(offset_global);
SectionCount count_global = new SectionCount(buffer, offset + 60, 4, GAM_NUM_GLOBAL_VARIABLES,
Variable.class);
addField(count_global);
addField(new ResourceRef(buffer, offset + 64, GAM_MASTER_AREA, "ARE"));
addField(new DecNumber(buffer, offset + 72, 4, GAM_CURRENT_LINK));
SectionCount count_journal = new SectionCount(buffer, offset + 76, 4, GAM_NUM_JOURNAL_ENTRIES,
JournalEntry.class);
addField(count_journal);
SectionOffset offset_journal = new SectionOffset(buffer, offset + 80, GAM_OFFSET_JOURNAL_ENTRIES,
JournalEntry.class);
addField(offset_journal);
SectionOffset offKillvariable = null, offFamiliar = null, offIWD2 = null, offIWD = null;
SectionOffset offLocation = null, offRubikon = null, offBestiary = null, offPocket = null;
SectionCount numKillVariable = null, numIWD2 = null, numIWD = null, numLocation = null, numPocket = null;
if (Profile.getEngine() == Profile.Engine.BG1) { // V1.1
addField(new DecNumber(buffer, offset + 84, 4, GAM_REPUTATION));
addField(new ResourceRef(buffer, offset + 88, GAM_CURRENT_AREA, "ARE"));
addField(new Flag(buffer, offset + 96, 4, GAM_CONFIGURATION, s_configuration_bg1));
addField(new Bitmap(buffer, offset + 100, 4, GAM_SAVE_VERSION, s_version_bg1));
addField(new Unknown(buffer, offset + 104, 76));
}
else if (Profile.getEngine() == Profile.Engine.IWD) { // V1.1
addField(new DecNumber(buffer, offset + 84, 4, GAM_REPUTATION));
addField(new ResourceRef(buffer, offset + 88, GAM_CURRENT_AREA, "ARE"));
addField(new Flag(buffer, offset + 96, 4, GAM_CONFIGURATION, s_configuration_iwd));
numIWD = new SectionCount(buffer, offset + 100, 4, GAM_NUM_UNKNOWN, UnknownSection3.class);
addField(numIWD);
offIWD = new SectionOffset(buffer, offset + 104, GAM_OFFSET_UNKNOWN, UnknownSection3.class);
addField(offIWD);
addField(new Unknown(buffer, offset + 108, 72));
}
else if (Profile.getEngine() == Profile.Engine.PST) { // V1.1
offRubikon = new SectionOffset(buffer, offset + 84, GAM_OFFSET_MODRON_MAZE, Unknown.class);
addField(offRubikon);
addField(new DecNumber(buffer, offset + 88, 4, GAM_REPUTATION));
addField(new ResourceRef(buffer, offset + 92, GAM_CURRENT_AREA, "ARE"));
offKillvariable = new SectionOffset(buffer, offset + 100, GAM_OFFSET_KILL_VARIABLES, KillVariable.class);
addField(offKillvariable);
numKillVariable = new SectionCount(buffer, offset + 104, 4, GAM_NUM_KILL_VARIABLES, KillVariable.class);
addField(numKillVariable);
offBestiary = new SectionOffset(buffer, offset + 108, GAM_OFFSET_BESTIARY, Unknown.class);
addField(offBestiary);
addField(new ResourceRef(buffer, offset + 112, GAM_CURRENT_AREA_2, "ARE"));
addField(new Unknown(buffer, offset + 120, 64));
}
else if (Profile.getEngine() == Profile.Engine.BG2 || Profile.isEnhancedEdition()) { // V2.0
addField(new DecNumber(buffer, offset + 84, 4, GAM_REPUTATION));
addField(new ResourceRef(buffer, offset + 88, GAM_CURRENT_AREA, "ARE"));
addField(new Flag(buffer, offset + 96, 4, GAM_CONFIGURATION, s_configuration));
addField(new DecNumber(buffer, offset + 100, 4, GAM_SAVE_VERSION));
offFamiliar = new SectionOffset(buffer, offset + 104, GAM_OFFSET_FAMILIAR_INFO, Familiar.class);
addField(offFamiliar);
offLocation = new SectionOffset(buffer, offset + 108, GAM_OFFSET_STORED_LOCATIONS, StoredLocation.class);
addField(offLocation);
numLocation = new SectionCount(buffer, offset + 112, 4, GAM_NUM_STORED_LOCATIONS, StoredLocation.class);
addField(numLocation);
addField(new DecNumber(buffer, offset + 116, 4, GAM_REAL_TIME));
offPocket = new SectionOffset(buffer, offset + 120, GAM_OFFSET_POCKET_PLANE_LOCATIONS, StoredLocation.class);
addField(offPocket);
numPocket = new SectionCount(buffer, offset + 124, 4, GAM_NUM_POCKET_PLANE_LOCATIONS, StoredLocation.class);
addField(numPocket);
if (Profile.isEnhancedEdition()) {
addField(new DecNumber(buffer, offset + 128, 4, GAM_ZOOM_LEVEL));
addField(new ResourceRef(buffer, offset + 132, GAM_RANDOM_ENCOUNTER_AREA, "ARE"));
addField(new ResourceRef(buffer, offset + 140, GAM_WORLDMAP, "WMP"));
addField(new TextString(buffer, offset + 148, 8, GAM_CAMPAIGN));
addField(new Bitmap(buffer, offset + 156, 4, GAM_FAMILIAR_OWNER, s_familiar_owner));
addField(new TextString(buffer, offset + 160, 20, GAM_ENCOUNTER_ENTRY));
} else {
addField(new Unknown(buffer, offset + 128, 52));
}
}
else if (Profile.getEngine() == Profile.Engine.IWD2) { // V2.2 (V1.1 & V2.0 in BIFF)
addField(new Unknown(buffer, offset + 84, 4));
addField(new ResourceRef(buffer, offset + 88, GAM_CURRENT_AREA, "ARE"));
addField(new Flag(buffer, offset + 96, 4, GAM_CONFIGURATION, s_configuration_iwd2));
numIWD2 = new SectionCount(buffer, offset + 100, 4, GAM_NUM_UNKNOWN, UnknownSection3.class);
addField(numIWD2);
offIWD2 = new SectionOffset(buffer, offset + 104, GAM_OFFSET_UNKNOWN, UnknownSection3.class);
addField(offIWD2);
addField(new Unknown(buffer, offset + 108, 72));
}
offset = offset_partynpc.getValue();
for (int i = 0; i < count_partynpc.getValue(); i++) {
PartyNPC npc = new PartyNPC(this, buffer, offset, i);
offset += npc.getSize();
addField(npc);
}
offset = offset_nonpartynpc.getValue();
for (int i = 0; i < count_nonpartynpc.getValue(); i++) {
NonPartyNPC npc = new NonPartyNPC(this, buffer, offset, i);
offset += npc.getSize();
addField(npc);
}
offset = offset_unknown.getValue();
if (offset > 0) {
for (int i = 0; i < count_unknown.getValue(); i++) {
addField(new UnknownSection2(this, buffer, offset + i * 20));
}
}
if (offRubikon != null) { // Torment
offset = offRubikon.getValue();
if (offset > 0) {
addField(new ModronMaze(this, buffer, offset));
offset += 1720;
}
}
offset = offset_global.getValue();
for (int i = 0; i < count_global.getValue(); i++) {
Variable var = new Variable(this, buffer, offset, i);
offset += var.getSize();
addField(var);
}
if (offKillvariable != null) { // Torment
offset = offKillvariable.getValue();
for (int i = 0; i < numKillVariable.getValue(); i++) {
KillVariable kvar = new KillVariable(this, buffer, offset, i);
offset += kvar.getSize();
addField(kvar);
}
}
offset = offset_journal.getValue();
for (int i = 0; i < count_journal.getValue(); i++) {
JournalEntry ent = new JournalEntry(this, buffer, offset, i);
offset += ent.getSize();
addField(ent);
}
if (offBestiary != null) { // Torment
offset = offBestiary.getValue();
if (offset > 0) {
addField(new Unknown(buffer, offset, 260, GAM_BESTIARY));
offset += 260;
}
}
if (offFamiliar != null) { // BG2
offset = offFamiliar.getValue();
if (offset > 0) {
Familiar familiar = new Familiar(this, buffer, offset);
offset += familiar.getSize();
addField(familiar);
}
}
if (offIWD2 != null && numIWD2 != null) { // Icewind2
// a leftover from BG2 Familiar Info structure?
if (numIWD2.getValue() > 0) {
offset = offIWD2.getValue();
for (int i = 0; i < numIWD2.getValue(); i++) {
UnknownSection3 unknown = new UnknownSection3(this, buffer, offset);
offset += unknown.getSize();
addField(unknown);
}
HexNumber offEOS = new HexNumber(buffer, offset, 4, GAM_OFFSET_END_OF_UNKNOWN_STRUCTURE);
addField(offEOS);
offset += 4;
int unknownSize = (offEOS.getValue() > buffer.limit() - 4) ?
buffer.limit() - offset - 4 : offEOS.getValue() - offset;
addField(new Unknown(buffer, offset, unknownSize, GAM_UNKNOWN_STRUCTURE));
offset += unknownSize;
addField(new Unknown(buffer, offset, 4));
offset += 4;
}
}
if (numIWD != null && offIWD != null) { // Icewind
// a leftover from BG2 Familiar Info structure?
if (numIWD.getValue() > 0) {
offset = offIWD.getValue();
for (int i = 0; i < numIWD.getValue(); i++) {
UnknownSection3 unknown = new UnknownSection3(this, buffer, offset);
offset += unknown.getSize();
addField(unknown);
}
HexNumber offEOS = new HexNumber(buffer, offset, 4, GAM_OFFSET_END_OF_UNKNOWN_STRUCTURE);
addField(offEOS);
offset += 4;
int unknownSize = offEOS.getValue() > buffer.limit() ? buffer.limit() - offset : offEOS.getValue() - offset;
addField(new Unknown(buffer, offset, unknownSize, GAM_UNKNOWN_STRUCTURE));
offset += unknownSize;
}
}
if (offLocation != null && numLocation != null) { // BG2?
offset = offLocation.getValue();
if (offset > 0) {
for (int i = 0; i < numLocation.getValue(); i++) {
StoredLocation location = new StoredLocation(this, buffer, offset, i);
offset += location.getSize();
addField(location);
}
}
}
if (offPocket != null && numPocket != null) { // BG2
offset = offPocket.getValue();
if (offset > 0) {
for (int i = 0; i < numPocket.getValue(); i++) {
StoredLocation location = new StoredLocation(this, GAM_POCKET_PLANE, buffer, offset, i);
offset += location.getSize();
addField(location);
}
}
}
if (offset == 0) {
offset = getField(getFieldCount() - 1).getOffset() + getField(getFieldCount() - 1).getSize();
}
return offset;
}
private void updateOffsets()
{
for (int i = 0; i < getFieldCount(); i++) {
Object o = getField(i);
if (o instanceof PartyNPC) {
((PartyNPC)o).updateCREOffset();
}
// if (o instanceof Familiar) {
// ((Familiar)o).updateFilesize((DecNumber)getAttribute("File size"));
// }
}
}
}