diff --git a/core/src/com/riiablo/save/CharData.java b/core/src/com/riiablo/save/CharData.java new file mode 100644 index 00000000..7a9a1c38 --- /dev/null +++ b/core/src/com/riiablo/save/CharData.java @@ -0,0 +1,512 @@ +package com.riiablo.save; + +import com.badlogic.gdx.Input; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.IntArray; +import com.badlogic.gdx.utils.IntIntMap; +import com.riiablo.CharacterClass; +import com.riiablo.Riiablo; +import com.riiablo.codec.excel.DifficultyLevels; +import com.riiablo.item.Attributes; +import com.riiablo.item.BodyLoc; +import com.riiablo.item.Item; +import com.riiablo.item.Location; +import com.riiablo.item.PropertyList; +import com.riiablo.item.Stat; +import com.riiablo.item.StoreLoc; +import com.riiablo.item.Type; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.Validate; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +public class CharData { + private static final int attack = 0; + private static final int kick = 1; + private static final int throw_ = 2; + private static final int unsummon = 3; + private static final int left_hand_throw = 4; + private static final int left_hand_swing = 5; + private static final int scroll_of_identify = 217; + private static final int book_of_identify = 218; + private static final int scroll_of_townportal = 219; + private static final int book_of_townportal = 220; + + private static final IntIntMap defaultSkills = new IntIntMap(); + static { + defaultSkills.put(attack, 1); + defaultSkills.put(kick, 1); + //defaultSkills.put(throw_, 1); + defaultSkills.put(unsummon, 1); + //defaultSkills.put(left_hand_throw, 1); + defaultSkills.put(left_hand_swing, 1); + } + + public String name; + public byte charClass; + public int alternate; + public int flags; + public byte level; + public final int hotkeys[] = new int[D2S.NUM_HOTKEYS]; + public final int actions[][] = new int[D2S.NUM_ACTIONS][D2S.NUM_BUTTONS]; + public final byte towns[] = new byte[D2S.NUM_DIFFS]; + public int mapSeed; + public final byte realmData[] = new byte[144]; + + final MercData mercData = new MercData(); + final short questData[][][] = new short[Riiablo.MAX_DIFFS][Riiablo.MAX_ACTS][8]; + final int waypointData[][] = new int[Riiablo.MAX_DIFFS][Riiablo.MAX_ACTS]; + final long npcIntroData[] = new long[Riiablo.MAX_DIFFS]; + final long npcReturnData[] = new long[Riiablo.MAX_DIFFS]; + final Attributes statData = new Attributes(); + final IntIntMap skillData = new IntIntMap(); + final ItemData itemData = new ItemData(); + Item golemItemData; + + public int diff; + public boolean managed; + public CharacterClass classId; + + private byte[] data; // TODO: replace this reference with D2S.serialize(CharData) + + final IntIntMap skills = new IntIntMap(); + final Array chargedSkills = new Array<>(false, 16); + final Array skillListeners = new Array<>(false, 16); + + public static CharData loadFromD2S(int diff, D2S d2s) { + CharData charData = new CharData(diff, true).load(d2s); + if (d2s.file != null) charData.data = d2s.file.readBytes(); + return charData; + } + + public static CharData loadFromBuffer(int diff, ByteBuffer buffer) { + D2S d2s = D2S.loadFromBuffer(buffer, true); + return new CharData(diff, false).load(d2s); + } + + /** + * @param managed whether or not this data is backed by a file + */ + private CharData(int diff, boolean managed) { + this.diff = diff; + this.managed = managed; + } + + // created via new character or external player + public CharData(int diff, boolean managed, String name, byte charClass) { + this(diff, managed); + this.name = name; + this.charClass = charClass; + classId = CharacterClass.get(charClass); + alternate = D2S.PRIMARY; + flags = D2S.FLAG_EXPANSION; + level = 1; + Arrays.fill(hotkeys, D2S.HOTKEY_UNASSIGNED); + for (int[] actions : actions) Arrays.fill(actions, 0); + // TODO: check and set town against saved town + mapSeed = 0; + } + + public CharData load(D2S d2s) { + d2s.copyTo(this); + preprocessItems(); + return this; + } + + private void preprocessItems() { + itemData.preprocessItems(); + mercData.itemData.preprocessItems(); + } + + public void reset() { + softReset(); + name = null; + charClass = -1; + classId = null; + alternate = 0; + flags = 0; + level = 0; + Arrays.fill(hotkeys, D2S.HOTKEY_UNASSIGNED); + for (int i = 0, s = D2S.NUM_ACTIONS; i < s; i++) Arrays.fill(actions[i], 0); + Arrays.fill(towns, (byte) 0); + mapSeed = 0; + Arrays.fill(realmData, (byte) 0); + + mercData.flags = 0; + mercData.seed = 0; + mercData.name = 0; + mercData.type = 0; + mercData.xp = 0; + + for (int i = 0, i0 = Riiablo.MAX_DIFFS; i < i0; i++) { + for (int a = 0; a < Riiablo.MAX_ACTS; a++) Arrays.fill(questData[i][a], (short) 0); + Arrays.fill(waypointData[i], 0); + npcIntroData[i] = 0; + npcReturnData[i] = 0; + } + } + + void softReset() { + statData.base().clear(); + statData.reset(); + skillData.clear(); + itemData.clear(); + mercData.itemData.clear(); + golemItemData = null; + + skills.clear(); + chargedSkills.clear(); + skillListeners.clear(); + } + + public boolean isManaged() { + return managed; + } + + public byte[] serialize() { + Validate.isTrue(isManaged(), "Cannot serialize unmanaged data"); // TODO: replace temp check with D2S.serialize(CharData) + return ArrayUtils.nullToEmpty(data); + } + + public int getHotkey(int button, int skill) { + return ArrayUtils.indexOf(hotkeys, button == Input.Buttons.LEFT ? skill | D2S.HOTKEY_LEFT_MASK : skill); + } + + public void setHotkey(int button, int skill, int index) { + hotkeys[index] = button == Input.Buttons.LEFT ? skill | D2S.HOTKEY_LEFT_MASK : skill; + } + + public int getAction(int button) { + return getAction(alternate, button); + } + + public int getAction(int alternate, int button) { + return actions[alternate][button]; + } + + public void setAction(int alternate, int button, int skill) { + actions[alternate][button] = skill; + } + + public boolean hasMerc() { + return mercData.seed != 0; + } + + public MercData getMerc() { + return mercData; + } + + public short[] getQuests(int act) { + return questData[diff][act]; + } + + public int getWaypoints(int act) { + return waypointData[diff][act]; + } + + public long getNpcIntro() { + return npcIntroData[diff]; + } + + public long getNpcReturn() { + return npcReturnData[diff]; + } + + public boolean hasGolemItem() { + return golemItemData != null; + } + + public Item getGolemItem() { + return golemItemData; + } + + public Attributes getStats() { + return statData; + } + + public void updateStats() { + DifficultyLevels.Entry diff = Riiablo.files.DifficultyLevels.get(this.diff); + PropertyList base = statData.base(); + base.put(Stat.armorclass, 0); + base.put(Stat.damageresist, 0); + base.put(Stat.magicresist, 0); + base.put(Stat.fireresist, diff.ResistPenalty); + base.put(Stat.lightresist, diff.ResistPenalty); + base.put(Stat.coldresist, diff.ResistPenalty); + base.put(Stat.poisonresist, diff.ResistPenalty); + base.put(Stat.maxfireresist, 75); + base.put(Stat.maxlightresist, 75); + base.put(Stat.maxcoldresist, 75); + base.put(Stat.maxpoisonresist, 75); + + statData.reset(); + int[] equippedItems = itemData.equipped.values(); + for (int i = 0, s = equippedItems.length, j; i < s; i++) { + j = equippedItems[i]; + if (j == ItemData.INVALID_ITEM) continue; + Item item = itemData.getItem(j); + if (isActive(item)) { + statData.add(item.props.remaining()); + Stat stat; + if ((stat = item.props.get(Stat.armorclass)) != null) { + statData.aggregate().addCopy(stat); + } + } + } + IntArray inventoryItems = itemData.getStore(StoreLoc.INVENTORY); + int[] inventoryItemsCache = inventoryItems.items; + for (int i = 0, s = inventoryItems.size, j; i < s; i++) { + j = inventoryItemsCache[i]; + if (j == ItemData.INVALID_ITEM) continue; + Item item = itemData.getItem(j); + if (item.type.is(Type.CHAR)) { + statData.add(item.props.remaining()); + } + } +// statData.update(this); // TODO: uncomment + + // FIXME: This corrects a mismatch between max and current, algorithm should be tested later for correctness in other cases + statData.get(Stat.maxstamina).set(statData.get(Stat.stamina)); + statData.get(Stat.maxhp).set(statData.get(Stat.hitpoints)); + statData.get(Stat.maxmana).set(statData.get(Stat.mana)); + + // This appears to be hard-coded in the original client + int dex = statData.get(Stat.dexterity).value(); + Stat armorclass = statData.get(Stat.armorclass); + armorclass.add(dex / 4); + armorclass.modified = false; + + skills.clear(); + skills.putAll(skillData); + skills.putAll(defaultSkills); + Item LARM = getEquipped(BodyLoc.LARM); + Item RARM = getEquipped(BodyLoc.RARM); + if ((LARM != null && LARM.typeEntry.Throwable) + || (RARM != null && RARM.typeEntry.Throwable)) { + skills.put(throw_, 1); + if (classId == CharacterClass.BARBARIAN) { + skills.put(left_hand_throw, 1); + } + } + for (int i = 0, s = inventoryItems.size; i < s; i++) { + Item item = itemData.getItem(inventoryItemsCache[i]); + if (item.type.is(Type.BOOK) || item.type.is(Type.SCRO)) { + if (item.base.code.equalsIgnoreCase("ibk")) { + skills.getAndIncrement(book_of_identify, 0, item.props.get(Stat.quantity).value()); + } else if (item.base.code.equalsIgnoreCase("isc")) { + skills.getAndIncrement(scroll_of_identify, 0, 1); + } else if (item.base.code.equalsIgnoreCase("tbk")) { + skills.getAndIncrement(book_of_townportal, 0, item.props.get(Stat.quantity).value()); + } else if (item.base.code.equalsIgnoreCase("tsc")) { + skills.getAndIncrement(scroll_of_townportal, 0, 1); + } + } + } + + chargedSkills.clear(); + for (Stat stat : statData.remaining()) { + switch (stat.id) { + case Stat.item_nonclassskill: + skills.getAndIncrement(stat.param(), 0, stat.value()); + break; + case Stat.item_charged_skill: + chargedSkills.add(stat); + break; + default: + // do nothing + } + } + } + + public int getSkill(int skill) { + return skills.get(skill, 0); + } + + public ItemData getItems() { + return itemData; + } + + public Item getItem(int i) { + return itemData.getItem(i); + } + + public Item getCursor() { + return itemData.getCursor(); + } + + public Item getSlot(BodyLoc bodyLoc) { + return itemData.getSlot(bodyLoc); + } + + public Item getEquipped(BodyLoc bodyLoc) { + return getEquipped(bodyLoc, alternate); + } + + public Item getEquipped(BodyLoc bodyLoc, int alternate) { + return itemData.getEquipped(bodyLoc, alternate); + } + + public boolean isActive(Item item) { + if (item == null) return false; + return item.bodyLoc == BodyLoc.getAlternate(item.bodyLoc, alternate); + } + + public void itemToCursor(int i) { + assert itemData.cursor == ItemData.INVALID_ITEM; + itemData.cursor = i; + Item item = itemData.getItem(i); + item.location = Location.CURSOR; + } + + public void locationToCursor(int i) { + itemToCursor(i); + } + + public void cursorToLocation(StoreLoc storeLoc, int x, int y) { + assert itemData.cursor != ItemData.INVALID_ITEM; + Item item = itemData.getItem(itemData.cursor); + item.location = Location.STORED; + item.storeLoc = storeLoc; + item.gridX = (byte) x; + item.gridY = (byte) y; + itemData.cursor = ItemData.INVALID_ITEM; + } + + public void swapLocationItem(int i, StoreLoc storeLoc, int x, int y) { + cursorToLocation(storeLoc, x, y); + locationToCursor(i); + } + + public void bodyToCursor(BodyLoc bodyLoc) { + bodyToCursor(bodyLoc, false); + } + + public void cursorToBody(BodyLoc bodyLoc) { + cursorToBody(bodyLoc, false); + } + + public void swapBodyItem(BodyLoc bodyLoc) { + swapBodyItem(bodyLoc, false); + } + + public void bodyToCursor(BodyLoc bodyLoc, boolean merc) { + assert itemData.cursor == ItemData.INVALID_ITEM; + Item item; + if (merc) { + int i = mercData.itemData.equipped.get(bodyLoc); + itemData.cursor = itemData.add(item = mercData.itemData.remove(i)); + mercData.itemData.equipped.remove(item.bodyLoc); + } else { + itemData.cursor = itemData.equipped.get(bodyLoc); + item = itemData.getItem(itemData.cursor); + itemData.equipped.put(item.bodyLoc, ItemData.INVALID_ITEM); + } + item.location = Location.CURSOR; + } + + public void cursorToBody(BodyLoc bodyLoc, boolean merc) { + assert itemData.cursor != ItemData.INVALID_ITEM; + Item item = itemData.getItem(itemData.cursor); + item.location = Location.EQUIPPED; + item.bodyLoc = bodyLoc; + if (merc) { + itemData.remove(itemData.cursor); + int i = mercData.itemData.add(item); + mercData.itemData.equipped.put(bodyLoc, i); + } else { + itemData.equipped.put(bodyLoc, itemData.cursor); + } + itemData.cursor = ItemData.INVALID_ITEM; + } + + public void swapBodyItem(BodyLoc bodyLoc, boolean merc) { + assert itemData.cursor != ItemData.INVALID_ITEM; + int oldCursor = itemData.cursor; + itemData.cursor = ItemData.INVALID_ITEM; + bodyToCursor(bodyLoc, merc); + int newCursor = itemData.cursor; + itemData.cursor = oldCursor; + cursorToBody(bodyLoc, merc); + itemData.cursor = newCursor; + } + + public void beltToCursor(int i) { + itemToCursor(i); + } + + public void cursorToBelt(int x, int y) { + assert itemData.cursor != ItemData.INVALID_ITEM; + Item item = itemData.getItem(itemData.cursor); + item.location = Location.BELT; + item.gridX = (byte) x; + item.gridY = (byte) y; + itemData.cursor = ItemData.INVALID_ITEM; + } + + public void swapBeltItem(int i) { + assert itemData.cursor != ItemData.INVALID_ITEM; + int oldCursor = itemData.cursor; + itemData.cursor = ItemData.INVALID_ITEM; + beltToCursor(i); + int newCursor = itemData.cursor; + itemData.cursor = oldCursor; + Item oldItem = itemData.getItem(oldCursor); + cursorToBelt(oldItem.gridX, oldItem.gridY); + itemData.cursor = newCursor; + } + + public int getAlternate() { + return alternate; + } + + public void setAlternate(int alternate) { + if (this.alternate != alternate) { + this.alternate = alternate; + Item LH = getEquipped(BodyLoc.LARM); + Item RH = getEquipped(BodyLoc.RARM); + updateStats(); + itemData.notifyEquipmentAlternated(alternate, LH, RH); + } + } + + public int alternate() { + int alt = getAlternate() > D2S.PRIMARY ? D2S.PRIMARY : D2S.SECONDARY; + setAlternate(alt); + return alt; + } + + public static class MercData { + public int flags; + public int seed; + public short name; + public short type; + public int xp; + + public final ItemData itemData = new ItemData(); + + public String getName() { + return String.format("0x%04X", name); + } + } + + public void clearListeners() { + itemData.equipListeners.clear(); + mercData.itemData.equipListeners.clear(); + skillListeners.clear(); + } + + public boolean addSkillListener(SkillListener l) { + skillListeners.add(l); + return true; + } + + private void notifySkillChanged(IntIntMap skills, Array chargedSkills) { + for (SkillListener l : skillListeners) l.onChanged(this, skills, chargedSkills); + } + + public interface SkillListener { + void onChanged(CharData client, IntIntMap skills, Array chargedSkills); + } +} diff --git a/core/src/com/riiablo/save/D2S.java b/core/src/com/riiablo/save/D2S.java index 2f1495f5..67fdb95f 100644 --- a/core/src/com/riiablo/save/D2S.java +++ b/core/src/com/riiablo/save/D2S.java @@ -6,11 +6,14 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.GdxRuntimeException; +import com.riiablo.CharacterClass; import com.riiablo.Riiablo; import com.riiablo.codec.COF; import com.riiablo.codec.util.BitStream; import com.riiablo.item.Item; import com.riiablo.item.Location; +import com.riiablo.item.PropertyList; +import com.riiablo.item.Stat; import com.riiablo.util.BufferUtils; import com.riiablo.util.DebugUtils; @@ -143,6 +146,78 @@ public class D2S { return d2s; } + CharData copyTo(CharData data) { + data.softReset(); + data.name = header.name; + data.charClass = header.charClass; + data.classId = CharacterClass.get(header.charClass); + data.alternate = header.alternate; + data.flags = header.flags; + data.level = header.level; + System.arraycopy(header.hotkeys, 0, data.hotkeys, 0, D2S.NUM_HOTKEYS); + for (int i = 0, s = D2S.NUM_ACTIONS; i < s; i++) System.arraycopy(header.actions[i], 0, data.actions[i], 0, D2S.NUM_BUTTONS); + System.arraycopy(header.towns, 0, data.towns, 0, D2S.NUM_DIFFS); + data.mapSeed = header.mapSeed; + System.arraycopy(header.realmData, 0, data.realmData, 0, header.realmData.length); + + data.mercData.flags = header.merc.flags; + data.mercData.seed = header.merc.seed; + data.mercData.name = header.merc.name; + data.mercData.type = header.merc.type; + data.mercData.xp = header.merc.xp; + data.mercData.itemData.clear(); + if (header.merc.seed != 0) data.mercData.itemData.addAll(header.merc.items.items.items); + + BitStream bitStream; + for (int i = 0, i0 = Riiablo.MAX_DIFFS; i < i0; i++) { + bitStream = new BitStream(quests.data[i]); + for (int q = 0, q0 = 8; q < q0; q++) data.questData[i][0][q] = (short) bitStream.readUnsigned31OrLess(16); + for (int q = 0, q0 = 8; q < q0; q++) data.questData[i][1][q] = (short) bitStream.readUnsigned31OrLess(16); + for (int q = 0, q0 = 8; q < q0; q++) data.questData[i][2][q] = (short) bitStream.readUnsigned31OrLess(16); + for (int q = 0, q0 = 8; q < q0; q++) data.questData[i][3][q] = (short) bitStream.readUnsigned31OrLess(16); + for (int q = 0, q0 = 8; q < q0; q++) data.questData[i][4][q] = (short) bitStream.readUnsigned31OrLess(16); + + bitStream = new BitStream(waypoints.diff[i].data); + data.waypointData[i][0] = bitStream.readUnsigned31OrLess(9); + data.waypointData[i][1] = bitStream.readUnsigned31OrLess(9); + data.waypointData[i][2] = bitStream.readUnsigned31OrLess(9); + data.waypointData[i][3] = bitStream.readUnsigned31OrLess(3); + data.waypointData[i][4] = bitStream.readUnsigned31OrLess(9); + + bitStream = new BitStream(npcs.data[NPCData.GREETING_INTRO][i]); + data.npcIntroData[i] = bitStream.readUnsigned(64); + bitStream = new BitStream(npcs.data[NPCData.GREETING_RETURN][i]); + data.npcReturnData[i] = bitStream.readUnsigned(64); + } + + PropertyList base = data.statData.base(); + base.put(Stat.strength, stats.strength); + base.put(Stat.energy, stats.energy); + base.put(Stat.dexterity, stats.dexterity); + base.put(Stat.vitality, stats.vitality); + base.put(Stat.statpts, stats.statpts); + base.put(Stat.newskills, stats.newskills); + base.put(Stat.hitpoints, stats.hitpoints); + base.put(Stat.maxhp, stats.maxhp); + base.put(Stat.mana, stats.mana); + base.put(Stat.maxmana, stats.maxmana); + base.put(Stat.stamina, stats.stamina); + base.put(Stat.maxstamina, stats.maxstamina); + base.put(Stat.level, stats.level); + base.put(Stat.experience, (int) stats.experience); + base.put(Stat.gold, stats.gold); + base.put(Stat.goldbank, stats.goldbank); + + for (int spellId = data.classId.firstSpell, s = data.classId.lastSpell, i = 0; spellId < s; spellId++, i++) { + data.skillData.put(spellId, skills.data[i]); + } + + data.itemData.clear(); + data.itemData.addAll(items.items); + data.golemItemData = golem.item; + return data; + } + public static class Header { static final int SIZE = 0x14F; diff --git a/core/src/com/riiablo/save/EquipAdapter.java b/core/src/com/riiablo/save/EquipAdapter.java new file mode 100644 index 00000000..dc577e97 --- /dev/null +++ b/core/src/com/riiablo/save/EquipAdapter.java @@ -0,0 +1,9 @@ +package com.riiablo.save; + +import com.riiablo.item.BodyLoc; +import com.riiablo.item.Item; + +public class EquipAdapter implements ItemData.EquipListener { + @Override public void onChanged(ItemData items, BodyLoc bodyLoc, Item oldItem, Item item) {} + @Override public void onAlternated(ItemData items, int alternate, Item LH, Item RH) {} +} diff --git a/core/src/com/riiablo/save/ItemData.java b/core/src/com/riiablo/save/ItemData.java new file mode 100644 index 00000000..4c46cfc7 --- /dev/null +++ b/core/src/com/riiablo/save/ItemData.java @@ -0,0 +1,161 @@ +package com.riiablo.save; + +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.IntArray; +import com.badlogic.gdx.utils.IntIntMap; +import com.riiablo.Riiablo; +import com.riiablo.codec.excel.SetItems; +import com.riiablo.item.BodyLoc; +import com.riiablo.item.Item; +import com.riiablo.item.Location; +import com.riiablo.item.Quality; +import com.riiablo.item.StoreLoc; +import com.riiablo.util.EnumIntMap; + +public class ItemData { + public static final int INVALID_ITEM = -1; + + final Array itemData = new Array<>(Item.class); + + int cursor; + + final EnumIntMap equipped = new EnumIntMap<>(BodyLoc.class, INVALID_ITEM); + final Array equipListeners = new Array<>(false, 16); + + final IntIntMap equippedSets = new IntIntMap(); // Indexed using set id + final IntIntMap setItemsOwned = new IntIntMap(); // Indexed using set item id + + public void clear() { + cursor = INVALID_ITEM; + itemData.clear(); + equipped.clear(); + equipListeners.clear(); + equippedSets.clear(); + setItemsOwned.clear(); + } + + void preprocessItems() { + cursor = ItemData.INVALID_ITEM; + Item[] items = itemData.items; + for (int i = 0, s = itemData.size; i < s; i++) { + Item item = items[i]; + switch (item.location) { + case EQUIPPED: + equipped.put(item.bodyLoc, i); + break; + case BELT: + item.gridY = (byte) -(item.gridX >>> 2); + item.gridX &= 0x3; + break; + case CURSOR: + assert cursor == INVALID_ITEM : "Only one item should be marked as cursor"; + cursor = i; + break; + case STORED: + case UNK3: + case UNK5: + case SOCKET: + default: + } + if (item.quality == Quality.SET) setItemsOwned.getAndIncrement(item.qualityId, 0, 1); + } + } + + public Item getItem(int i) { + return itemData.get(i); + } + + public Item getCursor() { + return cursor == INVALID_ITEM ? null : getItem(cursor); + } + + public Item getSlot(BodyLoc bodyLoc) { + int i = equipped.get(bodyLoc); + return i == ItemData.INVALID_ITEM ? null : getItem(i); + } + + public Item getEquipped(BodyLoc bodyLoc, int alternate) { + return getSlot(BodyLoc.getAlternate(bodyLoc, alternate)); + } + + public int add(Item item) { + int i = itemData.size; + itemData.add(item); + if (item.quality == Quality.SET) setItemsOwned.getAndIncrement(item.qualityId, 0, 1); + return i; + } + + public Item remove(int i) { + Item item = getItem(i); + itemData.removeIndex(i); + int[] vals = equipped.values(); + for (int j = 0, s = vals.length; j < s; j++) if (vals[j] > i) vals[j]--; + if (item.quality == Quality.SET) setItemsOwned.getAndIncrement(item.qualityId, 0, -1); + return item; + } + + public void addAll(Array items) { + itemData.addAll(items); + } + + public IntArray getLocation(Location location) { + return getLocation(location, StoreLoc.NONE); + } + + public IntArray getLocation(Location location, StoreLoc storeLoc) { + Item[] items = itemData.items; + IntArray copy = new IntArray(items.length); + for (int i = 0, s = itemData.size; i < s; i++) { + Item item = items[i]; + if (item.location != location) continue; + if (location == Location.STORED && item.storeLoc != storeLoc) continue; + copy.add(i); + } + + return copy; + } + + public IntArray getStore(StoreLoc storeLoc) { + return getLocation(Location.STORED, storeLoc); + } + +// public Item equip(BodyLoc bodyLoc, Item item) { +// Item oldItem = equipped.put(bodyLoc, item); +//// if (item != null) item.update(this); +// updateSets(oldItem, item); +// updateStats(); +// notifyEquipmentChanged(bodyLoc, oldItem, item); +// return oldItem; +// } + + private void updateSets(Item oldItem, Item item) { + if (oldItem != null && oldItem.quality == Quality.SET) { + SetItems.Entry setItem = (SetItems.Entry) oldItem.qualityData; + int id = Riiablo.files.Sets.index(setItem.set); + equippedSets.getAndIncrement(id, 0, -1); + } + if (item != null && item.quality == Quality.SET) { + SetItems.Entry setItem = (SetItems.Entry) item.qualityData; + int id = Riiablo.files.Sets.index(setItem.set); + equippedSets.getAndIncrement(id, 0, 1); + } + } + + public boolean addEquipmentListener(EquipListener l) { + equipListeners.add(l); + return true; + } + + void notifyEquipmentChanged(BodyLoc bodyLoc, Item oldItem, Item item) { + for (EquipListener l : equipListeners) l.onChanged(this, bodyLoc, oldItem, item); + } + + void notifyEquipmentAlternated(int alternate, Item LH, Item RH) { + for (EquipListener l : equipListeners) l.onAlternated(this, alternate, LH, RH); + } + + public interface EquipListener { + void onChanged(ItemData items, BodyLoc bodyLoc, Item oldItem, Item item); + void onAlternated(ItemData items, int alternate, Item LH, Item RH); + } +} diff --git a/core/src/com/riiablo/util/EnumIntMap.java b/core/src/com/riiablo/util/EnumIntMap.java new file mode 100644 index 00000000..e3536ed1 --- /dev/null +++ b/core/src/com/riiablo/util/EnumIntMap.java @@ -0,0 +1,148 @@ +package com.riiablo.util; + +import com.badlogic.gdx.utils.Collections; +import com.badlogic.gdx.utils.GdxRuntimeException; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +public class EnumIntMap> implements Iterable> { + private final Class keyType; + private final int defaultValue; + + private K[] keyUniverse; + private int[] vals; + + private Entries entries1, entries2; + + public EnumIntMap(Class keyType, int defaultValue) { + this.keyType = keyType; + this.defaultValue = defaultValue; + keyUniverse = keyType.getEnumConstants(); + vals = new int[keyUniverse.length]; + Arrays.fill(vals, defaultValue); + } + + public void clear() { + Arrays.fill(vals, defaultValue); + } + + public int get(K key) { + return vals[key.ordinal()]; + } + + public int remove(K key) { + return put(key, defaultValue); + } + + public int put(K key, int value) { + int i = key.ordinal(); + int val = vals[i]; + vals[i] = value; + return val; + } + + public int[] values() { + return vals; + } + + @Override + public Entries iterator() { + return entries(); + } + + public Entries entries () { + if (Collections.allocateIterators) return new Entries<>(this); + if (entries1 == null) { + entries1 = new Entries<>(this); + entries2 = new Entries<>(this); + } + if (!entries1.valid) { + entries1.reset(); + entries1.valid = true; + entries2.valid = false; + return entries1; + } + entries2.reset(); + entries2.valid = true; + entries1.valid = false; + return entries2; + } + + public static class Entry> { + public K key; + public int value; + + public String toString () { + return key + "=" + value; + } + } + + private static class MapIterator> { + public boolean hasNext; + + final EnumIntMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator(EnumIntMap map) { + this.map = map; + reset(); + } + + public void reset() { + currentIndex = -1; + nextIndex = -1; + findNextIndex(); + } + + void findNextIndex() { + hasNext = false; + int[] vals = map.vals; + int defaultValue = map.defaultValue; + for (int n = map.keyUniverse.length; ++nextIndex < n;) { + if (vals[nextIndex] != defaultValue) { + hasNext = true; + break; + } + } + } + + public void remove() { + throw new UnsupportedOperationException("#remove() not supported."); + } + } + + public static class Entries> extends EnumIntMap.MapIterator implements Iterable>, Iterator> { + private Entry entry = new Entry<>(); + + public Entries(EnumIntMap map) { + super(map); + } + + /** Note the same entry instance is returned each time this method is called. */ + public Entry next() { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new GdxRuntimeException("#iterator() cannot be used nested."); + entry.key = map.keyUniverse[nextIndex]; + entry.value = map.vals[nextIndex]; + currentIndex = nextIndex; + findNextIndex(); + return entry; + } + + public boolean hasNext() { + if (!valid) throw new GdxRuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public Entries iterator() { + return this; + } + + public void remove() { + super.remove(); + } + } +}