#API Discussions General
1512 messages · Page 2 of 2 (latest)
use -ids 
How about nesting it under a guid?
no it needs to be guid
because mods will do the same
overwrite each other,
use the same id,
etc...
I feel like it has to be a separate save file
or else you break the original
unless you can append it somehow
without breaking the original
made that yeahg
it has to be invisible to the original parser
well if you are appending it should be invisible data
oh
that way they can remove mods and continue
well I feel like if playing modded, you should be able to switch to unmodded and load successfully
true
could be done w/ an edit to the transpiler
ModData file:
{
"saveFile": "savefile_name_goes_here",
"modData": {
"mod_guid1": {
// serialised data goes here, under the mod's preferred key-value pairs
},
"mod_guid2": {
// ...
},
// ...
}
}
so yeah if that works it should be fine
This should be clean
switching to unmodded ingame is hell
works perfectly
We should make an attribute for save files
I'm not a fan of adding it to the original savefile. If the parser is updated at any point to take it into account, it can corrupt saves.
yeah I think the way bizzle does it is best
I think our api should serialize and deserialize it for the mods though
That's a separate save file though
That runs into the issue where you can't use QoL / fix / performance / graphical mods on vanilla saves
ES3 already done it
bit hacky but
i don't want to get to the hell of serialization
¯_(ツ)_/¯
and made it fool-proof
That's really not an appropriate solution imo.
exactly
good point
config should be used
Having separate ModData allows us to customise things a lot more.
for configs then
What?
Yeah, obviously we should be using the BepInEx.Configuration API
so for configuration stuff
But for modded save data, we shouldn't enforce modded saves
they can be easily disabled
ie:
public class MyMod
{
[SaveData]
public SaveFile Save;
}
public class SaveInfo
{
public string Version { get; set; }
public int Value1 { get; set; }
}
its just kinda a simple switch esp if i make the transpiles dynamic
thats my thought
we can serialize it and deserialize it automatically for the plugin
private static IEnumerable<CodeInstruction> SpoofSaveData(IEnumerable<CodeInstruction> instructions)
{
var codes = new List<CodeInstruction>(instructions);
for (int i = 0; i < codes.Count; i++)
{
CodeInstruction code = codes[i];
if (code.opcode == OpCodes.Ldstr)
{
if (code.operand.ToString() == "LCGeneralSaveData" || code.operand.ToString().Contains("LCSaveFile"))
{
code.operand = "ModSaves/" + code.operand.ToString().Replace("LC", "BC");
}
}
}
return codes.AsEnumerable();
}```
we should also configure it to ignore extra keys, and use defaults automatically for missing keys
could be possible to change that to be dynamic variables
which, if we do that actually allows us to easily add more saves
@versed prairie pushed another so called 'final' commit to move the save directory to ModSaves/
for example filename += pagenum
and having arrows on the slots for selecting a save
then all you have to do is reinit the slots when that changes
yeah
thats a simple solution that allows for inf save
s
well
rest of the ways r kinda a pain
does that save the basegame to one file, and modded to a second
So, what I had in mind for ModData was this:
using API.ModData
public class Plugin
{
private ModData modData = ModData.load(PluginInfo.GUID)
public void SomeMethod()
{
var someValue = modData.getData<SomeModType>("someKey");
}
}
Transparent for the modder, saves to a ModData file, and can be added or removed without breaking vanilla.
nope
effectively it makes 3 alternative saves
to use when modding
like minecraft or tmodloader
then you can just use es3
afaik the existing save system wont care about unknown keys

I figure we just store the data normally, then create a second file for modded.
Whenever the game loads / saves, we can call our custom implementation for saving /loading the modded data separately
ah
so normal players saves can be loaded
then the basegame file is preserved
Thoughts on this? @warped canyon @velvet mirage
Yeah, exactly
but modded saves are seperate?
No, we shouldn't separate modded saves imo
so saving itd be somewhere else
it should be transparent
see this is why i dont wanna do that here ill send an ss of what we'd have to do
uhhh, i used Harmony instance instead of the guid
because overloading
well then you could lose your save file when switching back to vanilla

theres a lot
lot
we would have to fix
this is how other mod systems do it
personally im fine with that
its clean and intuitive
This is my suggestion
so the base save is safe
It's clean, doesn't write into the vanilla save data
Yep, exactly
I think we all need to be conforming
its best for everyone
i agree but this simply isnt the way to do it i feel
Bizzle already uses their own API, so I'm not sure how conforming we can get with them unless they're interested in switching to our ecosystem.
I think it is the safest way
its messier for the end user
I agree a modding community should conform to common APIs as much as possible, to be clear.
my way prevents any save corruption
it can screw with things
We're not writing into vanilla savefiles, so there's no risk of corruption
¯_(ツ)_/¯
i understand yours does this as well
well there wont be corruption if the vanilla saves are unchanged
we dont touch the vanilla system
mods can still corrupt vanilla saves
how?
for example saving settings data anything
like a mod wants to change keybinds to be
imaginary keybinds
ok so
yeah
you can save that
thats so far beyond our scope
then it their fault for writing those to the vanilla saves
hardly feasible

my system requires no work on the modders part whatsoever
Loading some JSON data is amongst the most simple things you can do in programming.
no look
ours would too
look at this idea
ie:
public class MyMod
{
[SaveData]
public SaveFile Save;
}
public class SaveInfo
{
public string Version { get; set; }
public int Value1 { get; set; }
}
ok lets say
custom scrap right
after you add that
you have to spoof this function
here
the save key for scrap is shipScrapValues
uhh, aren't we all arguing thing that is exactly same functionality but different code
to define a save, all the modder would have to do is add a field with the [SaveData] attribute
Alright, can we move on, we're discussing in circles and I'd like to get back to being productive 😅
ok
i see that
my system focuses on extending the existing saved data
ie items and moons
its possible these could be seperate systems
as yours seems focused on saving new items
anyway, how is the result of the lobby metadata you got @versed prairie ?
here ill open a separate discussion
lobby metadatas fun
So, for our core API I assume we're going with the following file format in practice. Correct?
ModData being JSON / serialised data - I don't mind working on that if you don't want to, Ozzy.
@warped canyon @velvet mirage @shell nest @languid tusk
I got sidetracked by this discussion, but so far so good.

I agree
and it would require me to rewrote the entire save
maybe if you inject into es3 it could be feasible
we may want to allow the plugin to deserialize the json
an override method
that way they can parse old saves and convert to new ones
how do we
go speed
and use binary serialization for simple value?
but nvm it too godlike to work with
we only want 'simple' thing
but I think we should have a default converter
Do we want to make the syntax something akin to this?
using API.ModData
public class Plugin
{
private ModData modData = ModData.load(PluginInfo.GUID)
public void SomeMethod()
{
var someValue = modData.getData<SomeModType>("someKey");
}
}
We could add a method like this for custom deserialising, as per Red's suggestion:
var someValue = modData.deserialiseData<SomeModType>("someKey", someDeserialiser);
hmmmm
I must admit I don't fully understand what you mean with this format. Is SaveFile a serialisable format that's saved / loaded into ModData?
ok t here is newtonsoft json
all modded games ive played have done that
ok it possible
withoutt the need to break everything i done and build again from scratch
ah wait
I've barely ever known a modded game that has entirely separate saves for modding. In any case, we don't need to copy another solution if we can validate this is a fine one. Each game is different.
i see the usecase i believe
this would be the whole plugin-sided implementation.
On save load, our api would search for any plugins that use [SaveData] attribute, and load it with the respective deserialized manner
this is for client mods that need to load per save values
ahhhh
ok that makes sense
ok yeah i see the use of this sytem
by alot
yeah this is nescessary

for example custom cozy light colors
I'll get on your review now, Ozzy.
thats a good system
then if the developer wants to specify their own save data deserializer, we pass the json for their key.
@versed prairie so i trash the currentt ModData and make a new one?
Gimme 5 minutes.
which one
If you feel it's necessary, maybe?
i do think that it works w/ the modded save system aswell
your save system
If you do start from scratch, use a branch! @velvet mirage
i think both systems can be utilized as they both have their own use cases
I do waaay to much config serialization, but I have code to do that
Do you want me to review your current PR, or do we close the PR and re-review the new system?
perhaps the best solution is just to make a more saves mod
ill do that
and this api can save
Modded saves really should just be a More Saves mod, yeah.
not a bad idea
ill be using this api youre making for items
More saves is nice 👌
yeah i already have the code for it
hell i can actually put more profiles in there too
if youve noticed some data is excanged between saves
and then a mod can extend functionality
uh welp let first merge the isolation between modded & vanilla player first? dont'?
if you dont mind the request could you add a modded save for LCGeneralSaveData
@velvet mirage did you see my pr for stylecop
if not ill just do it in the official repo
you pr the wrong repo
i do wonder if you could combine the existing es3 system w/ the new modded saves system
yeah
pr the stylecop on the offical repo
I thought you were gonna take your repo and move it to the official, so you could accept my pr then move
for example saves.getkey("SavedItems") would check if thats in modded saves then check the es3 saves
since each pr must a per-feature
because my changes have to take place with the code. The official repo didnt have any code
hmm
so let it come after my pr

Hold up
yeah
thats Why i had to pr to your repo thoughj
whut
strange, i didn't saw your pr
weird
one idea is as follows
mods can read any value from es3 but saving that value writes it to modsaves
the existing es3 system is modified to read from modsaves first before reading from the actual saves to check
anyways
Can you PR the style changes? @warped canyon
this would be simple and adds a lot of possibility
yeah once ozzy is done
half of the pr is xml annotations
Which part are we merging?
/ Are we keeping the current ModData you made?
for example, lets say i need to change this value w/ a transpiler
would there be an easy way to do that via this api
if not is a modification to es3 a better solution?
probably not
maybe moddedsaves could use the es3 system
i will push a commit to delete it
for serializing and deserializing
considering its already there
we will (or atleast I will) likely need to change the loading of a normal value to a "value exists in moddedsaves or exists in normal saves"
utilizing only transpilers
ok updated the pr
Perhaps hold off until the ModData part of the Core lib is ready. Or focus on more saves for now. We can build in support for target savegame paths, so it should be simple for you to add support later
i will do the ModData tomorrow
it 2:50am at my place
gotta slleep
or else i'm gonna burn all of my brain cell

yeah
@versed prairie I talked to the creators of MEC, and they said we can include it in the repo. If we use MEC, we don’t have to make our own timing / asynchronous api.
I’m trying to see if I can also add the pro version if I buy it. (Licensing and adding it to the build may be weird)
Let's hold off on pro versions until we actually get people to use it
Thanks for looking into it, though
Yeah that’s a good point. I’m still gonna see if it’s possible to include it in the future. I’ll add the free version in later today.
I’ll make a pr
Also I’m gonna start hooking a few functions and working on an event api
So that plug-in authors (and us) can hook events
I have a few specific implementations for the events that I’d like to use but I’ll have to make a demo to show it
I made a very basic guide for publicizing in #1173330296809197659
Idk if we could reasonably use the pro version unless we get some special permission. Extension assets have to be purchased by seat (per developer / independent contractor)
I'd like to see a demo, yeah. I'm still not 100% sure what I'd personally use it for, but if it's useful for others, it obviously might have a place in the API.
Never worked with MEC before
It’s pretty great. The only downside I’ve ever had is that coroutines don’t report errors they have. I have a “safe coroutine” that my friend made to fix this problem.
For the events I’m gonna start with saving (ie Saving and LoadingSave) then we can hook those for our custom save options
We could then start branching to player events like DamagingPlayer and PlayerDied
Toss me your git user in DMs and I'll add you to the org, then you can work on a branch on the main repo.
yeah 2018s lc api is super scuffed
¯_(ツ)_/¯
idk i just wanna add all my stuff to it so people only have to install one lmao
i dont use git often so i probably dont see the issues everyone has w/ that approach
but imo it feels like a nice way to avoid the complications that come w/ cross plugin communication
and makes it much simplier for the end user
and I understand that it has previously been argued that the end user's experience is not relevant to the development of mods
this is normally argued as that its because the multi plugin approach is "right for the modding community"
but what are the users if not the modding community itself
¯_(ツ)_/¯
idk lmao
Shimmy posted some nice reasons I agree with, over in #1172376119194951730.
And we're obviously going for a git-first approach to collaboration.
So if you miss context surrounding the benefits of that / miss experience with git, it's understandable.
yeah
We can easily set up git workflow that build into a single .dll, pulling the code from all separate APIs.
There's also Thunderstore which auto-adds dependencies, so multiple APIs is a non-issue for users if we go that route.
But yeah, read Shimmy's reasoning. It's clear and concise imo.
yeah i can see that
just read em
good points
ok cool
here ya are
was made for v38
huge. thanks
should be fine but untested
on save data: make it an option to overwrite vanilla saved w/ modded progress or backup original save and restore it when modding is disabled
Oh, we discussed a design for mod-related data. It's over at https://discord.com/channels/1169792572382773318/1173344541001134100
oh i missed that thread
sounds good to me
I reckon json serializing would be the easiest way to go
Yup, I agree
Sounds good
I'll send my moonapi code if/when it's done
I got that like half sone
Tbh I need saveapi to fin it so
What are you doing in the moon API?
I have some dynamic moon code I used for a mod I'm working on, and it works fine on a vanilla save. (Adds two moons, and some terminal nodes for travel)
👀
What's ConfigPersists?
yo! love to see it
not fully sure of it, though the config value could be set for the local player, the host, or the save game. So config settings could sync across players
for config settings that modify game elements

me when "AAAAAA"
We probably should make a basic player api to help with the items
so you can find a player and do player.giveitem
or something of the sorts
idk exactly whats out there, as there may be a basic api for this already
@versed prairie thoughts on dynamic patching. The api that im basing the event system does this. Basically events don't have to be patched until something hooks onto the event.
Pros & cons?
it is good for updates, because if events break but aren't actually in use, it won't patch them
I can see it improve compatibility if nothing uses the patched method
Possibly performance as well, though likely negligible
but it can also be more complicated and confusing
Could you get a demo / example snippet to showcase how it's done?
as adding and removing transpilers can be questionable
I like the idea in theory. But depending on how it's done, it might add development complexity
public class Event
{
public static Event operator +(Event @event, CustomEventHandler handler)
{
@event.Subscribe(handler);
return @event;
}
public void Subscribe(CustomEventHandler<T> handler)
{
if (Events.Instance.Config.UseDynamicPatching && !patched)
{
Events.Instance.Patcher.Patch(this);
patched = true;
}
InnerEvent += handler;
}
}
public static class EventHandlers
{
// Developers hook to this with the +=
public static Event<Event1EventArgs> Event1 { get; set; } = new ();
// Allows invoking the event from outside the class (inside separate patch classes)
public static void OnEvent1(Event1EventArgs ev) => Event1.InvokeSafely(ev);
}
public class Plugin
{
public Plugin()
{
EventHandlers.Event1 += OnEvent1;
}
public void OnEvent1(Event1EventArgs ev)
{
// Developer runs code
}
}
actually the current implementation is incredibly smart
as I look through it it kinda blows my mind
fairly clean and organized and very well thought through
it does require all of our patches to use an attribute with the respective event type
public void Patch(ILcApiEvent @event)
{
try
{
List<Type> types = new(UnpatchedTypes.Where(x => x.GetCustomAttributes<EventPatchAttribute>().Any((epa) => epa.Event == @event)));
foreach (Type type in types)
{
List<MethodInfo> methodInfos = new PatchClassProcessor(Harmony, type).Patch();
if (DisabledPatchesHashSet.Any(x => methodInfos.Contains(x)))
ReloadDisabledPatches();
UnpatchedTypes.Remove(type);
}
}
catch (Exception ex)
{
Log.Error($"Patching by event failed!\n{ex}");
}
}
Each patch must have the [EventPatch] attribute so the patcher can find it:
[EventPatch(typeof(Handlers.Player), nameof(Handlers.Player.IssuingMute))]
[HarmonyPatch(typeof(VoiceChatMutes), nameof(VoiceChatMutes.IssueLocalMute))]
internal static class IssuingMute
{
}
@shell nest Made a PR. Rider pointed out it's apparently bad practice to instantiate scriptables using new. https://github.com/LethalCompany/LCAPI.TerminalCommands/pull/1
Very simple and minor "fix".
See for context: https://github.com/JetBrains/resharper-unity/wiki/ScriptableObjects-must-be-instantiated-with-ScriptableObject.CreateInstance-instead-of-new
You can review & accept or deny.
yeah same with gameobject, monobehaviours, and components
unity abstracts it so GameObject.AddComponent<T> or GameObject.CreateInstance<T> is the proper way to do it
Yeah, I read up on it a bit when I first saw it. Just been doing it since then, but forgot the actual reasons.
here are my thoughts on the base event types
Door event could be opening / closing / leaving
AttackerEvent - Player v Player, (maybe enemies - this could be separate)
IDeniableEvent - Basic Functions that can be cancelled such as dealing damage, closing doors
IDoorEvent - Opening, Closing, Entering, Leaving
IFirearmEvent - Gun
IHazardEvent - Player steps in cobwebs, mine, or in sight of turret
IItemEvent - Drop, etc...
ILcApiEvent - Basic Event that everything inherits
IPickupEvent - this is probably better just being IItemEvent
IPlayerEvent - Join, Leave, and other general actions
IRagdollEvent - Body Events
IRoomEvent - Steam Event? - could just be added as a hazard
IUsableEvent - Key usage, bell ring, etc...
Can you create a forum thread for this?
yeah'
It's something we can put on the potential TODO. I think it's neat in theory, but it'll require a better understanding of the game's codebase to properly define events and organise it.
This can be my task
I think this adds power to our api
plenty of other cool things, but this is what I think is really important for simplifying the modding process
event based is definitely the way to go
Sure, if you want to work on it, by all means! 😄
didn't even realize TerminalNode was a game object

I correctly named that variable, right? It was a "response"?
I went off of the method's documentation, but wasn't certain
yea, TerminalNodes are command responses
@warped canyon wow, for random unknown reason, my vs won't detect the Assembly-CSharp after your commit
wow
for some random reason again it now working
vs is magic
@versed prairie i have created a new pr for the isolation between vanilla and modded user
Prob env var being funky, as rider also struggled with it until I restarted it
Yeah, Rider was being funky with assembly decompiling until a restart.
Found a small number of issues, but looks good beyond that.
Resolve them and we can merge 👌
uhh, resole what issue tho?
don't see any in the pr comment

Uh, should have requested changes
didn't saw any
is github server being funky?
Gimme a sec
Add isolation between modded and vanilla player.
oh youre doing this a weird way
interesting
not how we've done it in the past
That's just how code reviews & PR's work on Github?
It's still WIP. I'll be expanding it with dependency checks later
ok saw it
and that dosent allow you a custom playerlist without some complicated workarounds
lol, github didn't update it somehhow
Might have been a me-issue
pushed a new commit to the pr
hhmm
might want to add something like a switch (plus counter) that will force a user to modded only when it switched on
not by default enabled like this
I'll update it to use the dependency check system after you've merged @velvet mirage
So a switch won't be necessary then
This is a good base to go from for now
I also think we should use public patches. The TerminalCommands API, and all the relevant docs use it. Best to conform to common standards.
@shell nest or @warped canyon
Can you give a final review? Looks alright to me.
@velvet mirage approved. We can merge, or wait for shimmy or red to have a final look
do you think we need
a switch
for mod to use
that will force player to only able to join modded lobbies?
and it will by default disabled?
instead of enabled like current pr
I'll work on the dependency check after you merge. Then we can use that instead of a toggle.
yeahh
you can merge it now
i will find anything else to work on

Could start work on ModData
You'll want to discuss it with Red. They have a good idea on how to approach it
yup ill check it out
I looked at the terminal api
it looks good
Shimmy's Terminal API is a work of art
(Cleaning up some unrelated-to-our-API messages)
i nutted
I've been thinking
Maybe one of the goals of the API should be to allow complete cross version usability of mods
If only the API breaks between updates
The only one mod needs to be fixed
That's a big part of APIs, yeah
yeah
We're trying abstracts a lot away and act as middleware already
Yeah
The Terminal API is a superb example
So this could effectively turn into a wrapper for the entire game
Really interesting
I do think that's the best solution
Another goal could be to make it so no one needs to use patches
Patches are unavoidable I think, but yeah: the point of APIs is providing as useful of a middleware between the game and mods. Mods should be able to achieve as much as possible using our APIs, since this also improves compatibility.
A strong object-oriented design, and a similar style between all APIs is a must as well. Usage needs to be intuitive between all APIs.
agreed
Need to do something with a terminal? Use the terminal API.
Want to add an item? Use the Item API. Want to additionally add it to the shop? Use the Store API, which bridges between the Item and Terminal API.
Etc
yeah
Once events are done by Red, we can use dynamic patching too. That'll improve compatibility a fair bit.
@quartz raft @tough totem @languid tusk @austere gyro @shell nest @velvet mirage
We'll move API development to a separate server. Easier to make a number of channels / mass ping for votes / etc...
I'll be setting it up properly tomorrow, but here is the invite for now:
Considering the planned size of the project, a forum isn't sufficient anymore to organise this.
