Jump to content

Persistent variables in InternalModule


Recommended Posts

So I'm writing an InternalModule, and I want a variable to be persistent.

If that were a PartModule, that would not be a problem, because I'd just have to [KSPField(isPersistant = true)] and it would get saved. But the state of InternalModules and internals themselves does not get natively saved in the persistence files, so that gets me nothing.

There are a lot of complicated ideas floating around in my head about getting some other component to deal with the saving for me, but I can't shake off the feeling that I'm missing some simple mechanism that would let me do without other components.

Any advice?

Link to comment
Share on other sites

I'm not sure if it is the 'correct' way, but on my Touchscreen mod to save my window positions and size I used the config nodes to read variables from and save them to a .cfg file in my mod directory.


OSKNode = ConfigNode.Load(KSPUtil.ApplicationRootPath + "GameData/Diazo/OSK/OSK.cfg"); //load OSK.cfg file from my mod directory as confignode named OSKNode
OSKWinScale = Convert.ToInt32(OSKNode.GetValue("OSKWindowScale")); //read value 'OSKWindowScale' from OSK.cfg into OSKWinScale variable. Note confignodes use strings, so I'm converting it from a string to integer here.
OSKNode.SetValue("OSKWindowScale", OSKWinScale.ToString()); //write the value of OSKWinScale variable to the OSKWindowScale value in the confignode OSKNode, coverting from type Int to Str
OSKNode.Save(KSPUtil.ApplicationRootPath + "GameData/Diazo/OSK/OSK.cfg"); //save the value of OSKNode in memory to disk so that it saves when KSP closes.

This code refences file OSK.cfg which is a text file that contains:


OSKWindowScale = 2

More variables can be added on additional lines in the OSK.cfg file.

It is pretty straightforward. The only catch is that they have to be strings when saved to the config node so this method does (probably) require conversion of data types.

The other limitation is that this is global across all profiles.

That works for my Touchscreen mod as that will be the same across profiles, but for something like a resource mod that changes on different profiles you would have to save the file to each profile somehow.

D.

edit: Hmm. Just looked up InternalModule. You would have to create a new value in the confignode for each part somehow? Maybe the PartID?

Edited by Diazo
Link to comment
Share on other sites

It looks like the classes were created with the same save system in mind as PartModules use, with Save(cfgNode), OnSave(...), Fields and all that stuff, but i couldn't get OnSave to execute, so they might just haven't finished it. I think thats kinda good... since adding internal modules to a save generated by this stupid cfg-node-system would have bloated it even more. Anyway, the only reasonable way i see to get you data into the games save file is by using a PartModule for it...

Ofc using an external file as suggested by Diazo is also an option, though might not optimal for vessel-related status data. For example loading the correct data for each of the save files.

Link to comment
Share on other sites

External file, as well as all the mechanics for storing data directly in the persistence file are going to totally suck for vessel related data, because then I have to care about vessels. :) The smoothest idea I had so far was to AddModule a module to the part that holds the internal and use that, but modules added with AddModule are not persistent.

So I guess I'll have to add one directly. :(

Link to comment
Share on other sites

If you want to roll your own persistence handling, XmlSerializer is pretty handy (assuming it is allowed in KSP).

Here's a quick example:


public class PersistentRasterPropMonitor
{
private string _transformName;
private string _textureLayerID;
private Transform _fontTransform;
private Color _blankingColor;
private int _screenRows;
private int _screenColumns;
private int _screenWidth;
private int _screenHeight;
private int _fontWidth;
private int _fontHeight;

public PersistentRasterPropMonitor(string transformName,
string textureLayerID,
Transform fontTransform,
Color blankingColor,
int screenRows,
int screenColumns,
int screenWidth,
int screenHeight,
int fontWidth,
int fontHeight)
{
this._textureLayerID = textureLayerID;
this._fontTransform = fontTransform;
this._blankingColor = blankingColor;
this._screenRows = screenRows;
this._screenColumns = screenColumns;
this._this._screenWidth = screenWidth;
this._this._screenHeight = screenHeight;
this._fontWidth = fontWidth;
this._fontHeight = fontHeight;
}

public string TransformName;
{
get { return _transformName; }
set { _transformName = value; }
}

public string TextureLayerID;
{
get { return _textureLayerID; }
set { _textureLayerID = value; }
}

public Transform FontTransform;
{
get { return _fontTransform; }
set { _fontTransform = value; }
}

public Color BlankingColor;
{
get { return _blankingColor; }
set { _blankingColor = value; }
}

public int ScreenRows;
{
get { return _screenRows; }
set { _screenRows = value; }
}

public int ScreenColumns;
{
get { return _screenColumns; }
set { _screenColumns = value; }
}

public int ScreenWidth;
{
get { return _screenWidth; }
set { _screenWidth = value; }
}

public int ScreenHeight;
{
get { return _screenHeight; }
set { _screenHeight = value; }
}

public int FontWidth;
{
get { return _fontWidth; }
set { _fontWidth = value; }
}

public int FontHeight;
{
get { return _fontHeight; }
set { _fontHeight = value; }
}
}

private static void Serialize(List<PersistentRasterPropMonitor> monitor)
{
XmlSerializer serializer = new XmlSerializer(typeof(List<PersistentRasterPropMonitor>));
TextWriter textWriter = new StreamWriter(KSPUtil.ApplicationRootPath + "Monitor/Monitor.xml");
serializer.Serialize(textWriter, monitor);
textWriter.Close();
}

private static List<PersistentRasterPropMonitor> Deserialize()
{
XmlSerializer deserializer = new XmlSerializer(typeof(List<PersistentRasterPropMonitor>));
List<PersistentRasterPropMonitor> monitor;
using (TextReader textReader = new StreamReader(KSPUtil.ApplicationRootPath + "Monitor/Monitor.xml");
{
monitor = (List<PersistentRasterPropMonitor>)deserializer.Deserialize(textReader);
textReader.Close();
}

return monitor;
}

Monitor.xml output:


<?xml version="1.0" encoding="utf-8"?>
<PersistentRasterPropMonitor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<TransformName>aTransformName</TransformName>
<TextureLayerID>aTextureLayerID</TextureLayerID>
<FontTransform>whateverTransform.ToString()Returns?</FontTransform>
<BlankingColor>Black</BlankingColor>
<ScreenRows>25</ScreenRows>
<ScreenColumns>80</ScreenColumns>
<ScreenWidth>800</ScreenWidth>
<ScreenHeight>600</ScreenHeight>
<FontWidth>7</FontWidth>
<FontHeight>14</FontHeight>
</PersistentRasterPropMonitor>

Link to comment
Share on other sites

If you want to roll your own persistence handling, XmlSerializer is pretty handy (assuming it is allowed in KSP).

I'm not above doing so, but I definitely don't want to deal with per-vessel (and even per-part, in this particular case) data myself, so it's more so about where to store it than how to serialize it.

Link to comment
Share on other sites

If it needs to be persistent per-save (so different for quicksave/persistent.sfs), the best way I've found is subclassing ScenarioModule and handling your business in the OnSave/OnLoad functions there. You'll still have to deal with matching the data to a vessel and part but that's easily done with the vessel's GUID (id) and part flightIDs

Link to comment
Share on other sites

Are partIDs unique per part globally, or only within the flight? Do they ever change?

And most importantly, how do I find which parts no longer exist anywhere?

flightIDs don't change between loads (but constructID does). I don't know if they're globally unique or not. Probable but unconfirmed. As for finding parts which no longer exist, that might not even be necessary depending on how the game saves. But you could easily look through existing vessel guids and part flightIDs; if you don't find the one you're looking for it's gone.

Here's some untested code I hacked together that might help you decide if you want to try this route. Note that I've never worked with Internal* stuff before so it might need fixing. It it works, every time the game saves, your module's OnSave should be called. If you're really lucky, KSPFields might work as well.

[KSPAddon(KSPAddon.Startup.Flight, false)]
public class InternalModuleSaverScenarioCreator : MonoBehaviour
{
public void Start()
{
bool scenarioExists = !HighLogic.CurrentGame.scenarios.All(scenario =>
scenario.moduleName != typeof(InternalModuleSaver).Name
);


if (!scenarioExists)
{
try
{
Debug.Log("Adding InternalModule scenario to game '" + HighLogic.CurrentGame.Title + "'");
HighLogic.CurrentGame.AddProtoScenarioModule(typeof(InternalModuleSaver), new GameScenes[1] { GameScenes.FLIGHT });
// the game will add this scenario to the appropriate persistent file on save from now on
}
catch (ArgumentException ae)
{
Debug.LogException(ae);
}
catch
{
Debug.Log("Unknown failure while adding scenario.");
}
}
Destroy(this);
}
}

/// <summary>
/// The main purpose of this module is to get our paws on the game's ConfigNode
/// being saved to so we can store our own data in there as well.
/// </summary>
public class InternalModuleSaver : ScenarioModule
{
public override void OnSave(ConfigNode node)
{
base.OnSave(node);

if (HighLogic.LoadedSceneIsFlight)
{
/* Node structure (as I would envision it)
*
* SCENARIO
* {
* name = blah // here by default
* scene = 7 // here by default
*
* VESSEL_INTERNAL // keep internal modules for each vessel organized separately
* {
* guid = vessel's guid here: if we can't find the vessel it's gone and none of these
* inner nodes are relevant
*
* YOURINTERNALMODULE // an instance of your module that combines identification and data
* {
* flightID = part this module is attached to (may be unnecessary)
* propID = id or other way of identifying which InternalProp the module is attached to
*
* DATA
* {
* your stuff here
* }
* }
* }
* VESSEL_INTERNAL {} etc
*/

foreach (Vessel vessel in FlightGlobals.Vessels)
{
ConfigNode vesselNode = node.AddNode("VESSEL_INTERNAL");
vesselNode.AddValue("guid", vessel.id.ToString());

foreach (Part part in vessel.parts)
foreach (InternalProp iprop in part.internalModel.props)
foreach (var yourModule in iprop.internalModules.OfType<YourInternalModule>())
{
ConfigNode internalModuleNode = vesselNode.AddNode("YOURINTERNALMODULE");
internalModuleNode.AddValue("flightID", part.flightID);
internalModuleNode.AddValue("propID", iprop.propID);

yourModule.Save(internalModuleNode.AddNode("DATA"));
// save or OnSave? maybe try both
}

}
}
}

public override void OnLoad(ConfigNode node)
{
base.OnLoad(node);

// reverse OnSave here
}
}

Link to comment
Share on other sites

Ooh, thanks. :)

I actually wrote a few pages of ScenarioModule that would keep the data for my InternalModules (A horrible way to treat C# but it should at least work) and then realized it's not loading and isn't even supposed to load outside a scenario. That kickstarting MonoBehaviour is the missing bit.

Link to comment
Share on other sites

I actually wrote a few pages of ScenarioModule that would keep the data for my InternalModules (A horrible way to treat C# but it should at least work)

I know, but I couldn't find any other decent way to get into the persistent file. When you say "keep its data" you actually mean just resolving which ConfigNode goes with which internal module, right? You shouldn't need to keep any InternalModule data in there. Aside from writing the load function, InternalModuleSaver is complete.

Edited by xEvilReeperx
Link to comment
Share on other sites

When you say "keep its data" you actually mean just resolving which ConfigNode goes with which internal module, right? You shouldn't need to keep any InternalModule data in there. Aside from writing the load function, InternalModuleSaver is complete.

No, initially I wrote a dangling data structure for general variable storage. I even got it to serialize, but now I have problems getting at the ScenarioModule to call it's getter/setter functions. :) But let me explain the situation in more detail...

So I have N InternalModules which live inside an InternalProp each, within the same InternalPart, which is hanging on a Part. Let's say they're all displays.

Now, every display has a current page that it is displaying. IVAs get recreated wholesale pretty often, so whenever you come back to IVA from pretty much anywhere, the state of the entire room is a blank slate, and if you changed any current pages, they're back to the ones they got on init. I want them to remember the pages they got switched to, and since I also have a boatload of animated switches (light switches in particular) which also want the same feature, I need a general mechanism to save that somewhere -- it needs to be accessible from multiple diverse modules, just one won't do, so I'd need to figure out how your code works first anyway.

Regardless, I think I found something that prevents going that route at all:

InternalModules actually appear to initialize after KSPAddons ...but before ScenarioModules.

O_o

Link to comment
Share on other sites

Are partIDs unique per part globally, or only within the flight? Do they ever change?

And most importantly, how do I find which parts no longer exist anywhere?

PartIDs (you mean where within the PART module it says name = xxxxx right?) do not change at all. If a craft uses radiator1 on one of its parts, it's the same for every radiator1 on that craft and on every other craft ever.

There are two possible exceptions that probably don't matter to you.

#1 - within a save file (*.sfs) underscores are represented by periods.

#2 - within a craft file, every part name has an apparently random digit appended to it.

Not sure what you mean by the second question....? Parts in the save file that you no longer have the part files for? Or do you mean something else?

Link to comment
Share on other sites

it needs to be accessible from multiple diverse modules, just one won't do, so I'd need to figure out how your code works first anyway.

The example code bothers with flightIDs for exactly that reason. Edit: I see what you're saying now. What prevents you from making relevant InternalModules share a common interface?

InternalModules actually appear to initialize after KSPAddons ...but before ScenarioModules.

That shouldn't matter. My understanding was that they don't receive OnLoad/OnSave at all, but even if they do and calling them again from ScenarioModule somehow screws things up, you could just move your persistent logic to a function unique to your class (CustomLoad/CustomSave) and then call those functions in ScenarioModule:OnSave instead.

Edited by xEvilReeperx
Link to comment
Share on other sites

PartIDs (you mean where within the PART module it says name = xxxxx right?)

No, I mean whatever gets saved in persistent.sfs as "uid" for example. Upon further examination, it seems they're probably unique within the ship, but not globally.

The example code bothers with flightIDs for exactly that reason. Edit: I see what you're saying now. What prevents you from making relevant InternalModules share a common interface?

This being my first week with C#/.NET, mostly. I barely know where most of my usual tools are, let alone the unusual ones, and I'm not really a programmer in the first place. :)

In the end I decided that this whole thing is overkill, though. Props that use InternalModules don't exist outside IVAs, whenever one is working with an IVA, they also can modify the command pod this IVA belongs to and add a PartModule to it. Scenario modules just make it easier for me to screw up, or worse, screw something else up. Using a PartModule for storage leaves me only with the problem of how to get an arbitrary dictionary of name=value pairs to store with a KSPField, because my InternalModules know better when they need to store something and when they don't.

Even that proved more complicated than I expected. Trying to mimic all the available examples that use KSPField to store an arbitrary type, like this one, for some reason failed to work at all (Load/Save of the class just don't get called and I can't see what am I doing differently that could cause this) and I couldn't find a working example among all the currently working mods with a published source that I could remember. Because OnLoad/OnSave get called after loading and saving of the regular KSPFields is done, I couldn't just pack a dictionary into a string right before it's to be saved either.

I had to basically settle for unpacking that dictionary from a string every time I store a variable and packing it back immediately, which is crude beyond words, but at least works reliably.

Link to comment
Share on other sites

whenever one is working with an IVA, they also can modify the command pod this IVA belongs to and add a PartModule to it.

If you can make PartModule work, that's absolutely the way to go. I've never worked with IVA stuff so I had no idea you could use PartModules there.

like this one, for some reason failed to work at all (Load/Save of the class just don't get called and I can't see what am I doing differently that could cause this) and I couldn't find a working example among all the currently working mods with a published source that I could remember.

As far as I know, nobody has managed to get custom KSPField types to work sadly. I'd love to be wrong.

Link to comment
Share on other sites

If you can make PartModule work, that's absolutely the way to go. I've never worked with IVA stuff so I had no idea you could use PartModules there.

You can't. :) An InternalModule can pretty easily find a PartModule that lives inside a specific part and make use of it's KSPField indirectly, though.

As far as I know, nobody has managed to get custom KSPField types to work sadly. I'd love to be wrong.

Gah!

Link to comment
Share on other sites

What was the problem with a PartModule? Just make sure its properly added to the parts prefab on ksp startup, either by adding it to your own parts cfg, using ModuleManager or directly manipulating PartLoader.LoadedPartsList[yourPartIndex].partPrefab, and it should load its data accordingly, just like any other PartModule. Ofc there are a few drawbacks... adding modules to foreign/a lot of parts can cause corrupted save games when adding mods (since the order of modules matters when loading) or compatibility issues in general. But it should work just fine for your own part. Saving data for multiple InternalModules in a single PartModule might be a little tricky, since you might not have KSPFields for an unknown number of InternalModules, but in the worst case you can still use config nodes directly... (even if IConfigNode would work, it would be pretty much the same amount of work anyway)

Link to comment
Share on other sites

There were two problems, the first being that I wanted to avoid making the user do meaningless configuration, because I have far too much meaningful configuration for the user to do. :)

The second one was that IConfigNodes don't work and OnSave/OnLoad execute after Save/Load are done, so I can't use them to serialize/deserialize something that will be kept in a KSPField string, and I don't know enough about how to properly manipulate config nodes directly from those in a different way.

Well, regardless, it's horrible but it works: https://github.com/Mihara/RasterPropMonitor/blob/master/JSIInternalPersistence.cs

Link to comment
Share on other sites

This thread is quite old. Please consider starting a new thread rather than reviving this one.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...