Jump to content

[Tutorial]Creating Modded Skins using Glassic API in DST and Skin System in DS


Rickzzs
 Share

Recommended Posts

links:

Glassic API for DST skin, open source

https://steamcommunity.com/sharedfiles/filedetails/?id=2521851770

Skin System for DS skin, partially open source

https://steamcommunity.com/sharedfiles/filedetails/?id=2883799470

contents:

1.A Brief Introduction of Skin in DS/DST

2.Drawing Variants

3.Applying Variants

4.Drawing Skins

5.Registering Skins

6.Applying Skins

1.A Brief Introduction of Skin in DS/DST

A skin in DST/DS means a different texture. Most skins only have a texture, some with different animation, some with visual fx and sound fx.

Something less than skin, I would call it variant, is simply a different texture. For example, merm guard is a variant of merm, pig guard is a variant of pigman. Just because their texture is so similar that it cannot be called a skin.

Skins in DST are split into some categories. The bulk of skins is items, and then the head bases and clothes. There are other kind of skins like player portraits, profile flairs, loading backgrounds, emoji, emotes, etc., but I'm not talking about these skins.

Today DST has about 500+ item/head skins and ~1000 clothes. Their textures are stored in data/dynamic/*.dyn and databundles/anim_dynamic.zip/anim/dynamic/*.bin. These assets , the former encrypted and the latter not, cannot be directly used. However, you can screenshot their in-game look in the curios.

To make your own skin, you can start by drawing your own texture first. And then you register your skins into the game, using either Modded Skins or Glassic API. Finally you apply your skin to an item.

The existence of an API in DST is because the official system won't allow a custom skin to be owned. The existence of an API in DS in because DS never had a skin system.

  • Like 1
Link to comment
Share on other sites

2.Drawing Variants

2.1Find Assets

Here I will show how to draw the variants of Replica Relic Dish.

ruinsrelic_chipbowl

Firstly you need to use search for the texture name of the dish. I recommend you use Everything, File Locator Pro and AnyTxt.

I search in databundles/scripts.zip/languages/, and find the "Replica Relic Dish"'s prefab name is "ruinsrelic_bowl".

Secondly you search the prefab. I find this prefab inside databundles/scripts.zip/prefabs/ruinsrelic.lua

Lastly you search its texture. I find

    local assets =
    {
        Asset("ANIM", "anim/"..build..".zip"),
    }

The build of ruinsrelic_chipbowl is

    local build = "ruins_"..name

The name is "chipbowl", so all sums up to "ruins_chipbowl". Go to anim/, search this name and I find

Snipaste_2023-08-24_11-25-43.png.ec14de71dae3853c8f4e35e95fb2cef4.png

It has anim.bin in it! Very lucky that you will not need to find this one in another asset. Actually, you need at least 1 anim.bin to play a proper animation. However, when you are just creating texture, anim.bin is useless.

2.2Extract Assets

Download pyscripts/ and html/ from the github repository, and install python3.

Once you have them ready, use a terminal to convert the asset. (The different folder name in the images is because I have moved these files to z:/rr/ruinsrelic_chipbowl/, just ignore this.)

python pyscripts/cli.py ruins_chipbowl.zip -json

Snipaste_2023-08-24_11-28-14.png.7c41db565dfe79858acba9eb20d15f12.png

This command will give you anim.json and build.json in a sub directory. Open the animation player (html/index.html) and load them. THe screenshot you see below is my modified version, I added a spice on the dish.

Snipaste_2023-08-24_11-31-56.thumb.png.b70957b1f0e3cc9ae302efe202e62259.png

You are free to draw your own texture now. But don't forget to change the attributes in build.json using a text editor.

build.json is something like

{
    "type": "Build", "version": 6, "name": "ruinsrelic_chipbowl", "scale": 1,
    "Atlas": ["atlas-0.tex"],
    "Symbol": {
        "chipbowl": [
            {"framenum": 0, "duration": 1, "x": -1.9999799728393555, "y": -17.949987411499023, "w": 205, "h": 121, "alphaidx": 96, "alphacount": 42},
            {"framenum": 1, "duration": 1, "x": 5.075075149536133, "y": -0.7749959826469421, "w": 215, "h": 87, "alphaidx": 138, "alphacount": 30}
        ],
        "spice": [
            {"framenum": 0, "duration": 1, "x": 6.899911880493164, "y": -29.324960708618164, "w": 232, "h": 140, "alphaidx": 6, "alphacount": 84}
        ],
        "chip": [
            {"framenum": 0, "duration": 1, "x": -1.9999799728393555, "y": -17.949987411499023, "w": 205, "h": 121, "alphaidx": 90, "alphacount": 6}
        ],
        "bowl": [
            {"framenum": 0, "duration": 1, "x": -1.9999799728393555, "y": -17.949987411499023, "w": 205, "h": 121, "alphaidx": 0, "alphacount": 6}
        ]
    },

You can see, there are some attributes of an image ,like x,y,w,h. w and h stands for width and height in pixel. x and y stands for the pivot of an image, as you see in the animation player there are also -x and -y as its real position. The relation between them are

-x = x - w / 2 ;
-y = y - h / 2 ;

Snipaste_2023-08-24_11-38-53.thumb.png.1e9a89c1f81035f3269d62a361472599.png

If you move the image to another place, change x,y in build.json as well. If you resize an image, change w,h.

calc.thumb.jpg.ba3d6f65b629a344bffeb93d15d57f81.jpg

For example, I split the dish into two parts (chip and bowl) and draw a new texture, and change build.json, reload it, and change y.

Snipaste_2023-08-24_12-01-39.thumb.png.aeac219291a7ada571afbcc6bae112bc.png

2.3Compile Assets

Firstly I want you to know that, if you are only making a texture, there is nothing to do with animation, so anim.bin is useless. Otherwise you will need anim.bin.

Assuming you don't need anim.bin. You have build.json and some images. Compile build.json and image folder to a zip you can use in game.

python pyscripts/cli.py ruins_chipbowl/build.json ruins_chipbowl/

Snipaste_2023-08-24_12-14-27.thumb.png.cd9b563737937bd2b03e2a0784d628c8.png

3.Applying Variants

3.1Add Assets

In your modmain.lua if you have no chance to add assets in prefab, add the assets like

Assets={Asset("ANIM","anim/ruinsrelic_chipbowl.zip")}

If you are using dynamic anim files, write

Asset("DYNAMIC_ANIM","anim/dynamic/xxx.zip"),--the build
Asset("PKGREF","anim/dynamic/xxx.dyn")--the texture

Or you have a xml

Asset("DYNAMIC_ATLAS","images/xxx.xml")

3.2Add Inventory Icons

If you want to also make a different icon for an inventory item, you need to create some 64x64 sized icons, and add these assets.

Assume you have them in a folder xxx_inventoryimages. Run

python pyscripts/cli.py xxx_inventoryimages/

And you will get xxx_inventoryimages.xml and xxx_inventoryimages.tex

Add these assets like

Asset("IMAGE","images/xxx_inventoryimages.tex"),
Asset("ATLAS","images/xxx_inventoryimages.xml"),
Asset("ATLAS_BUILD","images/xxx_inventoryimages.xml",256)--this number is mysterious, or maybe not required

3.3Register Inventory Icons

Assume you have xxx_inventoryimages.xml, use the following code to register them.

local atlas = "images/xxx_inventoryimages.xml"
local function ProcessAtlas(atlas, ...)
  local path = resolvefilepath_soft(atlas)
  if not path then
    print("[API]: The atlas \"" .. atlas .. "\" cannot be found.")
    return
  end
  local success, file = pcall(io.open, path)
  if not success or not file then
    print("[API]: The atlas \"" .. atlas .. "\" cannot be opened.")
    return
  end
  local xml = file:read("*all")
  file:close()
  local images = xml:gmatch("<Element name=\"(.-)\"")
  for tex in images do
    RegisterInventoryItemAtlas(path, tex)
    RegisterInventoryItemAtlas(path, hash(tex))
  end
end
ProcessAtlas(atlas)

3.3Use Texture

The most easy way to retexture an item is the SetBuild and AddOverrideBuild function.

inst.AnimState:SetBuild("xxx")

If you want only some of your texture applied, like you only want to override the torso symbol of a pigman.

inst.AnimState:OverrideSymbol("torso","xxx","torso")--symbol,build,override symbol
Edited by Rickzzs
  • Like 1
Link to comment
Share on other sites

4.Drawing Skins

The process for skins is the same as variants, so here I am going to talk about the restriction of skins.

When you create variants, you can freely modify the animation and use AnimState:SetBank(bank) to play your animation. But when you create skins, you are not encouraged to change the item's original animations, a skin is just a skin.

5.Registering Skins

5.1The process of registering skins contains full process of variants.

5.2Additionally, use CreatePrefabSkin to register it into the skin system.

5.3Finally, register it to the API.

Glassic API provides GlassicAPI.SkinHandler to register skins.

GlassicAPI.SkinHandler =
{
    GetModSkins                 = get_mod_skins,
    AddModSkin                  = add_mod_skin,
    RemoveModSkin               = remove_mod_skin,
    IsModSkin                   = validate_mod_skin, -- Backward compatible
    IsValidModSkin              = validate_mod_skin, -- Backward compatible
    ValidateModSkin             = validate_mod_skin,

    GetPlayerFromID             = get_player_from_id,
    SetCharacterExclusiveSkin   = set_character_exlusive_skin,
    DoesCharacterHaveSkin       = does_character_have_skin,
    DoesCharacterHasSkin        = does_character_have_skin, -- Backward compatible

    AddModSkins                 = add_mod_skins, -- Import skin data
    SetRarity                   = set_rarity,
}
GlassicAPI.SkinHandler.AddModSkin("item_skinname")
GlassicAPI.SkinHandler.AddModSkins({item1={"item1_skin1","item1_skin2"},item2={}})

Additionally GlassicAPI has the following features:

1.Add Custom Rarity by SetRarity

SetRarity(rarity, order, color, override_symbol, override_build)

The official rarities are

Spoiler
SKIN_RARITY = {
    Common = { 0.718, 0.824, 0.851, 1 },          -- B7D2D9 - a common item (eg t-shirt, plain gloves)
    Classy = { 0.255, 0.314, 0.471, 1 },          -- 415078 - an uncommon item (eg dress shoes, checkered trousers)
    Spiffy = { 0.408, 0.271, 0.486, 1 },          -- 68457C - a rare item (eg Trenchcoat)
    Distinguished = { 0.729, 0.455, 0.647, 1 },   -- BA74A5 - a very rare item (eg Tuxedo)
    Elegant = { 0.741, 0.275, 0.275, 1 },         -- BD4646 - an extremely rare item (eg rabbit pack, GoH base skins)
    HeirloomElegant = { 0.933, 0.365, 0.251, 1 }, -- EE5D40
    Character = { 0.718, 0.824, 0.851, 1 },       -- B7D2D9 - a character
    Timeless = { 0.424, 0.757, 0.482, 1 },        -- 6CC17B - not used
    Loyal = { 0.635, 0.769, 0.435, 1 },           -- A2C46F - a one-time giveaway (eg mini monument)
    ProofOfPurchase = { 0.000, 0.478, 0.302, 1 }, -- 007A4D
    Reward = { 0.910, 0.592, 0.118, 1 },          -- E8971E - a set bonus reward
    Event = { 0.957, 0.769, 0.188, 1 },           -- F4C430 - an event item
    Lustrous = { 1.000, 1.000, 0.298, 1 }         -- FFFF4C - rarity modifier
    -- #40E0D0 reserved skin colour
}

-- Share colours
SKIN_RARITY.Complimentary = SKIN_RARITY.Common
SKIN_RARITY.HeirloomClassy = SKIN_RARITY.HeirloomElegant
SKIN_RARITY.HeirloomSpiffy = SKIN_RARITY.HeirloomElegant
SKIN_RARITY.HeirloomDistinguished = SKIN_RARITY.HeirloomElegant

2.Set Character Exclusive by SetCharacterExclusiveSkin

SetCharacterExclusiveSkin(skin,character_name|{characters})

5.3[DS]Register it to Skin System.

The Skin System mod in DS provides exactly the same API as what DST does, but with fewer functions.

The lack of functions are listed below:

1.Do not use init_fn. All init_fn and clear_fn are moved to a global table SKIN_FNS.

If your previous code is

init_fn = function(inst) xxx_init_fn(inst, "xxx_skin1") end,

You need to change it to

local old_xxx_init_fn=SKIN_FNS.xxx_init_fn
function SKIN_FNS.xxx_init_fn(inst,buildname)
  if old_xxx_init_fn then old_xxx_init_fn(inst,buildname) end
  if buildname=="xxx_skin1" then
    --your own code
  end
end

Note that, if you have nothing special, you can just let the skin system do the basic things.

Spoiler
SKINFNS = {
  basic_init_fn = function(inst, build_name, def_build)
    inst.AnimState:SetSkin(build_name, def_build)
    SKINFNS.basic_inv_init_fn(inst)
  end,
  basic_clear_fn = function(inst, def_build)
    if def_build then inst.AnimState:SetBuild(def_build) end
    SKINFNS.basic_inv_clear_fn(inst)
  end,
  basic_inv_init_fn = function(inst)
    if inst.components.inventoryitem ~= nil then
      inst.components.inventoryitem:ChangeImageName(inst:GetSkinName())
    end
  end,
  basic_inv_clear_fn = function(inst)
    if inst.components.inventoryitem ~= nil then inst.components.inventoryitem:ChangeImageName() end
  end
}

 

 

Edited by Rickzzs
Link to comment
Share on other sites

6.Applying Skins

6.1TheSim:ReskinEntity

TheSim:ReskinEntity( inst.GUID, old_skinname, skinname, other.skin_id, player.userid )
--reskin inst having old skin "old_skinname" to skinname
--the ownership is validated using another skin_id or a userid
--the other.skin_id is used in special cases like Abigail, boat plank and lureplant. So you don't need to bother.
--if there is no old skinname, just put a nil
--if there is no other.skin_id, just put a nil
--if neither skin_id nor userid, this will have no effect

6.2inst.AnimState:SetSkin

inst.AnimState:SetSkin(skinname,default_build)
--default_build is used in case this skin is not owned

6.3inst.AnimState:OverrideSkinSymbol=OverrideSymbol

6.4inst.AnimState:OverrideItemSkinSymbol

inst.AnimState:OverrideItemSkinSymbol(symbol,build,overridesymbol,other.GUID,default_build)
--other.GUID is the entity that this build is linked to. For example, to reskin your swap_hat, you need to provide the hat.
--default_build is used when this skin is not owned

Your work will be writing some code to call these functions. Like what scripts/prefabs/hats.lua does

local function _base_onequip(inst, owner, symbol_override, swap_hat_override)
  local skin_build = inst:GetSkinBuild()
  if skin_build ~= nil then
    owner:PushEvent("equipskinneditem", inst:GetSkinName())
    owner.AnimState:OverrideItemSkinSymbol(swap_hat_override or "swap_hat", skin_build, symbol_override or "swap_hat", inst.GUID, fname)
  else
    owner.AnimState:OverrideSymbol(swap_hat_override or "swap_hat", fname, symbol_override or "swap_hat")
  end

  if inst.components.fueled ~= nil then
    inst.components.fueled:StartConsuming()
  end

  if inst.skin_equip_sound and owner.SoundEmitter then
    owner.SoundEmitter:PlaySound(inst.skin_equip_sound)
  end
end

 

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...