A library providing functions you can use to wrap userdata objects in Unitale and Create Your Frisk.
~ by WD200019 ~
This library is fully documented! Either open the lua file itself, or read on for a formatted version of the library's tutorial.
Any questions, comments, or concerns? Contact me on reddit or Discord (WD200019#8327
).
Say you're using CYF to make a moddable Lua environment, like Create Your Kris.
How would you force code like Player.Hurt(3)
to run some of your own code that you need to run? Maybe you need to instantly update an hp bar sprite, for instance.
It would be cool to replace Player.Hurt with your own function - but that's impossible, isn't it?
Let's talk about how to accomplish the above "by normal means" as well as with this library.
Regularly, you might forcefully update your hp bar sprite according to the player's hp every frame (which is already very wasteful, intensive and wildly inaccurate).
But even then, say you want something to happen if the user runs Player.Hurt
and it would now kill the player - like a custom game over screen? Tough luck - before you can run any of your own code, the engine will take over from here and instantly show the Undertale game over sequence.
The next step, then, is a metatable. A lua table that, when certain properties are get or set, will run custom code defined by you. But you don't want to manually copy and paste a "default case" function for every single property of the Player
object, do you? Player.x
, Player.y
, Player.sprite
, Player.hp
, Player.maxhp
, Player.maxhpshift
, Player.atk
, Player.weapon
, Player.lv
, Player.ishurting
, Player.lastenemychosen
...need I say more?
And that is where this library comes in. You can replace and create any and all properties and functions in any userdata object! ("userdata" means an object provided by the engine, such as the Time object, the Player object, the Bullet object, the Inventory object...)
This takes all of the work out of the process. You don't need to know metatables, or make a list of all properties of an object and their types, or even make a custom Player object at all!
The above example is now as simple as
wrapper = require "Libraries/userdataWrapper"
wrapper.WrapPlayer({
Hurt = {
set = function(_pla, pla, hurtAmount, invulnFrames)
-- get the hp value of the real Player object
if _pla.hp - hurtAmount <= 0 then
-- maybe you want a custom game over screen? put it here!
Encounter.Call("StartGameOver")
EndWave()
else
-- set hp bar sprites and whatnot!
Encounter.Call("UpdateFakeUI")
-- call the ACTUAL Player.Hurt function provided by the engine
_pla.Hurt(hurtAmount, invulnFrames)
end
end
}
})
After this code is run, ALL future occurences of Player.Hurt
(in this script) will instead call the function you defined above!
Now imagine putting this in a new .lua file and loading it as a library at the beginning of every wave - that's only one line in each wave file, and now magically every wave file in your mod has this customized version of the Player!
This library has many uses beyond this, too.
Read on for tutorials and documentation on the rest of the library!
So, here is a tutorial on "override tables". These are special tables that provide a list of properties (functions and variables) that you want to write custom code for.
They basically let you "override" (hence the name) userdata properties on a wrapped userdata object. In addition, any properties you add that are not properties of the original userdata object will be added as custom properties.
You can override all of: normal variables (like sprite.spritename
), read-only variables (like Player.maxhp
), and even functions (like Player.Hurt
).
This uses a system called "getters and setters".
So, here's an example: let's say I want to wrap a bullet, to make its Move function move it backwards instead of forwards.
If I want it to apply to a SINGLE bullet, then I just need to specify it in wrapper.WrapProjectile
:
bullet = CreateProjectile("bullet", 0, 0)
bullet = wrapper.WrapProjectile(bullet, { !! HERE !! })
Because we want to override a FUNCTION, we need to specify ONLY a "set" function:
bullet = CreateProjectile("bullet", 0, 0)
bullet = wrapper.WrapProjectile(bullet, {
Move = {
set = function(_prj, prj, x, y)
_prj.Move(-x, -y)
end
}
})
If the above example code were to be used in-game, then this specific bullet would always move backwards every time bullet.Move was called.
Here is a layout of how your functions are actually called:
set(_REAL_OBJECT, FAKE_OBJECT, ...)
get(_REAL_OBJECT, FAKE_OBJECT)
You get access to both the real object (like the actual userdata projectile, for instance), and the fake object (that is, the metatable that "replaces" the actual userdata object).
Let's make use of these two arguments together! How about making sprite.Set
automatically start an animation when you enter a certain phrase?
sprite = CreateSprite("bullet")
sprite = wrapper.WrapSprite(sprite, {
Set = {
set = function(_spr, spr, spritename)
if spritename == "attacking" then
spr.SetAnimation("attacking")
else
_spr.Set(spritename)
end
end
},
SetAnimation = {
set = function(_spr, spr, ...)
local args = {...}
if #args == 1 and args[i] == "attacking" then
_spr.SetAnimation({"1", "2", "3", "4", "5", "6"}, 0.4, "Character/Attacking")
_spr.loopMode = "ONESHOT"
else
_spr.SetAnimation(...)
end
end
}
})
Yeah, pretty useful, isn't it? Now, by just entering
sprite.Set("attacking")
the code automatically calls
sprite.SetAnimation({"1", "2", "3", "4", "5", "6"}, 0.4, "Character/Attacking")`
AND
sprite.loopMode = "ONESHOT"
!
(As a side note, the ...
you see above is a VarArg. You don't need to know how to do this to use this library.)
Next example: Overriding a variable that can be both get and set.
wrapper.WrapPlayer({
hp = {
set = function(_pla, pla, newValue)
if invincible then
DEBUG("You are powerless to try to change my hp.")
elseif newValue > 0 then
_pla.hp = newValue
else
DEBUG("Your silly scripts can't kill me!!")
invincible = true -- a global variable that can be used from the script itself
_pla.lv = 20
_pla.hp = 99
end
end,
get = function(_pla, pla)
if invincible then
return math.huge -- +infinity in Lua
else
return _pla.hp
end
end
}
})
In the above example, if any scripts try to set the player's hp to any value less than 1, a message will be displayed and the player's hp will get instantly set to 99. After this, the player's hp can no longer be set, and trying to check their hp value returns +infinity.
So, yes, as you can see, you can use this on all userdata values, and you can use both getters and setters. Now, on to the next step! Applying your changes to every userdata of a type!
So, I have a custom replacement for bullet.Move
that I want to apply to EVERY bullet. How do I do it?
bullet = wrapper.WrapProjectile(bullet, {
Move = {
set = function(_prj, prj, x, y)
_prj.Move(-x, -y)
end
}
})
Well, one way would be to wrap CreateProjectile...
local _cp = CreateProjectile
function CreateProjectile(...)
local realBullet = _cp(...)
return wrapper.WrapProjectile(bullet, {
Move = {
set = function(_prj, prj, x, y)
_prj.Move(-x, -y)
end
}
})
end
But this is tedious, it takes up extra room, and there's got to be a better way, right?
Well, this library has the ability to wrap the default userdata-creating functions for you!
Here's an easier way to do what I did above:
wrapper.projectileValues = {
Move = {
set = function(_prj, prj, x, y)
_prj.Move(-x, -y)
end
}
}
wrapper.WrapCreateProjectile()
wrapper.WrapCreateProjectileAbs()
There! Not only was it cleaner and easier to do, but it also applied the changes to CreateProjectileAbs
!
So: This is something you can do for ALL "multi-instance" userdata objects. The prime examples of what I'm talking about are projectiles and sprites. Basically, anything that you can create a potentially infinite amount of.
Now, for the full list of variables and functions for this library:
-
wrapper.autoWrapSprite
:= boolean =
true
- Set this to
true
to automatically wrapprojectile.sprite
for wrapped projectiles, andPlayer.sprite
for the wrapped Player. - If this is
true
, then the values inwrapper.spriteValues
will be applied to the sprite components of wrapped projectiles and the player.
- Set this to
-
wrapper.autoUnwrapUserdata
:= boolean =
true
-
Set this to
true
, and the default functions that take userdata values as arguments will be changed to automatically unwrap any table values you enter into them. -
As an example:
sprite.SetParent
takes a sprite object as its only argument. WithautoUnwrapUserdata
as true, all you have to do is pass a wrapped userdata OR a regular userdata. With this variable set tofalse
, you would always have to pass a regular userdata value.
-
-
wrapper.autoWrapFile
:= boolean =
true
- Set this to
true
, and whenever you useMisc.OpenFile
from the wrapped Misc object, it will, by default, return a wrapped File object. I say "by default" because if you provide your own customOpenFile
function, it will be used instead (nothing unique to this variable). - If this is
true
, then the values inwrapper.fileValues
will be applied to any File objects created from the wrapped Misc object.
- Set this to
-
wrapper.disguise
= boolean =
false
-
Set this to
true
to effectively "disguise" wrapped objects as real userdata values. What this means is: Error messages will be printed for trying to get non-existant properties, trying to convert the userdata to a string, using it in a for loop, and so on. -
This is actually useful for functions such as CYK's
table.copy
function, because if the Player were wrapped, it would duplicate the metatable and cause problems. Withdisguise
astrue
, such a function would be forced to believe that the wrapped userdata is a REAL userdata value.
-
-
wrapper.spriteValues
, -
wrapper.projectileValues
, -
wrapper.scriptValues
, -
wrapper.textValues
, -
wrapper.fileValues
,= override table (see section Override Tables) =
{}
- Set this to an override table, and the values you set here will be applied to ALL wrapped sprites/projectiles/etc by default. For a full guide on using these, see section Override Tables.
-
wrappedObject.userdata
= wrapped userdata object
- By simply checking
wrappedObject.userdata
, you will be given the original userdata that was wrapped by the library. This property cannot be overwritten.
- By simply checking
-
wrapper.WrapSprite(sprite, overrideTable = nil)
, -
wrapper.WrapProjectile(projectile, overrideTable = nil)
, -
wrapper.WrapScript(script, overrideTable = nil)
, -
wrapper.WrapText(text, overrideTable = nil)
, -
wrapper.WrapFile(file, overrideTable = nil)
,= takes 1 "multi-instance" userdata from Unitale/CYF, and one OPTIONAL override table (see section Override Tables)
-
Returns a single table with metatables that "wraps" a given Unitale/CYF userdata object.
-
If you provide an override table as
overrideTable
, the custom values you set in it will be applied to the returned object. -
If you leave the second argument blank, it will use the values in
wrapper.spriteValues
,wrapper.projectileValues
, etc.
-
-
wrapper.WrapPlayer(overrideTable = nil)
, -
wrapper.WrapAudio(overrideTable = nil)
, -
wrapper.WrapNewAudio(overrideTable = nil)
, -
wrapper.WrapInput(overrideTable = nil)
, -
wrapper.WrapTime(overrideTable = nil)
, -
wrapper.WrapMisc(overrideTable = nil)
, -
wrapper.WrapArena(overrideTable = nil)
, -
wrapper.WrapInventory(overrideTable = nil)
= takes 1 "single-instance" userdata from Unitale/CYF, and one OPTIONAL override table (see section OVERRIDE TABLES)
-
Immediately replaces a "single-instance" userdata from Unitale/CYF with a wrapped one.
-
The custom values you set in
overrideTable
will be applied to the returned table.etc.
-
-
wrapper.WrapCreateSprite()
, -
wrapper.WrapCreateProjectile()
, -
wrapper.WrapCreateProjectileAbs()
, -
wrapper.WrapCreateText()
= no arguments
- Replaces
CreateSprite
/CreateProjectile
/CreateProjectileAbs
/etc. with a function that automatically wraps created userdata objects withwrapper.spriteValues
,wrapper.projectileValues
, etc.
- Replaces
There are a few IMPORTANT points to make before I can say you know everything you need to know.
-
In CYF, you can use
bullet["variable"]
andsprite["variable"]
as shortcuts toGetVar
andSetVar
. Unfortunately, there is no way to differentiate betweenbullet["variable"]
andbullet.variable
.Trying to use
bullet.SetVar("x", 10)
and thenbullet["x"]
will return the actual table propertybullet.x
.So:
Or, the better option: AVOID using bullet.SetVar("x", 10)
and other variables with the same names as regular properties, because it's bad practice, confusing, and will break this library, as mentioned above.
-
ALL created wrapped objects will have an unchangeable property:
wrappedObject.userdata
. All you have to do is access this, and it will return the original userdata that was wrapped by the object.This is REQUIRED for functions like
SetParent
, ifautoUnwrapUserdata
is false. Use it like this:sprite.SetParent(wrappedObject.userdata)
-
You can check if something is a wrapped table or userdata with
type(object)
(unlessdisguise
is true). -
Wrapping functions and objects only affects the script this library is loaded in.
-
Don't worry about issues with code that compares userdatas and such. Functions like OnHit are safe!
bullet = CreateProjectile("bullet", 0, 0)
bullet = wrapper.WrapProjectile(bullet)
(...)
function OnHit(p)
if p == bullet then -- this checks out!
(...)
end
end
And, finally:
-
If your property has "set", but not "get", this library will interpret it as a userdata Function, and try to call it.
-
If your property has "get", but not "set", this library will interpret it as a read-only userdata variable.
-
If your property has neither "get" nor "set", trying to access it will error every time. There's no point in doing this.
Any questions, comments, or concerns? Contact me on reddit or Discord (WD200019#8327
).