Modding bits and pieces

Where modders get to tear my game apart when I *finally* release the tools

Modding bits and pieces

PostPosted by Railboy » Tue Apr 29, 2014 1:44 am

I'll be dumping bits and pieces of information about modding in this thread - over time I'll start organizing it into sections etc. For now, pure brain dump. Feel free to jump in and make suggestions. (Like I could stop you, haha)

In FRONTIERS nearly all data is treated as a Mod, even the base content that's distributed with the game. There are some exceptions (like some terrain-related binary data) but even those exceptions are wrapped inside and handled by a mod object.

This is the Mod class:

Code: Select all
namespace Frontiers {
   [Serializable]
   public class Mod : IComparable <Mod>
   {
      public string Name;
      public string Type;
      public string Description;
      public string Authorship;
      public float Version;
      public List <string> Dependencies;
      public int DisplayOrder;
      public bool Enabled;

      public int CompareTo (Mod other)
      {
         return DisplayOrder.CompareTo (other.DisplayOrder);
      }
   }
}


Mods are handled by a manager class called, predictably, 'Mods.' Mod data is loaded and saved using two main functions, LoadMod and SaveMod. There are some other functions like LoadGame / LoadWorldSettings / etc but they're just wrappers that end up calling these two main functions.

These functions are duplicated in two separate classes, ModsRuntime and ModsEditor. The ModsEditor class is used when loading and saving data within the Unity editor. It bypasses saving to profile and saves directly to the world's base data. ModsRuntime always writes to the player's profile so that base data is never changed during gameplay. (I'll make another post about base vs. profile data later.)

Code: Select all
namespace Frontiers
{
   public class Mods
   {
      public static Mods Get;
      public ModsRuntime Runtime;
      #ifdef UNITY_EDITOR
      public ModsRuntime Editor;
      #endif

      ...
   }

   public class ModsRuntime
   {
      ...

      public void SaveMod <T> (T mod, string modType, string modName) where T: Mod;
      public bool LoadMod <T> (ref T mod, string modType, string modName) where T : Mod;
   }

   #ifdef UNITY_EDITOR
   public class ModsEditor
   {
      ...

      public void SaveMod <T> (T mod, string modType, string modName) where T: Mod;
      public bool LoadMod <T> (ref T mod, string modType, string modName) where T : Mod;
   }
   #endif
}


//used in editor - saves / loads directly to base data
Mods.Get.Editor.LoadMod <Plant> (ref plant, "Plant", "TwistedRose");
//used at runtime -saves ONLY to player's profile / loads from profile first, then world, then base
Mods.Get.Runtime.LoadMod <Plant> (ref plant, "Plant", "TwistedRose");


Here's an example of a Mod. This will look familiar to anyone who completed the plant kit. You'll notice that while Plant inherits from Mod, PlantSeasonalSettings does not. I could have treated them as separate moddable objects and saved / loaded them independently of one another, but it made little sense to in this case.
Code: Select all
namespace Frontiers {
   [Serializable]
   public class Plant : Mod {
      public string CommonName;
      public string NickName;
      public string ScientificName;
      public bool AboveGround = true;
      public ElevationType Elevation = ElevationType.Medium;
      public ClimateType Climate = ClimateType.Temperate;
      public bool HasThorns = false;
      public bool HasFlowers = true;
      public int ThornVariation = 0;
      public int ThornTexture = 0;
      public int BodyType = 0;
      public int BodyVariation = 0;
      public int BodyTexture = 0;
      public int FlowerType = 0;
      public int FlowerVariation = 0;
      public int FlowerTexture = 0;
      public PlantRootType RootType = PlantRootType.TypicalBranched;
      public int RootVariation = 0;
      public int RootTexture = 0;
      public PlantRootSize RootSize = PlantRootSize.Medium;
      public SColor RootBaseColor = Color.white;
      public float RootHueShift = 0f;
      public List <PlantSeasonalSettings> SeasonalSettings = new List <PlantSeasonalSettings> ();
      public FoodStuffProps RawProps = new FoodStuffProps ( );
      public FoodStuffProps CookedProps = new FoodStuffProps ( );
   }

   [Serializable]
   public class PlantSeasonalSettings
   {
      [BitMaskAttribute (typeof (TimeOfYear))]
      public TimeOfYear Seasonality = TimeOfYear.SeasonSummer;
      public bool Flowers = false;
      public PlantBodyHeight BodyHeight = PlantBodyHeight.Medium;
      public PlantFlowerSize FlowerSize = PlantFlowerSize.Medium;
      public SColor BodyUnderColor = Color.white;
      public SColor FlowerUnderCOlor = Color.white;
      public float FlowerHueShift = 0f;
      public float BodyHueShift = 0f;
      public float FlowerDensity = 0.5f;
   }
}




All Mods must be serializable to XML. Serialization is handled automatically by the Mods class. A plant ends up looking like this:
Code: Select all
<?xml version="1.0" encoding="utf-8"?>
<Plant xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <Name>TwistedRose</Name>
 <Description />
 <Authorship />
 <Type>Plant</Type>
 <Version>1</Version>
 <Dependencies />
 <DisplayOrder>0</DisplayOrder>
 <Enabled>true</Enabled>
 <CommonName>Twisted Rose</CommonName>
 <NickName>Bleeding Heart Rose</NickName>
 <ScientificName />
 <AboveGround>true</AboveGround>
 <Elevation>Medium</Elevation>
 <Climate>Temperate</Climate>
 <HasThorns>true</HasThorns>
 <HasFlowers>true</HasFlowers>
 <ThornVariation>0</ThornVariation>
 <ThornTexture>0</ThornTexture>
 <BodyType>15</BodyType>
 <BodyVariation>0</BodyVariation>
 <BodyTexture>0</BodyTexture>
 <FlowerType>18</FlowerType>
 <FlowerVariation>0</FlowerVariation>
 <FlowerTexture>0</FlowerTexture>
 <RootType>ThickTaproot</RootType>
 <RootVariation>0</RootVariation>
 <RootTexture>0</RootTexture>
 <RootSize>Large</RootSize>
 <RootBaseColor>
  <a>1</a>
  <r>1</r>
  <g>1</g>
  <b>1</b>
 </RootBaseColor>
 <RootHueShift>0.22047244</RootHueShift>
 <SeasonalSettings>
  <PlantSeasonalSettings>
   <Seasonality>SeasonSpring</Seasonality>
   <Flowers>false</Flowers>
   <BodyHeight>ExtraShort</BodyHeight>
   <FlowerSize>Medium</FlowerSize>
   <BodyUnderColor>
    <a>1</a>
    <r>1</r>
    <g>1</g>
    <b>1</b>
   </BodyUnderColor>
   <FlowerUnderCOlor>
    <a>1</a>
    <r>1</r>
    <g>1</g>
    <b>1</b>
   </FlowerUnderCOlor>
   <FlowerHueShift>0.425196856</FlowerHueShift>
   <BodyHueShift>0</BodyHueShift>
   <FlowerDensity>0.5</FlowerDensity>
  </PlantSeasonalSettings>
  <PlantSeasonalSettings>
   <Seasonality>SeasonSummer</Seasonality>
   <Flowers>true</Flowers>
   <BodyHeight>Medium</BodyHeight>
   <FlowerSize>Large</FlowerSize>
   <BodyUnderColor>
    <a>1</a>
    <r>1</r>
    <g>1</g>
    <b>1</b>
   </BodyUnderColor>
   <FlowerUnderCOlor>
    <a>1</a>
    <r>1</r>
    <g>1</g>
    <b>1</b>
   </FlowerUnderCOlor>
   <FlowerHueShift>-0.976377964</FlowerHueShift>
   <BodyHueShift>0</BodyHueShift>
   <FlowerDensity>0.05</FlowerDensity>
  </PlantSeasonalSettings>
 </SeasonalSettings>
 <RawProps>
  <Name>Default</Name>
  <Type>None</Type>
  <IsLiquid>false</IsLiquid>
  <HungerRestore>B_OneFifth</HungerRestore>
  <HealthRestore>A_None</HealthRestore>
  <HealthReduce>A_None</HealthReduce>
  <Poisonous>None</Poisonous>
  <Hallucinogen>None</Hallucinogen>
  <ConditionChance>0</ConditionChance>
  <ConditionName />
  <EatFoodSound>EatFoodGeneric</EatFoodSound>
 </RawProps>
 <CookedProps>
  <Name>Default</Name>
  <Type>None</Type>
  <IsLiquid>false</IsLiquid>
  <HungerRestore>B_OneFifth</HungerRestore>
  <HealthRestore>A_None</HealthRestore>
  <HealthReduce>A_None</HealthReduce>
  <Poisonous>None</Poisonous>
  <Hallucinogen>None</Hallucinogen>
  <ConditionChance>0</ConditionChance>
  <ConditionName />
  <EatFoodSound>EatFoodGeneric</EatFoodSound>
 </CookedProps>
</Plant>


A simple class like Plant is pretty easy to read and manipulate. Other classes require some manual string manipulation to get them looking nice. Here's a serialized structure template:
Code: Select all
<?xml version="1.0" encoding="utf-8"?>
<StructureTemplate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>BogHut</Name>
 <Description />
 <Authorship />
 <Type>Structure</Type>
 <Version>1</Version>
 <Dependencies />
 <DisplayOrder>0</DisplayOrder>
 <Enabled>true</Enabled>
 <InteriorVariants />
 <Exterior>
  <StaticStructureLayers>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorhernHouseRoofBendPartial</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   0,   4,   4,   0,   270.0001,   0,   1,   1,   1,   [Default]
[Default],   [Default],   0,   4,   -4,   0,   0,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -4,   4,   4,   0,   180.0003,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -4,   4,   -4,   0,   89.99988,   0,   1,   1,   1,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorhernHouseRoofSlant</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   2,   4,   2,   0,   -0.0001062528,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -6,   4,   2,   0,   180.0002,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -2,   4,   -6,   0,   90.00037,   0,   1,   1,   1,   [Default]
[Default],   [Default],   1.999876,   4,   -2,   0,   -0.0001062528,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -2,   4,   6,   0,   270.0001,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -6,   4,   -2,   0,   180.0002,   0,   1,   1,   1,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorhernHouseRoofTopFlat</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   -2,   4,   2,   0,   -9.659346E-06,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -2,   4,   -2,   0,   0,   0,   1,   1,   1,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorthernHouseCeilingBend</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   -6,   -0.5,   -6,   0,   90.0004,   0,   1,   0.5,   1,   [Default]
[Default],   [Default],   -6,   -0.5,   6,   0,   180.0002,   0,   1,   0.5,   1,   [Default]
[Default],   [Default],   2,   -0.5,   -6,   0,   0.000560242,   0,   1,   0.5,   1,   [Default]
[Default],   [Default],   2,   -0.5,   6,   0,   270.0002,   0,   1,   0.5,   1,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorthernHouseFloor</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>29</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   -2,   0,   6,   0,   -9.659346E-06,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -2,   0,   -2,   0,   -9.659346E-06,   0,   1,   1,   1,   [Default]
[Default],   [Default],   2,   0,   2,   0,   0,   0,   1,   1,   1,   [Default]
[Default],   [Default],   2,   0,   -2,   0,   -9.659346E-06,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -2,   0,   -6,   0,   -9.659346E-06,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -2,   0,   2,   0,   -9.659346E-06,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -6,   0,   2,   0,   -9.659346E-06,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -6,   0,   -2,   0,   -9.659346E-06,   0,   1,   1,   1,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorthernHouseOuterColumnCeilingBend</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   -8.02857,   0,   -4.152335,   0,   89.99983,   0,   1.08,   1.08,   1.08,   [Default]
[Default],   [Default],   -4.213492,   0,   8.04981,   0,   180.0002,   0,   1.08,   1.08,   1.08,   [Default]
[Default],   [Default],   0.07225608,   0,   -8.015138,   0,   -1.931869E-05,   0,   1.08,   1.08,   1.08,   [Default]
[Default],   [Default],   4.092046,   0,   4.02441,   0,   270.0002,   0,   1.08,   1.08,   1.08,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorthernHouseOuterWallUpper</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   -8,   0,   -2,   0,   89.99966,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -2,   0,   8,   0,   179.9999,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -2,   0,   -8,   0,   -0.0003670551,   0,   1,   1,   1,   [Default]
[Default],   [Default],   4,   0,   2,   0,   269.9997,   0,   1,   1,   1,   [Default]
[Default],   [Default],   4,   0,   -2,   0,   269.9998,   0,   1,   1,   1,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorthernHouseOuterWallUpperBend</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   -2.602771E-06,   0,   3.99999,   0,   270,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -4,   0,   4,   0,   179.9997,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -3.999988,   0,   -3.999993,   0,   89.99977,   0,   1,   1,   1,   [Default]
[Default],   [Default],   0,   0,   -4,   0,   0,   0,   1,   1,   1,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorthernHouseOuterWallUpperCrossBeamA</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   -2,   0,   -8,   0,   -0.0003670551,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -2,   0,   8,   0,   179.9999,   0,   1,   1,   1,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorthernHouseOuterWallUpperCrossBeamB</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   -2,   0,   -8,   0,   -0.0003670551,   0,   1,   1,   1,   [Default]
[Default],   [Default],   4,   0,   2,   0,   269.9998,   0,   1,   1,   1,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorthernHouseOuterWallUpperDoor</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   -8,   0,   2.000498,   0,   89.9995,   0,   1,   1,   1,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>NorthernHousePieces</PackName>
    <PrefabName>NorthernHouseOuterWallUpperFiller</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   4,   -5,   -2,   0,   269.9996,   0,   1,   1,   1,   [Default]
[Default],   [Default],   4.000125,   -5,   2,   0,   269.9996,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -8,   -5,   -2,   0,   89.99988,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -2,   -5,   -8,   0,   -0.0005119453,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -8,   -5,   2,   0,   89.99988,   0,   1,   1,   1,   [Default]
[Default],   [Default],   -2,   -5,   8,   0,   179.9993,   0,   1,   1,   1,   [Default]
</Instances>
   </StructureLayer>
   <StructureLayer>
    <PackName>WoodSpikeAndFencePieces</PackName>
    <PrefabName>WoodSpikeThickType1</PrefabName>
    <Tag>GroundWood</Tag>
    <Layer>12</Layer>
    <Substitutions />
    <Instances>
[Default],   [Default],   -3.885732,   -8.396713,   8.906256,   0,   329.9995,   0,   1,   1.2285,   1,   [Default]
[Default],   [Default],   0.1142684,   -8.396713,   8.906251,   0,   209.9997,   0,   1,   1.2285,   1,   [Default]
[Default],   [Default],   -3.885753,   -8.396713,   -8.997555,   0,   29.99991,   0,   1,   1.2285,   1,   [Default]
[Default],   [Default],   -8.885738,   -8.396713,   3.735851,   0,   149.9997,   0,   1,   1.2285,   1,   [Default]
[Default],   [Default],   4.904416,   -8.396713,   -3.264166,   0,   149.9997,   0,   1,   1.2285,   1,   [Default]
[Default],   [Default],   -8.885747,   -8.396713,   -3.26415,   0,   149.9997,   0,   1,   1.2285,   1,   [Default]
[Default],   [Default],   4.904424,   -8.396713,   3.735834,   0,   149.9997,   0,   1,   1.2285,   1,   [Default]
[Default],   [Default],   0.1142471,   -8.396713,   -8.99756,   0,   149.9997,   0,   1,   1.2285,   1,   [Default]
</Instances>
   </StructureLayer>
  </StaticStructureLayers>
  <StaticStructureColliders />
  <StaticLayers />
  <GenericWItems>
</GenericWItems>
  <GenericWindows>
</GenericWindows>
  <GenericDoors>
</GenericDoors>
  <GenericDynamic>
</GenericDynamic>
  <GenericLights>
</GenericLights>
  <Fires>
</Fires>
  <FX />
  <UniqueDynamic />
  <UniqueWindows />
  <UniqueDoors />
  <UniqueWorlditems />
  <Characters />
  <ActionNodes />
 </Exterior>
 <Destroyed>
  <StaticStructureLayers />
  <StaticStructureColliders />
  <StaticLayers />
  <GenericWItems>
</GenericWItems>
  <GenericWindows>
</GenericWindows>
  <GenericDoors>
</GenericDoors>
  <GenericDynamic>
</GenericDynamic>
  <GenericLights>
</GenericLights>
  <Fires>
</Fires>
  <FX />
  <UniqueDynamic />
  <UniqueWindows />
  <UniqueDoors />
  <UniqueWorlditems />
  <Characters />
  <ActionNodes />
 </Destroyed>
</StructureTemplate>


Instead of saving each instance as a separate object with xml fields for X, Y, Z etc. I've just used a single tab-delineated string that I parse manually. Before doing this I couldn't make heads or tails of a serialized structure; now I can tell at a glance roughly what to expect. I'm hoping modders will use similar methods for complex data to keep everything readable. XML is very powerful and there are a lot of free tools out there for serializing and manipulating it, but it's kind of an eyesore.

More soon.
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1776
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding bits and pieces

PostPosted by Railboy » Wed May 07, 2014 5:20 pm

For the most part I'm trying to make FRONTIERS moddable without actual scripting. All signs point to players being able to write and import / attach their own C# scripts to FRONTIERS objects, but that will require compiling and distributing DLLs so I expect only hardcore modders will bother.

I also didn't want to create a new scripting language or implement an existing scripting language like Lua (though Lua was tempting).

So I've gone with kind of a weird hybrid - data based scripting. (I'm sure there's a real term for what I'm doing dating back to the 1980s, but if there is I haven't heard of it. So I'm making one up.) The idea is that you can implement new behaviors with small serializable objects that are created / modified outside the program and then loaded at runtime.

This approach has a couple of benefits and a couple of pretty severe limits. The benefit is that it's really, really easy to create new missions, conversations, skills and so on. All you have to do is crack open the data - all of these objects are saved as separate files in the MODS folder - muck around with what's there, and boom your changes are in the game. You can alter missions to refer to new objects or create objects that give your character a new condition, and the game won't know that it's dealing with modded data. If it finds it in the MODS folder then it's happy.

The downside is that it's only easy to create new behaviors within the limits of the existing system. If you want to change a creature's AI routine then you can edit its CreatureBehaviors, but all this will do is alter its behavior flowchart a bit. If you want it to behave in entirely new ways, you'll have to attach a new C# script to the object.

Here's an example of how you'd add a new Condition to the game (conditions are things like diseases, buffs, whatever):
Code: Select all
<?xml version="1.0" encoding="utf-8"?>
<Condition xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <Name>Fever</Name>
 <Description />
 <Type>Condition</Type>
 <Content>Base</Content>
 <Dependencies />
 <DisplayOrder>0</DisplayOrder>
 <Enabled>true</Enabled>
 <DisplayName />
 <IconName>ConditionIconFever</IconName>
 <AtlasName />
 <GainedSomethingMessage>You have a fever.</GainedSomethingMessage>
 <Symptoms>
  <Symptom>
   <Target>Health</Target>
   <SeekType>Negative</SeekType>
   <SeekValue>0</SeekValue>
   <SeekSpeed>0.001</SeekSpeed>
   <ActiveAfter>0</ActiveAfter>
   <ExpiresAutomatically>false</ExpiresAutomatically>
   <Duration>0</Duration>
  </Symptom>
 </Symptoms>
 <CureActions>
  <AvatarAction>SurvivalSleep</AvatarAction>
 </CureActions>
 <CureConditions>
  <string>WellRested</string>
 </CureConditions>
 <CureStates />
 <ForcedCures />
 <SkillUseCures />
 <FXOnStart>None</FXOnStart>
 <FXIntensityOnStart>0</FXIntensityOnStart>
 <FXOnPing>None</FXOnPing>
 <FXIntensityOnPing>0</FXIntensityOnPing>
 <FXOnExpire>None</FXOnExpire>
 <FXIntensityOnExpire>0</FXIntensityOnExpire>
 <DurationStacks>false</DurationStacks>
 <ExpiresAutomatically>true</ExpiresAutomatically>
 <Duration>1000</Duration>
 <TimeSoFar>0</TimeSoFar>
</Condition>


And here is the class that this data gets dumped into:
Code: Select all
   [Serializable]
   public class Condition : Mod
   {
      public Condition ()
      {
      }

      public string DisplayName = string.Empty;
      public string IconName = "ConditionIcon";
      public string AtlasName = "Default";
      public string GainedSomethingMessage = string.Empty;
      public List <Symptom> Symptoms = new List <Symptom> ();
      public List <AvatarAction> CureActions = new List <AvatarAction> ();
      public List <string> CureConditions = new List <string> ();
      public List <string> CureStates = new List <string> ();
      public List <string> ForcedCures = new List <string> ();
      public List <string> SkillUseCures = new List <string> ();
      //camera FX
      public CameraFX.FXType FXOnStart = CameraFX.FXType.None;
      public float FXIntensityOnStart = 0f;
      public CameraFX.FXType FXOnPing = CameraFX.FXType.None;
      public float FXIntensityOnPing = 0f;
      public CameraFX.FXType FXOnExpire = CameraFX.FXType.None;
      public float FXIntensityOnExpire = 0f;

      public bool HasExpired { get { return mExpired; } }

      public bool DurationStacks = false;
      public bool ExpiresAutomatically = true;
      public float Duration = 1.0f;
      public float TimeSoFar = 0f;
      public float WTDuration
      {
         get {
            return WorldClock.RTSecondsToGameSeconds (Duration);
         }
      }

      public float NormalizedTimeLeft {
         get {
            if (ExpiresAutomatically) {
               return Mathf.Clamp01 ((WTDuration - TimeSoFar) / WTDuration);
            }
            //if it doesn't expire automatically
            //1 means it'll be around forever
            return 1.0f;
         }
      }
      //this is only called when a condition is cloned
      //this should never be called when using a loaded state
      public void Initialize ()
      {
         if (SkillUseCures.Count > 0) {
            Player.Get.AvatarActions.Subscribe (AvatarAction.SkillUse, new ActionListener (SkillUse));
         }
         //make sure there are no duplicate targets in the symptoms
         //if there is make a loud noise and remove it
         mSymptomLookup.Clear ();
         for (int i = Symptoms.Count - 1; i >= 0; i--) {
            if (string.IsNullOrEmpty (Symptoms [i].Target)
                || mSymptomLookup.ContainsKey (Symptoms [i].Target)) {
               Symptoms.RemoveAt (i);
            } else {
               mSymptomLookup.Add (Symptoms [i].Target, Symptoms [i]);
            }
         }
         TimeSoFar = 0f;

         if (string.IsNullOrEmpty (DisplayName)) {   //TODO add spaces
            DisplayName = Name;
         }
      }
      //used when the player dies
      public void Cancel ()
      {   
         mExpired = true;
      }

      public void IncreaseDuration (float amountToIncrease)
      {
         //TODO add min/max variables
         TimeSoFar -= amountToIncreasef;
      }

      public bool HasSymptomFor (string statusKeeperName)
      {
         return mSymptomLookup.ContainsKey (statusKeeperName);
      }

      public bool HasSymptomFor (string statusKeeperName, out Symptom symptom)
      {
         return mSymptomLookup.TryGetValue (statusKeeperName, out symptom);
      }
      //conditions can expire for three reasons
      //- Duration             - it just goes away after a while
      //- AvatarAction          - because the player did something, eg drinking cures 'Dehydrated'
      //- Cured by condition       - because another condition wiped it out, eg 'Wet' cures 'On Fire'
      //- Forced cures          - because something outside the condition forced a cure, eg 'Spotted Mushroom' curing specific diseases
      public bool CheckExpired (float deltaTime,
                                        List <AvatarAction> recentActions,
                                        List <Condition> activeConditions)
      {
         TimeSoFar += deltaTime;
         if (mExpired || ExpiresAutomatically && (TimeSoFar >= WTDuration)) {//first see if it has expired automatically
            //or if all symptoms are expired
            mExpired = true;
         } else {   //if that didn't trigger, check the symptoms
            bool isActive = false;
            for (int i = 0; i < Symptoms.Count; i++) {   //this will include 'inactive' symptoms that have not been activated yet
               isActive |= Symptoms [i].IsActive (TimeSoFar);
            }
            if (!isActive) {   //if NO symptom is active then we're expired
               //from this poinf forward HasExpired will always return true
               mExpired = true;
            }
         }
         return mExpired;
      }

      public float ApplyTo (StatusKeeper keeper, float deltaTime)
      {
         Symptom symptom = null;
         if (mSymptomLookup.TryGetValue (keeper.Name, out symptom)) {   //if it's active...
            if (symptom.IsActive (TimeSoFar)) {
               //apply to the actual value not the normalized value
               float currentValue = keeper.Value;
               //get the ACTUAL seek value as it applies to this keeper's seek value type
               float seekValue = StatusKeeper.GetSeekValue (keeper.ActiveState.SeekType, symptom.SeekType, symptom.SeekValue);
               //apply the value to the current value
               return Mathf.Lerp (currentValue, symptom.SeekValue, symptom.SeekSpeed * deltaTime);
            } else {   //if it's not active just return the untouched value
               return keeper.Value;
            }
         } else {
            return keeper.Value;
         }
      }

      public bool SkillUse (float timeStamp)
      {
         foreach (string skillCure in SkillUseCures) {
            if (Skills.Get.IsSkillInUse (skillCure)) {
               Cancel ();
               break;
            }
         }
         return true;
      }

      protected bool mExpired = false;
      protected float mValueLastUpdate = 0f;
      protected Dictionary <string, Symptom> mSymptomLookup = new Dictionary <string, Symptom> ();
   }


As well as the Symptom class that Condition makes use of:
Code: Select all
   [Serializable]
   public class Symptom
   {
      public string Target = "StatusKeeper";//health, strength, whatever
      public StatusSeekType SeekType = StatusSeekType.Negative;
      public float SeekValue = 1.0f;
      public float SeekSpeed = 1.0f;
      //TODO timed symptoms
      //these can be used for a progression of effects
      //for instance drunkenness followed by a hangover
      //if IsExpired is true for all symptoms then the condition expires automatically
      public float ActiveAfter = 0f;
      public bool ExpiresAutomatically = false;
      public float Duration = 0f;

      public bool    IsActive (float timeSoFar)
      {   //true by default for most symptoms
         if ((timeSoFar > ActiveAfter) && !IsExpired (timeSoFar)) {   //yay, this symptom is active
            //this only gets flagged once
            //Debug.Log ("Symptom is active");
            mHasBecomeActive = true;
            return true;
         }
         return false;
      }

      public bool    IsExpired (float timeSoFar)
      {   //the duration isn't from the start of the condition
         //it's from the start of the symptom
         //if the symptom has never become active yet then it can't expire
         //this is useful for symptoms that don't kick in for a long time
         return (mHasBecomeActive && ExpiresAutomatically && (timeSoFar - ActiveAfter) > Duration);
      }

      protected bool mHasBecomeActive;
   }



In the PlayerStatus script, this routine gets run every frame:
Code: Select all
   protected IEnumerator CheckActiveConditions ()
   {
      mLastConditionCheckTime = WorldClock.Time;

      while (mCheckingActiveConditions) {

         while (GameManager.State != FGameState.InGame) {
            yield return null;
         }

         for (int i = State.ActiveConditions.Count - 1; i >= 0; i--) {
            //use the last checked time to get a delta time
            float deltaTime = (WorldClock.Time - mLastConditionCheckTime) * StatusTimecale;
            if (State.ActiveConditions [i].CheckExpired ((deltaTime),
                   RecentActions,
                   State.ActiveConditions)) {   //it's toast, remove it
               State.ActiveConditions.RemoveAt (i);
            }
            yield return null;//TODO check if this is wise?
         }
         mLastConditionCheckTime = WorldClock.Time;
         yield return new WaitForSeconds (CheckConditionsInterval);
      }
      mCheckingActiveConditions = false;
      yield break;
   }


Apart from the world time there's very little in this object that isn't controlled by the serialized data.

Conditions do their work on the player's StatusKeepers, which are also serialized and easily modifiable. StatusKeepers like Health, Strength etc. aren't privileged objects; they're just kept in a list that can be expanded or changed. They could be removed without breaking the game - even the interface only knows to display whatever it finds in that list. You could create a 'Happiness' status keeper, for instance. Then you could create several conditions that alter your happiness level in various ways. Then you could create a food or drink type that gives the player those conditions, or maybe trigger them in a conversation. Pretty soon you've got a completely different set of rules and goals.

StatusKeepers are currently serialized into the PlayerStatus script state, but I'll be breaking them out into their own Mod type so they can be altered or created more easily. Here's the StatusKeeper class. Anything not marked XMLIgnore is alterable, including colors and icons:

Code: Select all
   [Serializable]
   public class StatusKeeper
   {
      public StatusKeeper ()
      {
         mInitialized = false;
      }

      public string Name = "StatusKeeper";
      public string IconName = "StatusKeeperIcon";
      public string AtlasName = "Default";
      //used by GUI, stored in state Color vars
      [XmlIgnore]
      [HideInInspector]
      public Color HighColorValue = Color.white;
      [XmlIgnore]
      [HideInInspector]
      public Color LowColorValue = Color.white;
      [XmlIgnore]
      [HideInInspector]
      public Color MidColorValue = Color.white;
      //Value is the raw unclamped value of the seeker
      //condition modifiers are applied directly to Value
      public float Value = 0f;
      public StatusKeeperState DefaultState = new StatusKeeperState ();
      public List <StatusKeeperState> AlternateStates = new List <StatusKeeperState> ();
      [XmlIgnore]//ignore this because the player state will keep a set of active conditions without duplicates
      [HideInInspector]
      public List <Condition> Conditions = new List <Condition> ();
      [XmlIgnore]//ignore this because they'll be sent again anyway and we want to preserve references
      [HideInInspector]
      public List <StatusFlow> StatusFlows = new List <StatusFlow> ();
      //AdjustedValue value is the raw value with last update's overflow / underflow applied, then clamped to the -1 - 2 range
      //Normalized value clamps AdjustedValue to make it usable with GUI / display
      public bool Initialized { get { return mInitialized && mActiveState != null; } }

      public float AdjustedValue { get { return Mathf.Clamp (mValueWithFlow, -1f, 2f); } }

      public float NormalizedValue { get { return Mathf.Clamp01 (AdjustedValue); } }

      public StatusKeeperState ActiveState { get { return mActiveState; } }

      public float ChangeLastUpdate { get { return mValueWithFlow - mValueWithFlowLastUpdate; } }

      public StatusSeekType LastChangeType {
         get {
            StatusSeekType changeType = StatusSeekType.Neutral;
            if (ChangeLastUpdate < 0f) {
               if (ActiveState.NegativeChange == StatusSeekType.Negative) {
                  changeType = StatusSeekType.Negative;
               }
            } else {
               if (ActiveState.PositiveChange == StatusSeekType.Positive) {
                  changeType = StatusSeekType.Positive;
               }
            }
            return changeType;
         }
      }

      public float Overflow { //for values > 1, up to 2
         get {
            if (Value > 1f) {
               return Mathf.Clamp01 (Value - 1f);
            }
            return 0f;
         }
      }

      public float Underflow { //for values < 1, up to -1
         get {
            if (Value < 0f) {
               return Mathf.Clamp01 (Mathf.Abs (Value));
            }
            return 0f;
         }
      }
      //called when ChangeLastUpdate exceeds ChangeThreshold
      [XmlIgnore]//ignore this when serializing, player status is only one that uses it
      public Action OnChangeAction = null;
      public float OnChangeThreshold = 0.01f;

      public void Reset ()
      {
         StatusFlows.Clear ();
         mActiveState = DefaultState;
         Value = mActiveState.DefaultValue;
         mValueWithFlow = Value;
         mValueWithFlowLastUpdate = Value;
      }

      public void Initialize ()
      {
         if (mInitialized)
            return;
         //clear status flows but keep conditions
         //status flows are sent every update; conditions are only sent once
         StatusFlows.Clear ();
         mActiveState = DefaultState;
         Value = mActiveState.DefaultValue;
         mValueWithFlow = Value;
         mValueWithFlowLastUpdate = Value;

         mStateLookup = new Dictionary <string, StatusKeeperState> ();
         mStateLookup.Add ("Default", DefaultState);
         foreach (StatusKeeperState state in AlternateStates) {   //set icons & stuff on overflow/underflow
            mStateLookup.Add (state.StateName, state);
         }
         mInitialized = true;
         //refresh default state
         RefreshFlows ();
         GetColors ();

      }
      //when a status keeper's target is above or below zero
      //and its value rises or drops above or below zero
      //the overflow and underflow can be sent to another status keeper
      //eg hunger overflow will be sent to strength so eating more will boost strength
      //hunger underflow will be sent to health so starving will reduce health
      public void UpdateState (float deltaTime)
      {
         if (!mInitialized)
            return;

         Value = Mathf.Lerp (Value, mActiveState.SeekValue, mActiveState.SeekSpeed * deltaTime);
         //TODO this may not be necessary, keeping it for now
         mActiveState.Overflow.Disabled = (Value <= 1f) ? true : false;//not over one, disable
         mActiveState.Underflow.Disabled = (Value >= 0f) ? true : false;//not under zero, disable
         mActiveState.Overflow.FlowLastUpdate = Overflow;
         mActiveState.Underflow.FlowLastUpdate = Underflow;
      }
      //this is applying status flows to this keeper, not to other keepers
      //also where the keeper determines if a status flow is still targeting this keeper
      public void ApplyStatusFlows (float deltaTime)
      {
         if (!mInitialized)
            return;
         //store last update's value so we know how much we've changed
         mValueWithFlowLastUpdate = mValueWithFlow;
         //then reset mValueWithFlow to the current raw value and apply all flows to mValueWithFlow
         mValueWithFlow = Value;
         for (int i = StatusFlows.Count - 1; i >= 0; i--) {
            if (StatusFlows [i] == null || StatusFlows [i].TargetName != Name) {
               if (StatusFlows [i] != null)
                  StatusFlows [i].Disabled = true;
               StatusFlows.RemoveAt (i);
            } else if (StatusFlows [i].HasEffect) {   //if it has no effect it's disabled or shows no change
               mValueWithFlow = StatusFlows [i].ApplyTo (this, mValueWithFlow);
            }
         }
      }
      //this is where we apply the effects of conditions
      //also where we see if conditions have expired
      public void ApplyConditions (float deltaTime, List <AvatarAction> subscribedActions, List <Condition> activeConditions)
      {
         if (!mInitialized)
            return;

         for (int i = Conditions.Count - 1; i >= 0; i--) {
            if (Conditions [i] == null || Conditions [i].CheckExpired (deltaTime, subscribedActions, activeConditions)) {
               Conditions.RemoveAt (i);
            } else {
               Value = Conditions [i].ApplyTo (this, deltaTime);
            }
         }
      }
      //these are called once by PlayerStatus after SetState
      //the overflow and underflow are sent to the targeted StatusKeepers
      //StatusKeepers check to see if they're still the target of over/underflow
      //so there's no need to keep track of when it expires
      public bool HasUnderflowToSend (out StatusFlow underflow)
      {
         if (!mInitialized) {
            underflow = null;
            return false;
         }

         underflow = mActiveState.Underflow;
         return !string.IsNullOrEmpty (underflow.TargetName);
      }

      public bool HasOverflowToSend (out StatusFlow overflow)
      {
         if (!mInitialized) {
            overflow = null;
            return false;
         }

         overflow = mActiveState.Overflow;
         return !string.IsNullOrEmpty (overflow.TargetName);
      }
      //conditions will automatically be stacked with any existing conditions by the status manager
      //so we're safe to just add to our conditions list upon receiving
      public void ReceiveCondition (Condition newCondition)
      {   //TODO make sure this is all we actually need
         Conditions.Add (newCondition);
      }
      //these will automatically filter out duplicates
      //will usually be called after HasUnder/OverflowToSend by the status manager
      //StatusFlows aren't categorized by under/over once they're in the status keeper
      //all that matters to the keeper & the GUI is the effect that it has on the value
      public void ReceiveFlows (List <StatusFlow> newFlows)
      {
         if (!mInitialized)
            return;

         foreach (StatusFlow newFlow in newFlows) {
            if (newFlow != null) {
               bool replacedExisting = false;
               for (int i = StatusFlows.Count - 1; i >= 0; i--) {
                  if (StatusFlows [i] != null && StatusFlows [i] == newFlow) {//replace it outright
                     StatusFlows [i].Disabled = true;//disable the other flow //TODO make sure this doesn't break anything
                     StatusFlows [i] = newFlow;
                     replacedExisting = true;
                  }
               }
               if (!replacedExisting) {   //otherwise just add it normally
                  StatusFlows.Add (newFlow);
               }
            } else {
               //Debug.LogError ("Received NULL status flow in " + Name);
            }
         }
      }

      public void SetState (List <string> states)
      {
         if (!mInitialized)
            return;
         //go through the states in order and activate them in order
         //skip any that we don't have
         StatusKeeperState newActiveState = null;
         StatusKeeperState nextAttemptedState = null;
         bool activatedState = false;
         for (int i = 0; i < states.Count; i++) {   //set true if we found any state
            //go through all of them - later states will override earlier states
            if (mStateLookup.TryGetValue (states [i], out nextAttemptedState)) {
               if (nextAttemptedState != mActiveState) {
                  activatedState = true;
                  newActiveState = nextAttemptedState;
               }
            }
         }
         //if we found one
         if (activatedState) {//set it to the current active state
            mActiveState.Deactivate ();
            mActiveState = newActiveState;
            //update colors
            GetColors ();
            RefreshFlows ();
         }
      }

      public void ChangeValue (float amount, StatusSeekType changeType)
      {
         if (!mInitialized)
            return;

         Value += GetSeekValue (mActiveState.SeekType, changeType, amount);
         //TODO
      }

      protected void RefreshFlows ()
      {
         if (!mInitialized)
            return;

         mActiveState.Overflow.SenderName = Name;
         mActiveState.Underflow.SenderName = Name;
         mActiveState.Overflow.StateName = mActiveState.StateName;
         mActiveState.Underflow.StateName = mActiveState.StateName;

         //if (!string.IsNullOrEmpty (mActiveState.Underflow.IconName))
         mActiveState.Underflow.IconName = IconName;
         //if (!string.IsNullOrEmpty (mActiveState.Underflow.AtlasName))
         mActiveState.Underflow.AtlasName = AtlasName;

         //if (!string.IsNullOrEmpty (mActiveState.Overflow.IconName))
         mActiveState.Overflow.IconName = IconName;
         //if (!string.IsNullOrEmpty (mActiveState.Overflow.AtlasName))
         mActiveState.Overflow.AtlasName = AtlasName;
      }

      protected void GetColors ()
      {
         if (!mInitialized)
            return;

         if (!string.IsNullOrEmpty (mActiveState.HighColorName))
            HighColorValue = Colors.Get.ByName (mActiveState.HighColorName);
         else
            HighColorValue   = Colors.Get.ByName (DefaultState.HighColorName);

         if (!string.IsNullOrEmpty (mActiveState.MidColorName))
            MidColorValue = Colors.Get.ByName (mActiveState.MidColorName);
         else
            MidColorValue   = Colors.Get.ByName (DefaultState.MidColorName);

         if (!string.IsNullOrEmpty (mActiveState.LowColorName))
            LowColorValue = Colors.Get.ByName (mActiveState.LowColorName);
         else
            LowColorValue   = Colors.Get.ByName (DefaultState.LowColorName);

         if (mActiveState.MutedColor) {
            HighColorValue   = Colors.Muted (HighColorValue);
            MidColorValue   = Colors.Muted (MidColorValue);
            LowColorValue   = Colors.Muted (LowColorValue);
         }
      }

      public static float GetSeekValue (StatusSeekType originType, StatusSeekType appliedType, float seekValue)
      {
         //TODO implement inverting for mismatched seek types
         seekValue = Mathf.Abs (seekValue);
         switch (appliedType) {
         case StatusSeekType.Positive:
         case StatusSeekType.Neutral:
         default:
            break;

         case StatusSeekType.Negative:
            seekValue = -seekValue;
            break;
         }
         return seekValue;
      }

      protected bool mInitialized = false;
      protected float mValueWithFlow = 0f;
      protected float mValueWithFlowLastUpdate   = 0f;
      protected StatusKeeperState mActiveState = null;
      protected Dictionary <string, StatusKeeperState> mStateLookup   = null;
   }
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1776
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding bits and pieces

PostPosted by Gazz » Wed May 07, 2014 5:57 pm

That's pretty good.
Few mods ever totally change the scope of the game... and those are made by hardcore modders. =)

Frontiers could be the first game with survival gameplay that tracks a horniness stat. Fascinating. =P
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 658
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding bits and pieces

PostPosted by Railboy » Thu May 08, 2014 1:59 am

Gazz wrote:Frontiers could be the first game with survival gameplay that tracks a horniness stat.


What have I done? :o
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1776
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding bits and pieces

PostPosted by Zolana » Thu May 08, 2014 1:54 pm

Gazz wrote:
Frontiers could be the first game with survival gameplay that tracks a horniness stat. Fascinating. =P


Sounds like you've been planning this right from the start! :P
-Zolana

Generic ramblings about life, the universe, and everything: http://awjc.wordpress.com

Feel free to add me on Steam, but PM me here so I know who is adding me :)!
Zolana
Moderator
Moderator
 
Posts: 320
Joined: Thu Jul 18, 2013 11:48 am
Location: Woking, United Kingdom

Re: Modding bits and pieces

PostPosted by SignpostMarv » Thu May 08, 2014 10:21 pm

Potato tracker :D

How're things looking for those esoteric/flexible data api mods I'd like to do ?
User avatar
SignpostMarv
Alchemist
Alchemist
 
Posts: 298
Joined: Thu Jul 18, 2013 2:28 pm

Re: Modding bits and pieces

PostPosted by Railboy » Fri May 09, 2014 1:41 am

That's part of why I'm starting to post classes and raw data, I'm hoping to give people like you something to chew on & think about. As I post more maybe some ideas for custom tools will come into focus.
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1776
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding bits and pieces

PostPosted by Gazz » Fri May 09, 2014 5:02 am

Railboy wrote:That's part of why I'm starting to post classes and raw data, I'm hoping to give people like you something to chew on & think about. As I post more maybe some ideas for custom tools will come into focus.

In that case you could attach the files that contain these example snippets.

Doesn't matter if it's alpha/incomplete material. Seeing what range of classes there are provides much better context.

The only question is how much effort it would be to sanitize those to avoid (too many) early spoilers. =)
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 658
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding bits and pieces

PostPosted by Railboy » Sat May 10, 2014 6:56 pm

Good point, the trick is to pick stuff that isn't *so* unfinished that I lead modders astray or waste their time. Forum posts feel appropriately temporary. But looking over the changelog, there are a couple of really stable classes I can start with.

I'll start by posting the speech and book classes. (Speech is used for DTS dialog.) There are three files in each zip - an import file which can be edited in a text editor and imported using the Frontiers data importer, the serialized XML that can be loaded by Mods and the class itself. Both classes are pretty straightforward. The Mission and Conversation classes are pretty stable too, I'll probably upload those next.

(I really need to get some kind of official repository together for this stuff. But for now I'm just going to keep dumping everything in this thread.)
Attachments
Book.zip
(3.8 KiB) Downloaded 10 times
Speech.zip
(2.93 KiB) Downloaded 9 times
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1776
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding bits and pieces

PostPosted by Gazz » Sat May 10, 2014 11:34 pm

Railboy wrote:I really need to get some kind of official repository together for this stuff.

Not much point in that with all alpha data.
As long as it shows the file structure, it'll do.

There isn't much content but looking into these files is already a lot more interesting because it shows data types, possible other tags... and stuff. Stuff is important! =P
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 658
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding bits and pieces

PostPosted by SignpostMarv » Sun May 11, 2014 1:11 pm

Gazz wrote:
Railboy wrote:I really need to get some kind of official repository together for this stuff.

Not much point in that with all alpha data.
As long as it shows the file structure, it'll do.

There isn't much content but looking into these files is already a lot more interesting because it shows data types, possible other tags... and stuff. Stuff is important! =P


I much prefer github, although I'd agree with Gazz that there's little advantage at this point :D

Not-quite-on-topic for modding, but while we're on the subject for repository stuff; You mentioned in the early kickstarter/post-kickstarter stuff (I forget which right now) that you were planning on releasing much of the free materials that you'd used/adapted to develop frontiers. Have you decided upon a release date for such things? i.e. with mod kit or before/after release ?
User avatar
SignpostMarv
Alchemist
Alchemist
 
Posts: 298
Joined: Thu Jul 18, 2013 2:28 pm

Re: Modding bits and pieces

PostPosted by Railboy » Sun May 11, 2014 8:44 pm

I'll release the assets along with the beta. I'll be releasing them as Unity packages on the asset store and as FBX packages on explore-frontiers.com. They'll be updated periodically as I make changes to them leading up to the final release.

We ended up using more paid art assets than I'd anticipated (lots of trees and rocks) so I'm not sure how that's going to work out for modders. I may have to create 'placeholder' assets that modders can work with in the Unity editor that get swapped out for the real deal in-game, because I can't distribute other peoples' assets unless their license permits it. I'll be getting in touch with some of the artists to see if they'd allow me to distribute a Frontiers-specific package that includes their work but I don't know how many will go for it.

Some of the generic code (like the mesh combiner and the audio stack) has been integrated into the Hydrogen framework, so that's already available. The rest will be on github eventually.
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1776
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding bits and pieces

PostPosted by SignpostMarv » Sun May 11, 2014 11:03 pm

Railboy wrote:We ended up using more paid art assets than I'd anticipated (lots of trees and rocks) so I'm not sure how that's going to work out for modders. I may have to create 'placeholder' assets that modders can work with in the Unity editor that get swapped out for the real deal in-game, because I can't distribute other peoples' assets unless their license permits it.


All content is loaded as mod, yus? Mod A contains all placeholder/free assets, Mod B contains all paid assets that, mod loader parses for assets in B that override assets in A so the A assets aren't read from disk/loaded into memory as they're not needed?

p.s. Mod C loads HD textures :P
User avatar
SignpostMarv
Alchemist
Alchemist
 
Posts: 298
Joined: Thu Jul 18, 2013 2:28 pm

Re: Modding bits and pieces

PostPosted by Gazz » Mon May 12, 2014 12:51 am

As long as the assets are part of the game you don't need to release them a second time.
If modders want to use these rocks, they use them directly from the game files.

That's standard procedure. Only models / textures that modders create themselves would be released as part of a mod.
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 658
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding bits and pieces

PostPosted by Railboy » Mon May 12, 2014 1:14 am

Gazz wrote:As long as the assets are part of the game you don't need to release them a second time.
If modders want to use these rocks, they use them directly from the game files.


This would be true if I was using an in-game editor or something proprietary like the TES editor. It gets trickier when I'm counting on modders using the Unity editor, because it isn't capable of plugging into my game and extracting the base assets without actually extracting them into local (re-distributable) files. Not a big deal when we're talking about my structure pieces, which I'm giving away for free. Slightly trickier when I'm using a foliage pack that doesn't include redistribution rights.

There's a chance some of the artists would be cool with it, though. Or maybe I could pay each artist small fee per copy sold, or something. That would probably be worth it just to avoid the hassle of dealing with placeholders.
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1776
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding bits and pieces

PostPosted by Gazz » Mon May 12, 2014 4:59 am

How is it difficult?

If I look into the files that tell the game to place a Shrubbery Type 12, then link to the same resource files from my mod... would it not show a Shrubbery Type 12 ingame without having distributed the actual graphics?
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 658
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding bits and pieces

PostPosted by Railboy » Mon May 12, 2014 5:16 am

Gazz wrote:How is it difficult?

If I look into the files that tell the game to place a Shrubbery Type 12, then link to the same resource files from my mod... would it not show a Shrubbery Type 12 ingame without having distributed the actual graphics?


The problem isn't linking the stuff up at runtime using mod data, that'll all work fine. All standard procedure.

The problem is that when modders are using the graphical editor to generate their data they'll working partly blind since they'll have to use placeholders when manipulating certain objects. They'll show up in the game as the final object, but they'll have to actually launch the game to see what everything really looks like. As an artist that's an annoying pipeline. I want modders / artists to be able to see what their stuff will look like in the editor before they launch the game, just like I can.

If a modder was ultra hardcore and never touched anything but text files they'd never notice a problem.

Have you downloaded the unity editor yet, by the way? You should take it for a spin. :D
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1776
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding bits and pieces

PostPosted by Gazz » Mon May 12, 2014 10:28 am

So the problem is that the editor can not read the game files.
(running an editor for a game without having the game would just be silly)

Alas, I don't know Unity well enough to say anything about possible file format converters to fix that.
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 658
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding bits and pieces

PostPosted by Railboy » Mon May 12, 2014 6:39 pm

Gazz wrote:So the problem is that the editor can not read the game files.
(running an editor for a game without having the game would just be silly)

Alas, I don't know Unity well enough to say anything about possible file format converters to fix that.


No no, it can read them, but only as standalone exported assets. It can't read the compiled assets that come distributed with the game, I'd have to export them into the editor, which violates the redistribution license. Is this making sense yet? Somehow this all got really twisted around.
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1776
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding bits and pieces

PostPosted by Gazz » Mon May 12, 2014 7:09 pm

Yah, makes sense.

The editor is an off-the-shelf Unity tool that cannot use whatever method you use to read compiled assets in the game.
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 658
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding bits and pieces

PostPosted by crash2burn » Wed May 14, 2014 4:37 am

Really great to see game devs exposing this kind of info - keep it comin!
crash2burn
Butterfly Painter
 
Posts: 1
Joined: Wed May 14, 2014 4:35 am

Re: Modding bits and pieces

PostPosted by yarnevk » Wed May 14, 2014 10:44 am

Gazz wrote:Yah, makes sense.

The editor is an off-the-shelf Unity tool that cannot use whatever method you use to read compiled assets in the game.


Elder Scrolls did read its compiled output because it was supporting the free modding community, with the advantage it has customized object viewers so you do not need code to edit an object that is not itself a script, but Unity devs would be mad if anyone could decompile a game or middleware as easily as using the editor, so open source just needs provide the input files for recompiling. I would suspect it is against the EULAs of the game and Unity to try, but you might see if the middlewares have EULA that only require paying if you use it in a commercial game so that you can redistribute them for modders.
yarnevk
Pathmaker
 
Posts: 167
Joined: Tue Sep 17, 2013 2:48 pm


Return to Modding Central

Who is online

Users browsing this forum: No registered users and 0 guests

cron