Skip to content
47 changes: 39 additions & 8 deletions src/main/java/resaver/ess/ChangeFormExtraDataData.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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));
}
}

Expand Down
81 changes: 65 additions & 16 deletions src/main/java/resaver/ess/ChangeFormQust.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -151,9 +161,10 @@ public boolean matches(Optional<Analysis> 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;

Expand Down Expand Up @@ -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;
}
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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:
Expand Down
14 changes: 10 additions & 4 deletions src/main/java/resaver/ess/ChangeFormRefr.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down