diff --git a/src/main/java/resaver/ess/ChangeFormExtraDataData.java b/src/main/java/resaver/ess/ChangeFormExtraDataData.java index 20a4989..abc2b7f 100644 --- a/src/main/java/resaver/ess/ChangeFormExtraDataData.java +++ b/src/main/java/resaver/ess/ChangeFormExtraDataData.java @@ -69,7 +69,23 @@ public ChangeFormExtraDataData(ByteBuffer input, ESS.ESSContext context) throws super.readElement(input, "SUB_DATA2", in -> new ChangeFormExtraDataData(in, context)); super.readElement(input, "SUB_DATA3", in -> new ChangeFormExtraDataData(in, context)); break; - //case 16: + case 16: + this.NAME = "ExtraExtraData4"; + this.BRIEF = true; + super.readElement(input, "SUB_DATA1", in -> new ChangeFormExtraDataData(in, context)); + super.readElement(input, "SUB_DATA2", in -> new ChangeFormExtraDataData(in, context)); + super.readElement(input, "SUB_DATA3", in -> new ChangeFormExtraDataData(in, context)); + super.readElement(input, "SUB_DATA4", in -> new ChangeFormExtraDataData(in, context)); + break; + case 20: + this.NAME = "ExtraExtraData5"; + this.BRIEF = true; + super.readElement(input, "SUB_DATA1", in -> new ChangeFormExtraDataData(in, context)); + super.readElement(input, "SUB_DATA2", in -> new ChangeFormExtraDataData(in, context)); + super.readElement(input, "SUB_DATA3", in -> new ChangeFormExtraDataData(in, context)); + super.readElement(input, "SUB_DATA4", in -> new ChangeFormExtraDataData(in, context)); + super.readElement(input, "SUB_DATA5", in -> new ChangeFormExtraDataData(in, context)); + break; case 22: this.NAME = "Worn"; this.BRIEF = true; @@ -364,12 +380,12 @@ public ChangeFormExtraDataData(ByteBuffer input, ESS.ESSContext context) throws super.readRefID(input, "REF", context); break; case 135: - this.NAME = "TEST"; - RefID r1 = super.readRefID(input, "R1", context); - RefID r2 = super.readRefID(input, "R2", context); - RefID r3 = super.readRefID(input, "R3", context); - RefID r4 = super.readRefID(input, "R4", context); - this.BRIEF = true; + this.NAME = "FollowerSwimBreadcrumbs"; + this.BRIEF = false; + super.readFloats(input, "UNK1", 3); + super.readRefID(input, "REF", context); + super.readInt(input, "UNK2"); + super.readVSElemArray(input, "BREADCRUMBS", in -> new FollowerSwimBreadcrumb(in, context)); break; case 136: this.NAME = "AliasInstanceArray"; @@ -498,6 +514,16 @@ protected String toStringStructured(String name, int level) { final public String NAME; final private boolean BRIEF; + static class FollowerSwimBreadcrumb extends GeneralElement { + FollowerSwimBreadcrumb(ByteBuffer input, ESS.ESSContext context) throws ElementException { + super.readFloats(input, "UNK1", 3); + super.readRefID(input, "REF1", context); + super.readFloats(input, "UNK2", 3); + super.readRefID(input, "REF2", context); + super.readByte(input, "UNK3"); + } + } + static class AliasInstance extends GeneralElement { AliasInstance(ByteBuffer input, ESS.ESSContext context) throws ElementException { @@ -607,7 +633,12 @@ static class SayToTopicInfo extends GeneralElement { super.readByte(input, "UNK1"); super.readInt(input, "UNK2"); super.readRefID(input, "REF2", context); - super.readElement(input, "DATA2", in -> new SayToTopicInfoData2(in, context)); + // type 113 carries a SayToTopicInfoData = vsval count + SayToTopicInfoData2[count] + // + uint16 + refID[4] (UESP). Stock reads a single bare SayToTopicInfoData2, so the count + // byte corrupts TEXT1's wstring length -> BufferUnderflow on every inline-text instance. + super.readVSElemArray(input, "DATA2", in -> new SayToTopicInfoData2(in, context)); + super.readShort(input, "UNK_U16"); + super.readElements(input, "REFS", 4, in -> context.readRefID(in)); } } diff --git a/src/main/java/resaver/ess/ChangeFormQust.java b/src/main/java/resaver/ess/ChangeFormQust.java index 8c087f1..f523328 100644 --- a/src/main/java/resaver/ess/ChangeFormQust.java +++ b/src/main/java/resaver/ess/ChangeFormQust.java @@ -73,24 +73,34 @@ public ChangeFormQust(ByteBuffer input, Flags.Int flags, boolean inline, ESS.ESS : Float.NaN; this.QUEST_STAGES = flags.getFlag(CHANGE_QUEST_STAGES) - ? (QuestStage[]) super.readVSElemArray(input, "QUEST_STAGES", i -> new QuestStage(i, context)) + ? (Element[]) super.readVSElemArray(input, "QUEST_STAGES", i -> new QuestStage(i, context)) : null; this.QUEST_OBJECTIVES = flags.getFlag(CHANGE_QUEST_OBJECTIVES) - ? (QuestObjective[]) super.readVSElemArray(input, "QUEST_OBJECTIVES", i -> new QuestObjective(i, context)) + ? (Element[]) super.readVSElemArray(input, "QUEST_OBJECTIVES", i -> new QuestObjective(i, context)) : null; this.QUEST_RUN_DATA = flags.getFlag(CHANGE_QUEST_RUNDATA) ? new QuestRunData(input, context) : null; - - - - + + // QuestInstances sits between RUNDATA and ALREADY_RUN (UESP Save File Format/QUST + // Changeform). Stock ReSaver never reads CHANGE_QUEST_INSTANCES(27), silently under-reading + // every instanced quest. Validated byte-exact (position==limit) across 3 saves. + this.QUEST_INSTANCES = flags.getFlag(CHANGE_QUEST_INSTANCES) + ? new QuestInstances(input, context) + : null; + this.ALREADY_RUN = flags.getFlag(CHANGE_QUEST_ALREADY_RUN) ? super.readByte(input, "ALREADY_RUN") : 0; - + + // self-checking byte-exact gate (mirrors REFR/ACHR). Skipped when inline (this + // changeform is nested inside another, where trailing data belongs to the parent). + if (!inline && super.readUnparsed(input)) { + throw new UnparsedException(); + } + } catch (UnparsedException ex) { throw new ElementException("Unparsed data in QUST", ex, this); } catch (RuntimeException ex) { @@ -151,9 +161,10 @@ public boolean matches(Optional analysis, String mod) { final private ChangeFormFlags CHANGEFORMFLAGS; final private Flags.Short QUEST_FLAGS; final private float SCRIPT_DELAY; - final private QuestStage[] QUEST_STAGES; - final private QuestObjective[] QUEST_OBJECTIVES; + final private Element[] QUEST_STAGES; + final private Element[] QUEST_OBJECTIVES; final private QuestRunData QUEST_RUN_DATA; + final private QuestInstances QUEST_INSTANCES; final private byte ALREADY_RUN; @@ -189,24 +200,61 @@ public String toString() { public int UNK2; } - + + // ---- QuestInstances / QuestInstanceData (UESP Save File Format/QUST Changeform) ---- + // CHANGE_QUEST_INSTANCES(27) data; written for quests with live alias instances. + static private class QuestInstances extends GeneralElement { + + public QuestInstances(ByteBuffer input, ESS.ESSContext context) throws ElementException{ + super.readInt(input, "UNK"); + super.readVSElemArray(input, "INSTANCE_DATA", i -> new QuestInstanceData(i, context)); + } + } + + static private class QuestInstanceData extends GeneralElement { + + public QuestInstanceData(ByteBuffer input, ESS.ESSContext context) throws ElementException{ + super.readInt(input, "UNK"); + super.readVSElemArray(input, "STRUCT1", i -> new QuestInstanceStruct1(i, context)); // {uint32; refID} + super.readVSElemArray(input, "STRUCT2", i -> new QuestInstanceStruct2(i, context)); // {refID; uint32} + super.readShort(input, "UNK16"); + super.readByte(input, "UNK8"); + } + } + + static private class QuestInstanceStruct1 extends GeneralElement { + + public QuestInstanceStruct1(ByteBuffer input, ESS.ESSContext context) throws ElementException{ + super.readInt(input, "UNK"); + super.readRefID(input, "REF", context); + } + } + + static private class QuestInstanceStruct2 extends GeneralElement { + + public QuestInstanceStruct2(ByteBuffer input, ESS.ESSContext context) throws ElementException{ + super.readRefID(input, "REF", context); + super.readInt(input, "UNK"); + } + } + static private class QuestRunData extends GeneralElement { public QuestRunData(ByteBuffer input, ESS.ESSContext context) throws ElementException{ UNK = super.readByte(input, "UNK"); COUNT1 = super.readInt(input, "COUNT1"); - ITEMS1 = (QuestRunDataItem1[]) super.readElements(input, "ITEMS1", COUNT1, i -> new QuestRunDataItem1(i, context)); + ITEMS1 = (Element[]) super.readElements(input, "ITEMS1", COUNT1, i -> new QuestRunDataItem1(i, context)); COUNT2 = super.readInt(input, "COUNT2"); - ITEMS2 = (QuestRunDataItem2[]) super.readElements(input, "ITEMS2", COUNT2, i -> new QuestRunDataItem2(i, context)); + ITEMS2 = (Element[]) super.readElements(input, "ITEMS2", COUNT2, i -> new QuestRunDataItem2(i, context)); FLAG = super.readElement(input, "FLAG", Flags::readByteFlags); ITEM3 = FLAG.allZero() ? null : new QuestRunDataItem3(input, context); } final public byte UNK; final public int COUNT1; - final public QuestRunDataItem1[] ITEMS1; + final public Element[] ITEMS1; final public int COUNT2; - final public QuestRunDataItem2[] ITEMS2; + final public Element[] ITEMS2; final public Flags.Byte FLAG; final public QuestRunDataItem3 ITEM3; } @@ -216,7 +264,7 @@ static private class QuestRunDataItem1 extends GeneralElement { public QuestRunDataItem1(ByteBuffer input, ESS.ESSContext context) throws ElementException{ UNK = super.readInt(input, "UNK"); FLAGS = super.readElement(input, "FLAG", Flags::readByteFlags); - REFS = (RefID[]) super.readElements(input, "REFS", (FLAGS.allZero() ? 1 : 5), context::readRefID); + REFS = (Element[]) super.readElements(input, "REFS", (FLAGS.allZero() ? 1 : 5), context::readRefID); } @Override @@ -226,7 +274,7 @@ public String toString() { final public int UNK; final public Flags.Byte FLAGS; - final public RefID[] REFS; + final public Element[] REFS; } static private class QuestRunDataItem2 extends GeneralElement { @@ -252,6 +300,7 @@ static private class QuestRunDataItem3Data extends GeneralElement { public QuestRunDataItem3Data(ByteBuffer input, ESS.ESSContext context) throws ElementException{ int type = super.readInt(input, "TYPE"); switch (type) { + case 0: // type 0 is valid and refID-typed (like 1/2/4); stock ReSaver threw on it. case 1: case 2: case 4: diff --git a/src/main/java/resaver/ess/ChangeFormRefr.java b/src/main/java/resaver/ess/ChangeFormRefr.java index 7882599..c1a6994 100644 --- a/src/main/java/resaver/ess/ChangeFormRefr.java +++ b/src/main/java/resaver/ess/ChangeFormRefr.java @@ -104,10 +104,16 @@ public ChangeFormRefr(ByteBuffer input, Flags.Int changeFlags, RefID refid, Opti inventory = super.readVSElemArray(input, "INVENTORY", in -> new ChangeFormInventoryItem(in, context)); } - if (changeFlags.getFlag(CHANGE_REFR_PROMOTED)) { - super.readVSElemArray(input, "PROMOTION", in -> context.readRefID(in)); - } - + // The promoted-reference list is already read as the EXTRADATA "PromotedRef" element + // (ChangeFormExtraDataData case 140), which the CHANGE_REFR_PROMOTED flag also triggers + // above. This separate read is a duplicate that consumes past the end of the body and + // throws BufferUnderflowException on every promoted REFR. (Verified byte-exact: removing + // it recovers 6,226 REFRs on one save / 11,306 on another, with zero regressions — every + // recovered form reaches the readUnparsed() boundary cleanly.) + //if (changeFlags.getFlag(CHANGE_REFR_PROMOTED)) { + // super.readVSElemArray(input, "PROMOTION", in -> context.readRefID(in)); + //} + if (changeFlags.getFlag(CHANGE_REFR_ANIMATION)) { super.readBytesVS(input, "ANIMATIONS"); }