Question about storing tool data

I’m struggling to work out an effective way to handle creating, storing and loading in weapons from a data-point of view, that being, what type of information and or how should I actually be storing this data?

There are abilities, items and a main weapon which for the purposes of this are all ‘tool’ objects but stored slightly different in my datastore. Focusing on the main ( or equipped ) weapon here is an example of the data

		weaponData = {
			['Sword'] = {toolID = 'sword_1', toolType = 'weapon', amount = 1, state = {}}
		},

The goal is to have a tool which is effectively a copy of a premade tool, i.e a ‘wooden sword’ which all users could potentially start with, this weapon would have the EXACT same stats as a premade weapon but be infinitely expandable using the state table which would store ‘changed’ states ( such as damage etc ) to allow for this premade weapon to be made custom through in-game actions.

Currently I don’t even really have a use for the name of the table at the moment, toolID is a lookup for a specific model / ‘preset’ called “sword_1”, the ‘tooltype’ is to set it as a weapon which is a ‘hack’ or band aid I’ve implemented to allow other tools to also work by having attributes to determine what tool is what, obviously the ‘amount’ is the ‘amount’ of tools but even then Is it best to have an ‘amount’ or multiple entries in the datastore? what if I want two ‘Sword’ ? would they overlap? should I use a unique ID for each weapon?

Really my questions are as follows;

  1. Is there a better way to handle tools? What information / method do you use to store these?

  2. How do you handle the creation of tools / loading in tools? For context I currently clone a model from replicated storage and also have a module manipulate a little data, I think modules could potentially handle this better?

  3. What’s the best way to detect which specific tool has been used from the server without relying on the client ( to avoid them trying to spoof data etc )

  4. Currently A LOT of my code relies on using specific strings to transmit data around, is there a better method to this or is it acceptable to use strings to pass around data? i.e using ‘attack’ or ‘block’ into a ‘combat function’ to determine what action to do i.e the player fires a remote to the server.

Thank you for reading, if i have further questions il be sure to post them below. PLEASE feel free to ask for any information regarding this and I’ll be happy to provide. If it’s relevant I am using profileservice :slight_smile:

The optimal method of storing inventories varies with how your game functions. Specifying amount for an item that can receive modifications isn’t practical if these modifications occur frequently enough to the point where amount almost always stays at 1. Entries that never/rarely receive individual modifications (e.g., crafting materials) can uniquely specify amount to minimize overhead.

I don’t understand what you mean by this, but explicitly declaring toolType is probably redundant since (I’m assuming) any item of type sword_1 will always be a weapon. You can streamline “types” by implementing a class hierarchy. For example:

item
├──equippable
│   ├──sword
│   │   └──wooden sword
│   └──bow
│       └──wooden bow
└──ingredient
    ├──gold
    └──fabric

Using metatables, we can create an instance of the item’s respective class on runtime to automatically read default values and directly call intuitive methods (such as :drop() which discards an item) that subclasses can override as necessary. This is completely optional, but it’s my approach to this kind of thing since OOP experience nicely transfers over here. You can declare superclass item in a module like such:

local item = {
	weight = 0; -- an example of a default value. since we specify this in the main superclass, all items will default to 0 weight unless explicitly specified otherwise
};
item.__index = item;

function item:new(owner: Player?) -- instantiate an item. this constructor is meant to be inherited by instantiable (non-abstract) subclasses, so we should never call it directly
	local self = setmetatable({ -- our new item
		model = self.model:Clone(); -- the item superclass doesn't specify a model, but instantiable classes should (for example purposes, you can handle models entirely clientside instead)
	}, self); -- methods can change the definition of "self" to the first argument when called as a function (i.e., item.new(anotherClass, owner))
	-- this trick is useful for calling :new() from a superclass context when we've overridden it in our subclass

	self:setOwner(owner);

	return self;
end

function item:setOwner(plr: Player?) -- an example method that changes the owner. this is only one way to implement ownership and might be practical only if items are often exchanged
	if self.owner then
		-- TODO: unassociate this item from the old owner's (self.owner) inventory
		-- also notify them that they no longer own the item with remote events
	end

	self.owner = plr;

	if plr then
		-- TODO: associate this item with the new owner's inventory
		-- also notify them that they now have the item (update their gui accordingly)
	end
end

function item:newSubclass( -- allows us to make a new subclass of "item" without going through the hassle of handling metatables externally
	tbl : { model: PVInstance? } -- if model isn't specified, we're probably making an abstract (sword, bow) class
)
	tbl.super = self; -- convenient for referencing superclass constructors later
	tbl.__index = tbl; -- this subclass table can now handle inheritance

	return setmetatable(tbl, self); -- self can be a subclass here since this method can be called from that context.
	-- in this way, we can create subclasses for subclasses
end

With :newSubclass(), we can create an abstract class equippable:

local equippable = require(<item superclass modulescript>):newSubclass{}; -- intentionally pass an empty table for abstraction

-- you can override :new() to better suit creating equippable items here

function equippable:equip() -- an example method inherited by all equippable items
	-- validate the equip and then tell all other clients to render it accordingly
	-- we shouldn't need to ask the client who equipped to render the item because they already know they equipped
	-- if equip validation fails, then we can give the owner feedback to ensure that their client didn't incorrectly render an equip
end

function equippable:unequip()
end

From here, you probably get the idea. sword ideally inherits equippable and defines some generic slashing logic. You can also set toolType (or name the field itemType if all items have one) in its table so that we can index and interpret item.toolType from all swords; the same should be done for other types at your leisure. Anyways, wooden sword should inherit sword and define typical values for a wooden sword in its table, which methods then use. For special modifications, each wooden sword can have its properties set which causes indexing (e.g., reading sword.damage) to never ignore the metatable’s (superclass’s) definition for damage since the instance already defines it (hence “override”).

When it comes to server-client interaction, the server should validate any attempts to affect the gamestate like inflicting damage. A variety of checks exist and drastically vary according to the game, but ensuring all conditions are how they’re supposed to be (such as cooldowns) is really the only thing you need to do before applying the change and notifying other players of it. The client should also first perform its own checks (preferably even stricter than the server) and begin visual queues immediately to provide an illusion that their request processed instantly. Your code should revert/delete said visual queues if the server deems the request to be invalid. This is also the reason I try to handle item models locally.

As for using strings to convey actions through remote events, you can obviously do it. I prefer separating them into individual remote event objects since I like to “objectify” my environment for lack of a better term.

4 Likes

I appreciate the response a ton and will begin attempting to implement it + follow up with any additional questions I might have during this process.

Thank you very much.

( will mark as answer shortly )

Could you provide an example of the serverside script for the first module you posted? I’m not familiar with OOP so an example ‘real world’ usage would be much appreciated :+1:

Il be doing some reading up on OOP and this type of coding as well.

The first module acts abstractly, meaning it functions as a contract/framework for its respective subclasses. Do you want me to fully implement setOwner? That method already exemplifies the superclass’s purpose, and its behavior should vary according to your preferences and circumstances.

Once you create classes for individual items, you can call :new() for each item you pull from the datastore, “deserializing” your table of items and their modifications into functional instances of your classes:

local modules = item:GetDescendants(); -- get every subclass of item
local class = {}; -- class dictionary of itemType keys and class values
for _, module in modules do
	local this = require(module);
	class[this.itemType] = this;
end

--[[
	deserializing data
]]
local exampleData = {
	{ -- an example wooden sword as an item entry
		itemType = 'wooden_sword';
		damage = 15; -- should be an override if we have a default value for damage
	};
};

local inventory = {}; -- our deserialized result to use throughout the session

for slot, entry in exampleData do
	local item = class[entry.itemType]:new(player); -- pass the data owner since our example item class took the owner as an argument in its constructor

	entry.itemType = nil; -- disregard itemtype as it has already served its purpose
	-- you dont need to do this if you store overrides in their own table though

	for override, value in entry do -- since the entry itself (not a subtable) stores our overrides
		item[override] = value; -- transfer all necessary overrides
	end

	inventory[item] = true; -- cache the item to the player's inventory as a key for efficient lookups
	-- remember that the server shouldn't really care about item order (unless you decide to save it); this is usually a purely visual thing that the client guis handle autonomously
end

You can also compact your inventory of functional item instances back into a datastore-compatible format (reserialization):

local resultData = {};

for item, _ in inventory do -- inventory is predefined from deserialization
	local entry = { -- the respective serialized data entry for each item
		itemType = item.itemType; -- since itemType shouldn't be explicitly overridden, we need to manually add it (this index operation automatically uses the metatable's value)
	};

	for override, value in item do -- check all explicitly set values in the item object
		local t = typeof(value);
		entry[override] = (t == 'number'
						or t == 'string')
						and value
						or nil; -- serialize our override as long as its a number or string (to avoid serializing unstorable data)
	end

	resultData[#resultData + 1] = entry; -- add to our resultant data
end

Remember that this is practically pseudocode and should change according to how you implement your data storage and general player data. You can optimize and clarify this structure, but some sort of serialization will always be necessary to convert your primitive datastore entries into dynamic data structures.
Your classes can and will need to reference your overrides in their methods. For example, calling :slash() on any sword might need to reference self.damage, which could either refer to the default value in any of the unique sword classes (e.g., wooden swords might have damage default to 10) or to an override (i.e., a direct declaration in a specific wooden sword instance).

1 Like

If you’re into OOP, you could do that. This is how my framework would look like in explorer

| Tools(Folder)
       | SwordsClass(Classes are module scripts)
              | IronSwordClass
              | WoodenSwordClass
       | AxeClass
              | IronAxeClass
              | WoodenAxeClass

For the general classes like swords or axes, I would do something like this

local Swords = {}
Swords.__index = Swords

function Swords.new(damage, color, particles)
   local self = {}
   
   self.damage = damage
   self.color = color
   self.particles = particles

   return self
end

function Swords:Slash()
end

Then for the contents in the specifics, for example IronSword would look like this

local IronSword = {}
IronSword.__index = IronSword

function IronSword.new()
   local self = {}
   self.sword = SwordsClass.new(5, Grey, Sparkles)
   return self
end

Thank you for the follow-up. I’ll try to implement something simple and see if it works.

it’s difficult for me to learn without being able to put the scripts in-game and modify / manipulate them. Perhaps a random example that prints out something which shows off the usage of subclasses ( inheritance ), this would help me understand it a lot because right now I can understand what the lines mean but it’s hard to fully figure out exact what each thing does without being able to manipulate it further. in addition to this I’ll be doing some more reading on OOP.

I really appreciate you taking the time to answer my questions.

A simple approach to achieve this is by creating NumberValues for each of your tools and naming them accordingly. Initially, the NumberValue will be set to 0, indicating that the tool is locked, while a value of 1 signifies that the user has access to the tool. You can also write a script that grants the weapon to the player when the value is set to 1.

You’ll want to save these NumberValues using Datastores, but it’s quite simple if you check out a tutorial on YouTube, or perhaps you already know how to do it.

This wouldn’t solve my issue, but I appreciate the effort :+1:

The code should work out of the box so long as your subclasses (as seen in code snippet #2 which calls :newSubclass()) properly reference their respective superclasses with require(). I may have forgotten or incorrectly written something somewhere If your finished hierarchy doesn’t seem to function as intended, so I’ll be sure to try and replicate your error if needed.
Once you set up your class tree, modifying it is as easy as creating methods that suit your needs (like my :setOwner(player) method that handles everything related to item ownership), adding default fields to your classes, overriding whenever applicable, and creating new classes as needed.

Here’s a sword abstract class that inherits equippable:

--[[
	remember, equippable inherits the item class and subsequently
	the "newSubclass" method, so we can call it here
]]
local sword = require(--[[equippable module path]]):newSubclass{
	cooldown_rigidity = .85;
	swing_speed = 2;
	damage = 10;
};

--[[
	here's a cool method that ALL items that inherit sword can call.
	if a specific sword wants to be fancier, it can define a more complex version.
	this method will validate swinging on the backend.
	we can propagate its returned value to the client with the sword to tell it to
	undo visual effects in the case that the check failed.
]]
function sword:canSlash(): boolean
	--for example, let's check swinging cooldown
	local t = workspace:GetServerTimeNow();

	--[[
		remember, "self" refers to whatever sword we called a method on.
		here, i make it so that self.last_swing is the last timestamp the
		sword was swung.
		if self.last_swing is nil, that means it's the first time we're swinging.
		this conditional will instantly return false if the cooldown is still
		applicable; we calculate cooldown using the inverse of self.swing_speed,
		which has a default value of 2. this allows swords to perform :slash()
		twice per second (reciprocal of 2 = 1/2).

		*in actuality, we need to be more lenient in verifying cooldowns due to
		 latency between server and client, so we decrease the effective cool-
		 down by 15% (* .85).
	]]
	if
	   self.last_swing and
	   t - self.last_swing < self.cooldown_rigidity / self.swing_speed
	then return false; end -- return false, signifying that the server denied the slash.
					       -- this boolean can make the remote handler tell the client
					       -- to undo whatever visual effect they showed

	return true; -- tell the caller that the check succeeded
end

function sword:slash()
	local t = workspace:GetServerTimeNow();
	self.last_swing = t; -- update last_swing because we successfully slashed
						 -- at this point. the server shouldn't be calling :slash()
						 -- unless it wants to force one regardless of whether
						 -- :canSlash() succeeds. you can actually combine the
						 -- the two methods, but you won't be able to force
						 -- slashes or check whether it's doable without also
						 -- slashing.
	-- do other fancy things like hitbox calculations and damage
end

return sword; -- since our class embodies a modulescript

Unique items can either be very fancy (unique logic and overrides) or very simple (abide by whatever our abstract sword already defines). Here’s where we finally make a wooden sword:

local wooden_sword = require(--[[sword module path]]):newSubclass{
	model = serverStorage.models.wooden_sword; -- this class is instantiable, so we will meet
											   -- the requirements for our :new() constructor
}; -- let's not override anything to showcase inheritance

return wooden_sword;
--[[
	that's all we'll do for simplicity. you don't even need to
	declare wooden_sword here; you can just pass in :newSubclass()
	directly to the module's return statement.
]]

Let’s also make an orichalcum rapier to contrast inheritance and overrides:

local ori_rapier = require(--[[sword module path]]):newSubclass{
	model = serverStorage.models.orichalcum_rapier;
	damage = 75; -- ALOT more damage than the default sword
	swing_speed = 5; -- ALOT less cooldown
};

function ori_rapier:new(owner: Player?) -- this will override item.new, but we can still use
						 			    -- its code with a little trick akin to java, c#, etc.
	-- we would implement extra constructor logic here

	return self.super.new(self, owner);
	--[[
		this calls our superclass's implementation for :new().
		"sword" itself does not redefine :new(), but remember
		that it inherits "equippable" which also inherits "item".
		"item" does have a definition for new which will be used.
	]]
end

function ori_rapier:canBackstep(): boolean
	return true; -- we should be doing checks to see if we can perform a backstep
end

function ori_rapier:backstep() -- food for thought. our rapier should have a cool ability!
	-- backstep logic
end

return ori_rapier;

Let’s test both instantiable classes:

local wooden_sword = require(--[[ wooden sword module path ]]);
local orichalcum_rapier = require(--[[ orichalcum rapier module path ]]);

game.Players.PlayerAdded:Connect(function(plr)
	local sword = wooden_sword:new(plr); -- call our constructor and pass in "owner"

	print(sword.damage); -- the wooden_sword class itself doesn't specify damage so
						 -- we reference sword's "damage" field (which is 10).
	sword.damage *= 2;	 -- thanks to metatables, we can just override this
						 -- specific sword's damage with a single operation.
	print(sword.damage); -- should be 20

	print(sword:canSlash()); -- should be true
	sword:slash();
	task.wait(.2);
	print(sword:canSlash()); -- should be false
	task.wait(.3);
	print(sword:canSlash()); -- should be true

	local rapier = orichalcum_rapier:new(plr);

	print('dmg: ' .. rapier.damage .. ', dex: ' .. rapier.swing_speed); -- should be "dmg: 75, dex: 5"
	print(rapier:canSlash()); -- should be true
	rapier:slash();
	task.wait(.2);
	print(rapier:canSlash()); -- should be true again since our swing speed is enhanced
end)

I focused mostly on structure rather than actual functionality because server-client communication and hitbox logic are entirely different rabbit holes, but a streamlined OOP structure seeks to make it a little bit more intuitive down the line despite the difficulty.

Mastering OOP is commendable and has many uses outside Lua, especially in the workforce where C-based languages (Java, C#, etc. are usually object-oriented) dominate most enterprises. The approach I handed you will probably seem much simpler once you take courses that stress inheritance and data structures, as you can then equate several aspects to Lua’s metatables.
Then again, my opinion is advice at best, so don’t hesitate to cast it aside if it’s too inconvenient to adopt. I’ve learned as a hobbyist that obsessing over structure and optimization can be fun at first but eventually very draining and demotivational.
Think of your time dedicated to learning an object-oriented approach as an investment. Try to master it if you believe your time is well-spent, but save it for the future if you plan to learn it formally and for a better reason someday. Programming can be quite the subjective paradigm.

I’ll attempt to implement this, again, I appreciate you putting a lot of effort into helping me out here.

Just to clarify ( and I’m sure i’ll figure this out through testing ), this method would allow me to create ‘preset’ weapons but further modify those / load in custom data / inputs onto said weapons should the user have a change on that tool? i.e 16 damage instead of a default 12.

Yes, and this is what Lua metatables (and inheritance by association I guess) seek to accomplish. Whenever we assign a metatable to a table, our __index metamethod intercepts and handles all index operations for nonexistent keys (table[key] = nil).
Lua instead scans our table that we assigned the metatable’s __index, which is always itself in our case (it doesn’t have to be, but that’s usually much less intuitive). You might know this already, but this is a basic and popular application of __index:

local mtbl = {
	damage = 10;
};
mtbl.__index = mtbl; -- setting the metatable's __index metamethod to itself

local tbl = setmetatable({}, mtbl);
print(tbl.damage); -- 10

tbl.damage += 1;
tbl.damage = tbl.damage + 1; 
--[[
	functionally equivalent to the previous line. note that
	__index does not intercept "tbl.damage =". this is because
	the operation is setting, not getting. we can actually also
	intercept setting with __newindex, but this is unnecessary
	for our hierarchy.

	"tbl.damage" after the equal sign IS an attempt to index
	the main table, so lua will intercept the attempt and use
	__index to yield a result instead. lua will look for our
	desired key in the metatable since its __index is set to
	itself.

	now that we set tbl.damage to 11 on the first line and
	set it again to 12 on the second line, tbl looks like
	this:
	{
		damage = 12;
	}

	this is considered an override; __index will no longer
	intercept indexing attempts for key "damage".
]]

print(tbl.damage); -- 12
print(mtbl.damage); -- still 10; default value is not affected

__index is one of many metamethods, and they’re called metamethods for good reason. You can set __index to a function that receives the original table and key with which an index operation was attempted. Here, I basically just set __index to a function that does the exact same thing as setting it to the metatable itself:

local mtbl = {
	damage = 10;
};
function mtbl.__index(tbl, key)
	return getmetatable(tbl)[key];
end

local tbl = setmetatable({}, mtbl);
print(tbl.damage); -- will still print 10

In hindsight, it might have been presumptuous of me to spring this on you since I had more of a “here’s my way of going about it” impression rather than aiming for a simplistic and evaluative approach, especially since your experience with metatables should be decently rich before attempting to create and apply compound structures with them. Hopefully, this offers a new perspective on how Lua can store and manipulate data, perhaps letting you come up with similar ideas that better suit your taste and needs. This back-and-forth might serve as a decent archive (and personal reference) in the far future for similar endeavors, so I don’t think that your possible decision against this implementation would be a waste for either of us.

1 Like

Sorry about the late reply! I’m absolutely implementing this method :slight_smile: I appreciate your help and will use what you’ve given me as references to further improve my skills! Thank you.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.