"System mods" vs "Mixins"

Post Reply
User avatar
Ben
Member
Posts: 160
Joined: Tue Mar 31, 2015 20:09

"System mods" vs "Mixins"

by Ben » Post

Hello there,

Summary: how do people share code across mods?

Here's my example: I'm currently implementing my second mod which has what I call "persistent player attributes"; like "hunger", "mana" or "energy". These should be persisted somewhere when the player logs off / the server is shut down, and restored again as needed. My first mod used a text file in the world directory to save these values. My second mod is using a trick I saw elsewhere: create a new inventory slot, and fill it with up to 65535 units of air. Bingo: persisted numeric attribute (yes, this particular thing also sounds like a core engine request, but that's another topic).

So now I have two mods, maybe more, that share code. I see three approaches to this:
  1. Copy and paste the code between the two mods.
  2. Place the shared code in a new "system" mod, and have both mods depend on it.
  3. Generate what I'm calling a "mixin" (if it already has another name, please let me know).
A mixin, in this case, is a separate lua file that gets included in / distributed with each mod, unchanged. Better yet: this file sets up its own global variable, complete with a "version" key, and adds its functionality to the global variable table if the version number is currently lower. In effect, an invisible submodule which can even be updated on the fly by whoever has the most recent version.

My question: does anyone have any thoughts on this? Any experience with this? Any other suggestions? What do other modders do?

Sokomine
Member
Posts: 4276
Joined: Sun Sep 09, 2012 17:31
GitHub: Sokomine
IRC: Sokomine
In-game: Sokomine

Re: "System mods" vs "Mixins"

by Sokomine » Post

Ben wrote: A mixin, in this case, is a separate lua file that gets included in / distributed with each mod, unchanged. Better yet: this file sets up its own global variable, complete with a "version" key, and adds its functionality to the global variable table if the version number is currently lower. In effect, an invisible submodule which can even be updated on the fly by whoever has the most recent version.
Sounds like a good concept. We all more or less suffer from the problem that some solutions are general enough so that they ought to be part of more than one mod - yet inclusion in the main game is tricky. I'd say it envolves over time - it starts with copies, gets its own file, and then perhaps its own mod (so 3 chronologicly before 2).

As for the saving of mod specific values, I have some functions I use in many of my mods. I hope those can eventually be added to builtin (minetest_game won't be sufficient). The functions are simple: They take a table value, serialize it and store it in the worlds folder. Restoring works likewise. See the first two functions here in save_restore.lua. Maybe you can support adding something like this to builtin?
A list of my mods can be found here.

4aiman
Member
Posts: 1208
Joined: Mon Jul 30, 2012 05:47

Re: "System mods" vs "Mixins"

by 4aiman » Post

There are various libraries (str_lib, plant_lib, components_lib, etc) which do not add any specific content or features to the game, but do precisely so in terms of API features.

That's why there is a "dependencies" mechanism for mods. So, nothing new here, sorry.

Spoiler
I'd *like* to suggest making a modpack with various libs in it and upload it to the mmdb...
But I won't.

'Cause there's a little problem: WHO is going to maintain that modpack?
I.e. someone who needs some API func can easily add that func to his/her own copy of downloaded "API pack".
But how he/she is going to share that new func with others?

That is the reason modders started making libs in the first place.
That's why there should be a list of dependencies in the 1st post of a mod release thread.


Expanding builtin can solve some of the issues, but who's gonna maintain newly added API calls?
Would it be the one who has created a specific lib or would it be one of the core devs?

Remember: *any* new feature requires *all* core devs to agree to add it.
Even if they come to agreement on some version of a lib, the same fate is awaiting any future additions.

In a situation like this, if you want to make your lib to be available for all ppl out there, a separate mod with your code would be the best way to share your code: no pre-moderation - no problems with distributing.

Then you'd be able to wait see how many users actually use your mod and decide whether it deemed useful by others or not. (If you'd like to)

But even in the case it would be a "worthy" mod, there's no guarantee it will become a part of default or builtin.
It took a year to add *some* vector calls to builtin.
Plantlife hadn't make it although VanessaE is more than your average MT dev.
Translation APIs got ignored too.

So...
Do you want to wait until 8 or so people would become kind enough to decide for hundreds of thousands and let *some* of your changes in?
While I may be (greatly) exaggerating the whole thing, it's up to everyone to decide whether you want to be in control of your useful API.



Lastly, I won't stop saying that making a subgame is a way to overcome any further difficulties with new API calls.
Grab your "mixin" and make some very basic mod (like default) to load it.

I'm suggesting that because I have some "list" (however vague it may be) of features I'd like to have in Minetest.
It's a good idea to thinks *why* are you coding some particular feature.
Do you have such a list?
If you do, then (maybe) it would be better to use your own API to make incredible subgame with the features only *your* game can afford. And when someone will ask how did you manage to pull all that out, you would be able to proudly present your lib.

If you want version control (in the case some user got your subgame and added some of mods you've created earlier) you can "map" your "mixin" code to the "minetest" (or any other) namespace.
Your mod will be able not only to add functions, but also to substitute older versions of those functions with the new ones in a way no any other mod will notice it!
You can add a version check before "mapping".
TL;DR:
There are plenty of possibilities of sharing your API with the others.
I can't see why someone's API should go into builtin and then suffer from pre-moderation.
Remember the "common" subgame.
It was a failure as a bunch of "system mods".

Do not restrict yourself while making some API and release it as a mod to depend upon.
If others had, there would've been no mobs, no signs with readable text, no plantlife.

User avatar
rubenwardy
Moderator
Posts: 6972
Joined: Tue Jun 12, 2012 18:11
GitHub: rubenwardy
IRC: rubenwardy
In-game: rubenwardy
Location: Bristol, United Kingdom
Contact:

Re: "System mods" vs "Mixins"

by rubenwardy » Post

It sounds like a very bad and hacky idea to use inventories to store integers.

Code: Select all

local data = {
	number = 3
}
local function load()
	local file = io.open(minetest.get_worldpath().."/filename.txt", "r")
	if file then
		local table = minetest.deserialize(file:read("*all"))
		file:close()
		if type(table) == "table" then
			data = table
			return
		end
	end
end

local function save()
	local file = io.open(minetest.get_worldpath().."/filename.txt", "w")
	if file then
		file:write(minetest.serialize(data))
		file:close()
	end
end

load()
print(number)
number = 4
save()
also see viewtopic.php?f=9&t=9276
Renewed Tab (my browser add-on) | Donate | Mods | Minetest Modding Book

Hello profile reader

4aiman
Member
Posts: 1208
Joined: Mon Jul 30, 2012 05:47

Re: "System mods" vs "Mixins"

by 4aiman » Post

Yep, viewtopic.php?f=9&t=9276 shows us that some ideas can wait years to get merged/adopted even if those are quite coder-friendly and useful...


The thing is, it's faster to write smth by yourself than using crappy forum search to find anything of value.
Believe me, I've spent days searching for this or that. But the mod above didn't show up.


The way Rubenwardy is saving data in this snippet produce too much output and serializes what shouldn't be serialized or let alone saved:

Code: Select all

"achievements: shows a list of your achievements in uncanny way...", "You haven't done smth special yet!", " unlocked following:"}}, ["register_achievement"] = loadstring("\27LJ\1\0K@/home/chaiman/magichet-dev/bin/../games/magichet-today/mods/4mcac/init.lua�\1\0\1\4\0\4\0\18\28H\0034\1\0\0007\1\1\0017\2\2\0006\1\2\1\14\0\1\0T\1\5�4\1\0\0007\1\1\0017\2\2\0002\3\0\0009\3\2\0014\1\0\0007\1\1\0017\2\2\0006\1\2\0017\2\3\0009\0\2\1G\0\1\0\9name\12trigger\8ras\
mcac4\1\1\1\1\1\1\1\1\1\1\1\2\2\2\2\2\2\3ac_def\0\0\19\0\0"), ["dug"] = {["Wanderer"] = {}, ["4aiman"] = {}}, ["save_time"] = 10, ["timer"] = 0, ["placed"] = {["Wanderer"] = {}, ["4aiman"] = {["gstone:button_off"] = 19, ["gstone:dust_off"] = 13, ["4stairs:stone_brick_chiseled"] = 21, ["default:dirt"] = 17}}, ["crafted"] = {["Wanderer"] = {}, ["4aiman"] = {["gstone:button_off"] = 49}}, ["got"] = {["Wanderer"] = {["test"] = true, ["check_inv"] = true}, ["4aiman"] = {["test"] = true, ["check_inv"] = true}}, ["events"] = {}, ["load_stuff"] = loadstring("\27LJ\1\0K@/home/chaiman/magichet-dev/bin/../games/magichet-today/mods/4mcac/init.lua�\3\0\0\6\0\17\0008P.\0144\0\0\0007\0\1\0004\1\2\0007\1\3\1>\1\1\2%\2\4\0$\1\2\1%\2\5\0>\0\3\2\15\0\0\0T\1,�\16\2\0\0007\1\6\0%\3\7\0>\1\3\0024\2\0\0007\2\8\2\16\3\0\0>\2\2\1\16\3\1\0007\2\9\1%\4\
\0%\5\11\0>\2\4\1\16\3\1\0007\2\9\1%\4\12\0%\5\13\0>\2\4\0014\2\0\0007\2\1\0024\3\2\0007\3\3\3>\3\1\2%\4\4\0$\3\4\3%\4\14\0>\2\3\2\15\0\2\0T\3\8�\16\4\2\0007\3\15\2\16\5\1\0>\3\3\0014\3\0\0007\3\8\3\16\4\2\0>\3\2\0014\3\16\0004\4\2\0007\4\3\4>\4\1\2%\5\4\0$\4\5\4>\3\2\1G\0\1\0\11dofile\
write\6w\6\
\8')\
...
While "too much" is "too individual", I doubt it's a good idea to serialize functions.
But, since we've got our own preferred methods I'd rather not elaborate what's wrong with this snipped according to my personal view.

One can still safely use Rubenwardy's snippet if there's a table with only the data you'd like to save.

NOTE: minetest.serialize sometimes produces non-de-serialize-able strings if there's mixed types of data in the table that had been serialized. Be extra cautious when serializing tables with positions as a keys.

For me it's no good to have such a "save state" - it's unreadable and it's not going to help "namespaced" mods as the strict.lua sees those. If one uses namespaces - he/she'll know what I'm talking about.

Thus being said, I'm using slightly different approach.
When I want to save stuff I Iterate the table with the names of fields I want to save and then just dump those chosen fields to a file.
The code below is from Magichet/4mcac mod and reproduced here on the same terms as the whole Magichet subgame:

Code: Select all


function mcac4.save_stuff()
   local stuff = ""
   local fields = {'got', 'dug','placed','events','crafted','smelted','closedf','happened'}
   for k,v in pairs(fields) do
       if type(mcac4[v]) == "table" then
          stuff = stuff.."mcac4.".. v .. ' = ' .. dump(mcac4[v]) .. '\n\n'
       end
   end
   local output = io.open(minetest.get_worldpath().."/4mcac.lua", "w")
   if output then
      output:write(stuff)
      io.close(output)
   end
end

function mcac4.load_stuff()
   local input = io.open(minetest.get_worldpath().."/4mcac.lua", "r")
   if input then
      dofile(minetest.get_worldpath().."/4mcac.lua")
   end
end

if not pcall(mcac4.load_stuff) then
   print('Achievements data is corrupted! All 4mcac stuff will be re-initialized.')
end
^ This won't try to serialize every field within mcac4 but the needed only.
It also produces some nice output (see below).

When I need to load stuff, I just execute the file I saved.
This technique produces readable save files with no info you don't need:

Code: Select all

mcac4.got = {
	Wanderer = {
		test = true,
		check_inv = true
	},
	["4aiman"] = {
		test = true,
		check_inv = true
	}
}

mcac4.dug = {
	Wanderer = {
		
	},
	["4aiman"] = {
		
	}
}

mcac4.placed = {
	Wanderer = {
		
	},
	["4aiman"] = {
		["gstone:button_off"] = 19,
		["gstone:dust_off"] = 13,
		["4stairs:stone_brick_chiseled"] = 21,
		["default:dirt"] = 17
	}
}

mcac4.events = {
	
}

mcac4.crafted = {
	Wanderer = {
		
	},
	["4aiman"] = {
		["gstone:button_off"] = 49
	}
}

mcac4.smelted = {
	Wanderer = {
		
	},
	["4aiman"] = {
		
	}
}

mcac4.closedf = {
	Wanderer = {
		[""] = 1,
		["initial ghosts warning"] = 3,
		["initial lang warning"] = 1
	},
	["4aiman"] = {
		[""] = 6,
		["initial ghosts warning"] = 13,
		["initial lang warning"] = 1,
		rules = 19
	}
}

mcac4.happened = {
	Wanderer = {
		
	},
	["4aiman"] = {
		
	}
}

User avatar
rubenwardy
Moderator
Posts: 6972
Joined: Tue Jun 12, 2012 18:11
GitHub: rubenwardy
IRC: rubenwardy
In-game: rubenwardy
Location: Bristol, United Kingdom
Contact:

Re: "System mods" vs "Mixins"

by rubenwardy » Post

You're using it wrong, which is why it saves too much. Pro-tip: use it right. Only save the data you want to save.
Either do namespace.data or do
namespace.one = data.one, etc and { one = namespace.one}
4aiman wrote:

Code: Select all

    function mcac4.save_stuff()
       local stuff = ""
       local fields = {'got', 'dug','placed','events','crafted','smelted','closedf','happened'}
       for k,v in pairs(fields) do
           if type(mcac4[v]) == "table" then
              stuff = stuff.."mcac4.".. v .. ' = ' .. dump(mcac4[v]) .. '\n\n'
           end
       end
       local output = io.open(minetest.get_worldpath().."/4mcac.lua", "w")
       if output then
          output:write(stuff)
          io.close(output)
       end
    end

    function mcac4.load_stuff()
       local input = io.open(minetest.get_worldpath().."/4mcac.lua", "r")
       if input then
          dofile(minetest.get_worldpath().."/4mcac.lua")
       end
    end

    if not pcall(mcac4.load_stuff) then
       print('Achievements data is corrupted! All 4mcac stuff will be re-initialized.')
    end
That's horrific, you basically reimplemented minetest.serialised but less generic.
Renewed Tab (my browser add-on) | Donate | Mods | Minetest Modding Book

Hello profile reader

4aiman
Member
Posts: 1208
Joined: Mon Jul 30, 2012 05:47

Re: "System mods" vs "Mixins"

by 4aiman » Post

What is even more horrific?
That would be the use of minetest.serialize itself.

Which you still do, BTW.

With your "right way" you're:
- still use minetest.serialize
- create unreadable save files
- indexing more tables than you need to
- creating more vars than you need to - one per "localized" field
- saving more files than you need to, thus increasing the load.


What I'm doing is:
- use only one additional variable
- do not index all fields (like minetest.serialize does) but only those needed
- saving only what I need regardless of *how "smart" my usage is*
- save only one file

Not to mention that dump is implemented in C and minetest.serialize in Lua..
Now you can go and brag about how "smart" you're using "more generic" minetest.serialize all you want.

User avatar
rubenwardy
Moderator
Posts: 6972
Joined: Tue Jun 12, 2012 18:11
GitHub: rubenwardy
IRC: rubenwardy
In-game: rubenwardy
Location: Bristol, United Kingdom
Contact:

Re: "System mods" vs "Mixins"

by rubenwardy » Post

- still use minetest.serialize (I'm using a third party library code, it is likely to be well tested and saves development time).
- create unreadable save files (readable code is better than readable files)
- indexing more tables than you need to (I use serialise correctly, unlike you, and only serialise what I need to)
- creating more vars than you need to - one per "localized" field (I assume you're talking about namespace.data.one, you don't have to do that, see below. - also less variables don't indicate easier of reading, although they help with understanding. Unless you're code golfing)
- saving more files than you need to, thus increasing the load. (how am I saving more? I only save one.)

given this namespace:

Code: Select all

local namespace = {
    func  = ...
    one = ...
    two = ...
}
my method:

Code: Select all

local res = minetest.serialise({
    one = namespace.one,
    two = namespace.two
})
your method:

Code: Select all

local fields = {"one", "two", "three"}
local res = ""
for k, v in pairs(fields) do
    res += "namespace." .. v .. " = " .. dump(namespace[v])
end
4aiman wrote:Not to mention that dump is implemented in C and minetest.serialize in Lua..
Wrong. They're both Lua.

https://github.com/minetest/minetest/bl ... s.lua#L123
Renewed Tab (my browser add-on) | Donate | Mods | Minetest Modding Book

Hello profile reader

4aiman
Member
Posts: 1208
Joined: Mon Jul 30, 2012 05:47

Re: "System mods" vs "Mixins"

by 4aiman » Post

Now you've turn constructive and not only offensive.


Still don't agree:

1. Does "likely to be" count as a term? Also, am I not using 3rd party code too? Or the code you're using becomes not-so-crappy automatically?
2. If you can't "read" some portion of the code I've supplied, then how are you manege to discuss it? O_o
3. Do you hear yourself? How do you serialize has nothing to do with what you're indexing.

6. Dump is in Lua. Ok, didn't know that.
So.... basically you're telling me that a 180-lines of code is faster and more readable than less than 40?

However wrong it may be to judge the code by the number of lines, we're talking about 5 times (!) more lines.
BTW, minetest.serialize uses more cycles than dump.



Do agree (after you've posted some more of what you're actually doing):
4. Now this is finally a good example on how one is to serialize stuff. Your first snippet was controversial. But the fact positions as key values are being serialized wrongly. However stupid that may sound to you, Lua supports tables as indexes, so should do minetest.serialize.
5. With the updated code it's clear that you don't. The first sample left the impression I've described.

User avatar
BrandonReese
Member
Posts: 839
Joined: Wed Sep 12, 2012 00:44
GitHub: bremaweb
IRC: BrandonReese
In-game: BrandonReese
Location: USA

Re: "System mods" vs "Mixins"

by BrandonReese » Post

These are the functions I added to the default mod in Adventuretest that simply save a table to a file and retrieve a table from a file.

Code: Select all

function default.serialize_to_file(filename,t)
	local f = io.open(filename, "w")
		f:write(minetest.serialize(t))
		f:close()
end

function default.deserialize_from_file(filename)
	local f = io.open(filename, "r")
		if f==nil then 
			--minetest.log("error","File "..filename.." not found, returning empty table")
			return {}
		end
			local t = f:read("*all")
			f:close()
		if t=="" or t==nil then 
			--minetest.log("error","File "..filename.." is blank, returning empty table")
			return {}
		end
		return minetest.deserialize(t)
end

4aiman
Member
Posts: 1208
Joined: Mon Jul 30, 2012 05:47

Re: "System mods" vs "Mixins"

by 4aiman » Post

I've used a code like that too, BrandonReese until that method failed me when I used a table (position) as a key in the table with my save data.

(It is nice to see some new API added to default).
Adventure test is one of the few games which actually expand API.


Also, the serialized data may be incomplete (due to a server crash) or corrupted.
In the case of incomplete data, I don't know how to do that w/o using regexp, but it would be nice to be able to restore at least something.

User avatar
Ben
Member
Posts: 160
Joined: Tue Mar 31, 2015 20:09

Re: "System mods" vs "Mixins"

by Ben » Post

This thread seems to have digressed a bit towards the concrete example of storing mod data, so I'm going to address that first. For reference, the "first mod" in my example, Thirsty, does loading and saving of per-player values like this:

Code: Select all

function thirsty.read_stash()
    local filename = minetest.get_worldpath() .. "/" .. thirsty.config.stash_filename
    local file, err = io.open(filename, "r")
    if not file then
        -- no problem, its just not there
        -- TODO: or parse err?
        return
    end
    thirsty.players = {}
    for line in file:lines() do
        if string.match(line, '^%-%-') then
            -- comment, ignore
        elseif string.match(line, '^P [%d.]+ [%d.]+ .+') then
            -- player line
            -- is matching again really the best solution?
            local hydro, dmg, name = string.match(line, '^P ([%d.]+) ([%d.]+) (.+)')
            thirsty.players[name] = {
                hydro = tonumber(hydro),
                last_pos = '0:0', -- not true, but no matter
                time_in_pos = 0.0,
                pending_dmg = tonumber(dmg),
                thirst_factor = 1.0,
            }
        end
    end
    file:close()
end

function thirsty.write_stash()
    local filename = minetest.get_worldpath() .. "/" .. thirsty.config.stash_filename
    local file, err = io.open(filename, "w")
    if not file then
        minetest.log("error", "Thirsty: could not write " .. thirsty.config.stash_filename .. ": " ..err)
        return
    end
    file:write('-- Stash file for Minetest mod [thirsty] --\n')
    -- write players:
    -- P <hydro> <pending_dmg> <name>
    file:write('-- Player format: "P <hydro> <pending damage> <name>"\n')
    for name, data in pairs(thirsty.players) do
        file:write("P " .. data.hydro .. " " .. data.pending_dmg .. " " .. name .. "\n")
    end
    file:close()
end
Some explanation: I code Perl for a living, and as such am a bit wary of serializing data as code and eval'ing it to deserialize it (which is what I remember seeing the MT functions do), but I love me some regular expressions, so that's the hammer I use :-P

Still, I need to take care of IO myself, including error handling. In contrast, storing values in hidden inventory fields is very hackish, but I wouldn't call it bad: it uses internal, very well tested code (imagine if inventories "sometimes didn't work"), it does not create any additional files, and is guaranteed to work across all versions, updates or other changes that themselves guarantee to preserve inventories.

Code snippet from the second mod, Beware the Dark, for completeness (needs refactoring, this is all hardcoded to get it out the door):

Code: Select all

-- setup on_joinplayer:
inv:set_size("bewarethedark_sanity", 1)
if inv:is_empty("bewarethedark_sanity") then
    inv:set_stack("bewarethedark_sanity", 1, ItemStack({ name = ":", count = 65535 }))
end

-- get value
local sanity = inv:get_stack("bewarethedark_sanity", 1):get_count() / 65535.0 * 20.0;

-- set value
local inv_count = sanity / 20.0 * 65535.0
if inv_count > 65535 then inv_count = 65535 end
if inv_count < 0 then inv_count = 0 end
inv:set_stack("bewarethedark_sanity", 1, ItemStack({ name = ":", count = inv_count}))
Back to the actual question: Consensus seems to be "make it a library mod to depend on". My problem with this approach is the burden it places on the user of the mod: if [bewarethedark] depends on, say, [playerdata], then every user of [bewarethedark] needs to track down and install [playerdata], and keep it up to date. If the moddb does these installations of dependencies, that helps, but still shows [playerdata] in the mod list, and requires the user (not just server owners, but regular players) to remember to enable it / not to uninstall it, even if they "don't see the point".

(Apologies if I'm getting things wrong, I'm new to Minetest, if that's not apparent by now ;-) )

The appeal of "mixins" for me is that they are invisible to mod users, and are distributed with the mods that use them. Sort of like "header libraries" in C++ (e.g. Boost).

For now, I'm the only one using my code anyway, so I'll go ahead and try out the mixin idea ;-)

4aiman
Member
Posts: 1208
Joined: Mon Jul 30, 2012 05:47

Re: "System mods" vs "Mixins"

by 4aiman » Post

@Ben:
The question was also "what other devs do", so no digressing here ;)

@Everyone
I still don't get what's new about "mixins".
Why including a file w/o which your mod can't even be loaded by MT suddenly turned to be a concept/feature? O_O


Besides, what will you do, if there will be 2 mods with different versions of a mixin and one of the mixins will prevail (because it's of a later version), but the mod with the old mixin appear to be incompatible with the new version of mixin?

Sure, you can update all your mods.
But what a common user should do/think/feel when one of your mods breaks the other one?


There should be more to mixins than it currently is...
Like the ability to check the name and version of the mod which has loaded the mixin.
And someway to determine what mixin versions satisfy the mod.
And if a mod is too old to work with the current mixin then... what?
Does it make sense to upgrade the mod?
What if user don't want to upgrade?
Does it make sense to downgrade the mixin?
Does it make sense to launch the mod which supplied newer mixin with the old mixin just because some older mod can only work with that older mixin it supplied?
What if there IS some version of mixin that can be used with both mods, but it's not available locally?


In other words, depending on a mixin will create more issues than solved problems.

User avatar
Ben
Member
Posts: 160
Joined: Tue Mar 31, 2015 20:09

Re: "System mods" vs "Mixins"

by Ben » Post

4aiman wrote:I still don't get what's new about "mixins".
Why including a file w/o which your mod can't even be loaded by MT suddenly turned to be a concept/feature? O_O
Maybe nothing's new, I dunno. That's why I'm asking ;-) . But the file would be part of the mod, same as "init.lua". MT would not even be able to tell that it's not a regular ".lua" file in the mod, which is included via "dofile" like all the others.
4aiman wrote: Besides, what will you do, if there will be 2 mods with different versions of a mixin and one of the mixins will prevail (because it's of a later version), but the mod with the old mixin appear to be incompatible with the new version of mixin?

Sure, you can update all your mods.
But what a common user should do/think/feel when one of your mods breaks the other one?
Yep, no breaking backwards compatibility, ever. You'd only be able to upgrade in a compatible manner. But isn't that the same with library mods, too?
4aiman wrote:There should be more to mixins than it currently is...
Like the ability to check the name and version of the mod which has loaded the mixin.
And someway to determine what mixin versions satisfy the mod.
And if a mod is too old to work with the current mixin then... what?
Does it make sense to upgrade the mod?
What if user don't want to upgrade?
Does it make sense to downgrade the mixin?
Does it make sense to launch the mod which supplied newer mixin with the old mixin just because some older mod can only work with that older mixin it supplied?
What if there IS some version of mixin that can be used with both mods, but it's not available locally?
Well, except for the last line about local availability, the same holds true for "mods" in place of "mixins", right? Except mods don't even have a system-readable version number, so it can't even be detected automatically.
4aiman wrote:In other words, depending on a mixin will create more issues than solved problems.
Again, I dunno. Most of the problems you've outlined come from backwards-incompatible changes. If those are absolutely forbidden, it should work. But yes, there can be a situation where a bug in one mod's mixin breaks not this mod, but another unrelated mod, and it would be really hard to detect.

I'm currently trying it with two of my mods, just to see if it works, and what happens.

4aiman
Member
Posts: 1208
Joined: Mon Jul 30, 2012 05:47

Re: "System mods" vs "Mixins"

by 4aiman » Post

The thing is, there will be some point in the development where you'd be forced to break compatibility :)

As for mods VS mixins...
I was rather suggesting the directions of development of the mixin idea than performing some criticism.
I'll go through all my statements:

1. Mods should be called by MT, not by other mods. That's why there are dependencies. Different lua files of a mod are being loaded by a dofile, and that's ok. However, in the case of mixins there should be something more than dofile. Some function to search and load a mixin of a MIN<version_needed_<MAX version. That being said, it's the mod responsibility to set those bounds precisely.

2. Mods available in modstore have a version numbers. But you're right. Too bad mods there have no such feature as "telling" MT the version of every dependency they need. I guess current dependency system should be revised.

3. Once again, if mods would've been able to demand a specific version of another mod - that would be cool. But in case of mod+mod there will be some error if those are not compatible. Mixin will give you that too, but in the case mixin is the problem we'll have not a "conflict" of two mods but a situation in which one mod changes the other "on the fly".
Moreover, how would one distinguish a mod and a mixin? Mods can't have same names, but mixins would be capable of that. Should there be 2 mixins with the same name and version but different "inside", user may well be left in frustration.

Thus being said, there should be some way to identify mixin by it's unique ID. Only then a version number detection should be able to kick in.

4. This one is already about a mod. But I'm interested in the answer nevertheless. Users won't like to answer the question about upgrade every time. But not upgrading or auto-upgrading are not better. Any suggestions?

5. See 4.

6. IF the mixin's author would be able to support backward-compatibility this may never become an issue. Mind the "if" clause. Well, time only knows. :)

7. If some mod A loads some mixin AA and AA overrides some mixin BB (loaded by a mod B), then there may arise a situation in which mod B would work, but not in the way mod A expects mod B to do it.
In other words, mod A may expect mod B to react to ALL params of some function, but mod B won't be able to do that - only because it doesn't know about all parameters.
This is an issue of design, not the actual code.
Pointing it out with the hope it'll help to not make those kind of errors.
------------------------------------------


When you'll finish tests, please, return here and tell ppl about your experience with mixins.

Regards!

User avatar
Ben
Member
Posts: 160
Joined: Tue Mar 31, 2015 20:09

Re: "System mods" vs "Mixins"

by Ben » Post

Alright, I've got some test instances: the mods Beware the Dark, version 0.2.1 and Thirsty, version 0.10.0 both use the same "persistent player attributes" mixin.

Note that both mods include the (currently) identical file "persistent_player_attributes.lua". Yes, they can both work at the same time (these two mods together overlap their hud elements, don't mind that though :-P) And yes, I've already thought of a (real!) improvement to the mixin, which I should be able to add to one of the mods, and both should use it.

That, by the way, is the main reason that "searching for a fitting version number" does not happen – all mods always include at least the version they need. Like console operating system updates on console game discs, if you're familiar with those: if a game needs a newer OS version than is currently installed, it gets installed from the disc then and there, and all other (older) games work with the new OS version. There is no search, no dependency management – what is needed is always available and will be used.

4aiman
Member
Posts: 1208
Joined: Mon Jul 30, 2012 05:47

Re: "System mods" vs "Mixins"

by 4aiman » Post

Well, IF you'll manage to keep backward-compatibility, then no search is needed ;)

But there are so many cases where some games refused to launch after OS upgrade, that I doubt there's a 100% way to avoid that.

User avatar
Ben
Member
Posts: 160
Joined: Tue Mar 31, 2015 20:09

Re: "System mods" vs "Mixins"

by Ben » Post

I have an actual test case for version upgrading now: Beware the Dark, version 0.3.2 has version 1.0.1 of the "persistent_player_attributes" mixin. It can function alongside the Thirsty mod, which still has version 1.0.0 .

My conclusion: it's not worth it.

Writing version 1.0.1 to override just those parts of 1.0.0 it needed was not easy. It also completely disorganised the code. And I met an effect I was not aware of: if Thirsty (using 1.0.0) loads before Beware the Dark (using 1.0.1), then any calls Thirsty makes to the shared mixin right away will use the 1.0.0 functions. Later calls (after Beware the Dark has loaded) will use the 1.0.1 functions.

Keeping this all straight is a major headache. I'll change the functionality from a shared mixin to simple duplicate code soon; at most as an optional system mod with local fallback. But I'm giving up on the mixin idea.

Thanks for listening!

4aiman
Member
Posts: 1208
Joined: Mon Jul 30, 2012 05:47

Re: "System mods" vs "Mixins"

by 4aiman » Post

Wow!
Thanks for your reply, Ben!

Maybe a mixin should be loaded via minetest.after(0,...)?
First all the mods are getting loaded and store a path and a version somewhere.
Then minetest.after decides which version to use.

Too bad this is not going to work with any registration.
Maybe if there would be a real-time registration of items, that would help.


Some thoughts on mixins regarding a game (not a set of separate mods) development:

It's a good thing that while developing a game one does not need to have any place to store settings at some "exterior place".

I'm using the default.* namespace to provide basic functions and every other function that should be used everywhere was moved to the default/functions.lua:
- minetest.on_dig from enchantments, specialties, ghosts, exploration, gstone, etc were merged into just one function
- the same goes for on_place


on_globalstep registrations appeared to be somewhat difficult to merge due to using namespased variables.

A little example:
On_globalstep in "ghosts" mod needs the ghosts.is_ghost variable.
There's no problem to move that to default and thus move all registration to the default/player.lua

But when in comes to specialized things like ench.boons table is becomes harder to move the registration - there are too many things to be moved that just won't fit the idea of a separate player.lua.

And then I had a great idea.
Every other mod does smth like this

Code: Select all

ghosts = default      -- the "ghosts" "namespace" is being "mapped" to the "default" one
ghosts.is_ghost = {}  -- now we can easily add any fields and safely use those within both "default" and "ghosts"
When the game complains about some variable being uninitialized, I can add the initialization wherever I want.
The only criteria here is "Where that variable should be?" (e.g. is it a common player/mob attribute or some mod's internal stuff?)

It is also true that I've forced myself to have differently named fields within all of connected mods, but that actually helps me to remove duplicates and/or merge several similar functions into one.

Post Reply

Who is online

Users browsing this forum: Google [Bot], rudzik8 and 2 guests