Jump to content

[Modded Skins API Tutorial] Creating Custom Skins for Mods


Hornete
 Share

Recommended Posts

3 hours ago, Garamonde said:

Thank you! I understand, you gotta do what you gotta do for the better! But uh, having said that, I now have an actual problem with loading my mod:

[00:00:05]: [string "../mods/K_K/modmain.lua"]:69: attempt to call global 'AddSkinnableCharacter' (a nil value)

You're attemping to call the function "AddSkinnableCharacter", this is a remnant from my old tutorial, removing this call from line 69 of your modmain should fix the issue, :)

  • Thanks 1
Link to comment
Share on other sites

Ahh, thank you! I think the only problem I'm having now is with filenames not matching. I'm getting dedicated server failed to start errors that tell me it "can't find 'kk_ghost' in any of the search paths," for example. And just to see if it'd work, I even manually renamed my .ZIP builds like this, to match your code in the OP:

image.png.006718ed31fd68239d71e78c120d216e.png

...With the "ms_" prefacing the alternate skin for my character. Only trouble is in doing that, the autocompiler tries to remake the ANIM builds and have them have "_build" appended to the names, just as the ESC mod tends to do. (The game also crashes upon trying to launch a server once I do this) Here is another log, I didn't see anything obvious in it, but hopefully it helps. Also hopefully that all makes sense, and sorry about the trouble!

master_server_log.txt

client_log.txt

Link to comment
Share on other sites

36 minutes ago, Garamonde said:

Ahh, thank you! I think the only problem I'm having now is with filenames not matching. I'm getting dedicated server failed to start errors that tell me it "can't find 'kk_ghost' in any of the search paths," for example.

May you show me your CreatePrefabSkin call? The part of the code where you're creating your skin.

For example:

table.insert(prefabs, CreatePrefabSkin("ms_whimsy_victorian", {
	assets = {
        Asset("ANIM", "anim/ms_whimsy_victorian.zip"),
        Asset("ANIM", "anim/ghost_whimsy.zip"),
    },
	skins = {
		normal_skin = "ms_whimsy_victorian",
		ghost_skin = "ghost_whimsy",
	},

	base_prefab = "whimsy",
	build_name_override = "ms_whimsy_victorian",

	torso_untuck_builds = { "ms_whimsy_victorian", },
	type = "base",
	rarity = "ModLocked",
	
	condition = {
		--no_gift = true
	},

	skin_tags = { "BASE", "WHIMSY", "VICTORIAN"},
}))

 

  • Like 1
Link to comment
Share on other sites

Sure!

local prefabs = {}

table.insert(prefabs, CreatePrefabSkin("kk_none", {
	assets = {
		Asset("ANIM", "anim/kk.zip"),
	},
	skins = {
		normal_skin = "kk",
		ghost_skin = "ghost_kk",
	},

	base_prefab = "kk",
	build_name_override = "kk",

	type = "base",
	rarity = "Character",

	skin_tags = { "BASE", "kk"},
}))

table.insert(prefabs, CreatePrefabSkin("ms_kk_hallowed", {
	assets = {
        Asset("ANIM", "anim/ms_kk_hallowed.zip"),
        Asset("ANIM", "anim/ghost_kk_hallowed.zip"),
    },
	skins = {
		normal_skin = "ms_kk_hallowed",
		ghost_skin = "ghost_kk_hallowed",
	},

	base_prefab = "kk",
	build_name_override = "ms_kk_hallowed",

	torso_untuck_builds = { "ms_kk_hallowed", },
	type = "base",
	rarity = "ModMade",

	skin_tags = { "BASE", "kk", "HALLOWED"},
}))


return unpack(prefabs)

This is the entirety of my "_none" script file.

Link to comment
Share on other sites

Hmm everything looks like it should be fine on the CreatePrefabSkin's end.

49 minutes ago, Garamonde said:

I even manually renamed my .ZIP builds like this

Ah! If you want to rename your builds, you shouldn't be renaming the compiled zip but rather the scml file name, that is your build name.

image.png.82fc9392f448ccd62fa374764a60d101.png
E.g. my build name here is "ms_example", if I wanted the build name to be "ms_test", i'd rename the scml document to "ms_test"

Make sure to change the folder name it's in too, it should always be matching with the name of the scml document

Be sure to let me know if this helped to solve your issue.

Edited by Hornete
  • GL Happy 1
Link to comment
Share on other sites

20 minutes ago, Garamonde said:

Ohhh, I forgot about the .SCML files! Alrighty, I renamed those and recompiled everything, but the game still crashes... I'll just send my whole folder, if that's more helpful. :-P

K_K.zip 15.51 MB · 1 download

And more logs, just in case:

master_server_log.txt 29.62 kB · 1 download client_log.txt 22.22 kB · 4 downloads

Hm, extremely strange, I'm able to open up a world with your mod and the API just fine and it works perfectly.

unknown.png

Your logs also don't seem to be providing any meaningful information as to what could be crashing you. Would you happen to have any other mods enabled? Client or server? I'd also suggest trying to verify the games files. Your client log has this at the end of it, which looks very strange and seemingly isn't related to any mods but rather assets in the vanilla game are running into errors:

[00:08:55]: ERROR: HWTexture::DeserializeTexture failed on anim/oasis_tile.zip:oasis_tile--atlas-0.tex. glGetError returned 0x505
[00:08:55]: 2048x2048 format: 0x83f3 mips: 12
[00:08:55]: ERROR: HWTexture::DeserializeTexture failed on anim/oasis_tile.zip:oasis_tile--atlas-1.tex. glGetError returned 0x505
[00:08:55]: 2048x2048 format: 0x83f3 mips: 12

 

  • Like 1
Link to comment
Share on other sites

Thank you! Yeah, my logs tend to be unhelpful a lot of the time, for some reason. :-P

So I tried it again, with no mods other than Modded Skins API and K_K, and I got a "ran out of memory and must shut down" crash... Which doesn't make sense given the fact that I can run servers with 19 mods... That's probably an issue with my PC, though.

I think the super-final thing now is that default K_K's ghost doesn't show up on the character select screen (but his alternate skin's ghost does):

image.thumb.png.1234c011d7d44ef745e95c4033d69948.png

image.thumb.png.81fecab82131aacc9d79fd5e1e05ae3b.png

Edited by Garamonde
Added an extra image.
Link to comment
Share on other sites

20 minutes ago, Garamonde said:

I think the super-final thing now is that default K_K's ghost doesn't show up on the character select screen (but his alternate skin's ghost does):

Oh! Did you load K_K's ghost asset? I noticed it's not in the "kk_none" assets table.
 

table.insert(prefabs, CreatePrefabSkin("kk_none", {
	assets = {
		Asset("ANIM", "anim/kk.zip"),
	},
...

--Should most likely be..
table.insert(prefabs, CreatePrefabSkin("kk_none", {
	assets = {
		Asset("ANIM", "anim/kk.zip"),
        Asset("ANIM", "anim/ghost_kk.zip"),
	},

 

  • GL Happy 1
Link to comment
Share on other sites

D'oh! I guess it would help to load the graphics of the ghost... :-P

image.png.9a2e0b016912e0e31931e2b629afbe80.png

There he goes! Thank you so much for your help!! I'll be sure to let you know when my mod's done! :D

EDIT: Oh. I just loaded into the world and...

image.thumb.png.6db06a913807f6fd8f586185483b0131.png

He's not showing up and the gold name isn't appearing.

Edited by Garamonde
  • Like 1
Link to comment
Share on other sites

14 minutes ago, Garamonde said:

EDIT: Oh. I just loaded into the world and...

Ah! For the "Thingamabob" thing, you'll need to make strings for the "kk_none" just like you did for the skin I believe

STRINGS.SKIN_NAMES.kk_none = "K_K"
STRINGS.SKIN_DESCRIPTIONS.kk_none = "A descriptor"


As for them being missing in the portrait frame, and having no portrait, AND missing their golden name... I'm not completely sure what causes those, but I'll try to look into it.

  • Thanks 1
Link to comment
Share on other sites

Somewhat good news at least, adding the "_none" skin strings fixed 2 of those issues:

image.thumb.png.8d8780f3956e86e9f55f799e8b471959.png

So now all that's left is the golden name and big portrait. I'm rather baffled at that because it worked prior to me changing stuff to accommodate for the API update... and I didn't change anything regarding either of those (except for the Hallowed Nights portrait filenames).

Link to comment
Share on other sites

2 minutes ago, Garamonde said:

[snip]

Ah! I believe I see what's going on, and I believe it's something on Klei's side actually.

So in that popup screen, that game checks to see if there's a xml file that's in the "bigportraits/characternamehere.xml" portrait, and if that doesn't exist, it defaults to "unknownmod" for the name. These portraits are something like this:

image.png.f8ba93161628c5ab5a51b8b83a29dd14.png

Most modded characters aren't gonna have these however, because, well, they're not used anymore! Only the oval portraits are now used! These rectangular portraits are from a earlier time in DST, and so there isn't and shouldn't be a need for the game to check for these, they should rather be looking for the charactername_none portrait instead.

TL:DR, if you wanna fix it, then you just need to make a new portrait just named "kk.xml" and "kk.tex" in the bigportraits folder, even though these don't get used at all the game thinks your characters assets don't exist for the popup avatar without them :P. I'll try to get a bug report in sometime so the developers could perhaps take a look at this outdated mechanic and make it more modder-friendly.

  • GL Happy 1
Link to comment
Share on other sites

Do I need to rebuild all my old skins if I'm transitioning from the old skins API? I'm worried about the "ms_" prefix requirement you outlined in the main post, I can totally put that in the ID, no problem, but none of my builds have that prefix. It seems to work fine in game, but I'm wondering if this is some Klei rule that I shouldn't violate.

  • Like 1
Link to comment
Share on other sites

8 minutes ago, TheSkylarr said:

but I'm wondering if this is some Klei rule that I shouldn't violate.

Oh no no, don't worry about it, it's just a 'us' thing, not a 'Klei' thing. We actually do put the ms_ prefix automatically for you if you don't have it, but it's in the main post just to let you know incase you relied on the game automatically finding your skin build from the name of the skin and thus need to either re-name the build or set the "build_name_override" in the skin prefab.

Link to comment
Share on other sites

54 minutes ago, Hornete said:

Oh no no, don't worry about it, it's just a 'us' thing, not a 'Klei' thing. We actually do put the ms_ prefix automatically for you if you don't have it, but it's in the main post just to let you know incase you relied on the game automatically finding your skin build from the name of the skin and thus need to either re-name the build or set the "build_name_override" in the skin prefab.

Awesome to know! Thanks for being so responsive!

Link to comment
Share on other sites

Figured I'd share how I saved myself some code. I have a bunch of skins that all use the same config, besides build and ID, so I just wrote a for loop to iterate through a list of my skins and add them with way less code. I still had to do the hatkid_none skin separately, but as far as the actual skins go, I saved myself from doing 9 iterations of the same code. Yay for basic for loops!

local hatskins = {
	"ms_hatkid_cat",
	"ms_hatkid_detective",
	"ms_hatkid_dye_bowkid",
	"ms_hatkid_dye_groovy",
	"ms_hatkid_dye_lunar",
	"ms_hatkid_dye_niko",
	"ms_hatkid_dye_pinkdanger",
	"ms_hatkid_dye_sans",
	"ms_hatkid_dye_toonlink",
}

for k,v in pairs(hatskins) do

	table.insert(prefabs, CreatePrefabSkin(v, {
		assets = {
			Asset("ANIM", "anim/" .. v .. ".zip"),
			Asset("ANIM", "anim/ghost_hatkid_build.zip"),
		},
		skins = {
			normal_skin = v,
			ghost_skin = "ghost_hatkid_build",
		},

		base_prefab = "hatkid",
		build_name_override = v,

		type = "base",
		rarity = "ModMade",

		skin_tags = { "BASE", "HATKID" },
	}))
end

 

  • Like 1
Link to comment
Share on other sites

Another question that has been bothering me is how would I set up the condition(s) for unlocking a locked skin; as for example, let's say I'd like to unlock the skin by digging up enough graves and killing a certain amount of innocent creatures (Glommer, Rabbits, Butterflies, etc.), how would I go about setting that up with using the following:

On 5/26/2022 at 12:33 AM, Hornete said:

Now as for actually unlocking the skin, we’ll be using a RPC that the API adds for you that’ll allow you to pass the id of your skin to be unlocked.


if CLIENT_MOD_RPC[“ModdedSkins”] then --This is necessary to make sure the RPC actually exists as depending on your mod a user may not always have the API enabled.
	SendModRPCToClient(GetClientModRPC("ModdedSkins", "UnlockModdedSkin"), sender_list, "skinid") --sender_list can either be nil to send the rpc to all clients, or be the string for a single clients user id, OR an array that includes multiple user id’s to send the rpc to all the clients bearing those user id’s. Skinid should be filled out with the id of the skin you’d like to be unlocked.
end

 

Link to comment
Share on other sites

14 hours ago, Cagealicous said:

Another question that has been bothering me is how would I set up the condition(s) for unlocking a locked skin; as for example, let's say I'd like to unlock the skin by digging up enough graves and killing a certain amount of innocent creatures (Glommer, Rabbits, Butterflies, etc.), how would I go about setting that up with using the following:

To do the former, you could have your character(or any player with a AddPlayerPostInit) in their fn by listening for the 'finishedwork' event and checking for the target if it's a tag, and if it's a grave, then we can tick up a counter that we initialized earlier. 

inst.gravesdugcounter = 0 --starts at 0
inst:ListenForEvent("finishedwork", function(inst, data)
    if data and data.target and data.target:HasTag("grave") then --Any entity with the grave tag, hey! it's mod compatibility too if any other mod adds a grave with this tag!
        inst.gravesdugcounter = inst.gravesdugcounter + 1 --take the last value of gravesdugcounter and add 1

        if inst.gravesdugcounter >= 10 then --Over OR equal to ten? then pass the check
			if CLIENT_MOD_RPC[“ModdedSkins”] then --This is necessary to make sure the RPC actually exists as depending on your mod a user may not always have the API enabled.
				SendModRPCToClient(GetClientModRPC("ModdedSkins", "UnlockModdedSkin"), inst.userid, "whatevertheskinidofyourcustomskinis")
			end
        end
    end
end)

You can do pretty much the same thing too for innocent creatures, but instead, listen for the 'killed' event!

inst.innocentskilled = 0 --starts at 0
inst:ListenForEvent("killed", function(inst, data)
    if data and data.victim and NAUGHTY_VALUES[data.victim.prefab] ~= nil then --We check if the victim killed has a naughtiness value
        inst.innocentskilled = inst.innocentskilled + 1 --take the last value of innocentskilled and add 1

        if inst.innocentskilled >= 10 then --Over OR equal to ten? then pass the check
			if CLIENT_MOD_RPC[“ModdedSkins”] then --This is necessary to make sure the RPC actually exists as depending on your mod a user may not always have the API enabled.
				SendModRPCToClient(GetClientModRPC("ModdedSkins", "UnlockModdedSkin"), inst.userid, "whatevertheskinidofyourcustomskinis")
			end
        end
    end
end)

 

Edited by Hornete
  • Thanks 1
Link to comment
Share on other sites

2 hours ago, Hornete said:

To do the former, you could have your character(or any player with a AddPlayerPostInit) in their fn by listening for the 'finishedwork' event and checking for the target if it's a tag, and if it's a grave, then we can tick up a counter that we initialized earlier. 


inst.gravesdugcounter = 0 --starts at 0
inst:ListenForEvent("finishedwork", function(inst, data)
    if data and data.target and data.target:HasTag("grave") then --Any entity with the grave tag, hey! it's mod compatibility too if any other mod adds a grave with this tag!
        inst.gravesdugcounter = inst.gravesdugcounter + 1 --take the last value of gravesdugcounter and add 1

        if inst.gravesdugcounter >= 10 then --Over OR equal to ten? then pass the check
			if CLIENT_MOD_RPC[“ModdedSkins”] then --This is necessary to make sure the RPC actually exists as depending on your mod a user may not always have the API enabled.
				SendModRPCToClient(GetClientModRPC("ModdedSkins", "UnlockModdedSkin"), inst.userid, "whatevertheskinidofyourcustomskinis")
			end
        end
    end
end)

You can do pretty much the same thing too for innocent creatures, but instead, listen for the 'killed' event!


inst.innocentskilled = 0 --starts at 0
inst:ListenForEvent("killed", function(inst, data)
    if data and data.victim and NAUGHTY_VALUES[data.victim.prefab] ~= nil then --We check if the victim killed has a naughtiness value
        inst.innocentskilled = inst.innocentskilled + 1 --take the last value of innocentskilled and add 1

        if inst.innocentskilled >= 10 then --Over OR equal to ten? then pass the check
			if CLIENT_MOD_RPC[“ModdedSkins”] then --This is necessary to make sure the RPC actually exists as depending on your mod a user may not always have the API enabled.
				SendModRPCToClient(GetClientModRPC("ModdedSkins", "UnlockModdedSkin"), inst.userid, "whatevertheskinidofyourcustomskinis")
			end
        end
    end
end)

 

Where would I put this in? My character's prefab file, or somewhere else? And, if it is in the prefab file, which part, like masterpostinit, or somewhere near the top?

Link to comment
Share on other sites

2 minutes ago, Cagealicous said:

Where would I put this in? My character's prefab file, or somewhere else? And, if it is in the prefab file, which part, like masterpostinit, or somewhere near the top?

Master postinit of your character file!

Link to comment
Share on other sites

If I was to make a custom skin for a DST character that already exists/that I own and draw the custom skin myself without tracing or reusing designs that already exist would that still be counted as bad? I really want to try to tweak Walter's bee suit to be a little more lively and less plain but have no clue how specific the rules are for making custom skins like that.

Link to comment
Share on other sites

7 hours ago, Hornete said:

To do the former, you could have your character(or any player with a AddPlayerPostInit) in their fn by listening for the 'finishedwork' event and checking for the target if it's a tag, and if it's a grave, then we can tick up a counter that we initialized earlier. 


inst.gravesdugcounter = 0 --starts at 0
inst:ListenForEvent("finishedwork", function(inst, data)
    if data and data.target and data.target:HasTag("grave") then --Any entity with the grave tag, hey! it's mod compatibility too if any other mod adds a grave with this tag!
        inst.gravesdugcounter = inst.gravesdugcounter + 1 --take the last value of gravesdugcounter and add 1

        if inst.gravesdugcounter >= 10 then --Over OR equal to ten? then pass the check
			if CLIENT_MOD_RPC[“ModdedSkins”] then --This is necessary to make sure the RPC actually exists as depending on your mod a user may not always have the API enabled.
				SendModRPCToClient(GetClientModRPC("ModdedSkins", "UnlockModdedSkin"), inst.userid, "whatevertheskinidofyourcustomskinis")
			end
        end
    end
end)

You can do pretty much the same thing too for innocent creatures, but instead, listen for the 'killed' event!


inst.innocentskilled = 0 --starts at 0
inst:ListenForEvent("killed", function(inst, data)
    if data and data.victim and NAUGHTY_VALUES[data.victim.prefab] ~= nil then --We check if the victim killed has a naughtiness value
        inst.innocentskilled = inst.innocentskilled + 1 --take the last value of innocentskilled and add 1

        if inst.innocentskilled >= 10 then --Over OR equal to ten? then pass the check
			if CLIENT_MOD_RPC[“ModdedSkins”] then --This is necessary to make sure the RPC actually exists as depending on your mod a user may not always have the API enabled.
				SendModRPCToClient(GetClientModRPC("ModdedSkins", "UnlockModdedSkin"), inst.userid, "whatevertheskinidofyourcustomskinis")
			end
        end
    end
end)

 

Hmm... that code didn't seem to unlock the skin. I did add prints after each one in the following to check if it was working:

	inst.gravesdugcounter = 0 --starts at 0
	inst:ListenForEvent("finishedwork", function(inst, data)
		if data and data.target and data.target:HasTag("grave") then
			inst.gravesdugcounter = inst.gravesdugcounter + 1
			print("dig")
		end
		
		if inst.gravesdugcounter >= 10 then --Over OR equal to ten? then pass the check
			if CLIENT_MOD_RPC["ModdedSkins"] then 
				SendModRPCToClient(GetClientModRPC("ModdedSkins", "UnlockModdedSkin"), inst.userid, "ms_characternamehere_shadow")
				print("unlock")
			end
		end
	end)

As for my character's ..._skins.lua file I put the following for the character:
 

table.insert(prefabs, CreatePrefabSkin("ms_characternamehere_shadow",
{
	base_prefab = "characternamehere",
	build_name_override = "ms_characternamehere_shadow",
	type = "base",
	rarity = "ModLocked",
	skin_tags = {"BASE", "CHARACTERNAMEHERE", "SHADOW"},
	skins = {
		normal_skin = "ms_characternamehere_shadow",
		ghost_skin = "ghost_characternamehere_build",
		powerup = "ms_characternamehere_shadow_powerup",
	},
	assets = {
		Asset("ANIM", "anim/ms_characternamehere_shadow.zip"),
		Asset("ANIM", "anim/ms_characternamehere_shadow_powerup.zip"),
		Asset("ANIM", "anim/ghost_characternamehere_build.zip"),
	},
}))

It also seems to not show the unlock requirements button too. I put that in modmain.lua along with any other strings.

(Also, that isn't the character's name by the way. Just made it filler for whoever.)

Edited by Cagealicous
Link to comment
Share on other sites

4 minutes ago, Cagealicous said:

As for my character's ..._skins.lua file I put the following for the character:

You need to include the 'condition' table, even if you have no conditions set the table needs to exist.

table.insert(prefabs, CreatePrefabSkin("ms_characternamehere_shadow",
{
	base_prefab = "characternamehere",
	build_name_override = "ms_characternamehere_shadow",
	type = "base",
	rarity = "ModLocked",
	condition = { --Here!

	},
	skin_tags = {"BASE", "CHARACTERNAMEHERE", "SHADOW"},
	skins = {
		normal_skin = "ms_characternamehere_shadow",
		ghost_skin = "ghost_characternamehere_build",
		powerup = "ms_characternamehere_shadow_powerup",
	},
	assets = {
		Asset("ANIM", "anim/ms_characternamehere_shadow.zip"),
		Asset("ANIM", "anim/ms_characternamehere_shadow_powerup.zip"),
		Asset("ANIM", "anim/ghost_characternamehere_build.zip"),
	},
}))

 

  • Thanks 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
  • Create New...