Type Problem for my OOP structure

Hello guys.
I need help with my OOP structure in roblox.

The problem I’m having is that my CharacterEntity Module which inherits of of my Entity Module cannot access MT methods of itself (does not autocomplete) still haven’t debugged to see if they cannot be called at all. So sth like :SetNextAction is not possible but methods out of the MT of Entity are accessible.

f.e in the Entity Module any MT method can be called on self from other MT methods but this is not the case for CharacterEntity. Whenever I try to index any MT methods from the module the only ones that get autocompleted are those of the Entity MT

The problem seems to stem from setting the metatable of MT to {__index = Entity.MT}
but removing only that does not fix it I also need to remove my annotation of self as {} & Entity.Entity. When I remove both only then can I call :SetNextAction

CharacterEntity Module:

--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local HttpService = game:GetService("HttpService")
local ServerScriptService = game:GetService("ServerScriptService")

local ChooseEncounterAction = ReplicatedStorage.Remotes.Events.ChooseEncounterAction

local GlobalTypes = require(ReplicatedStorage.SharedServices.Utilities.GlobalTypes)
local FunctionUtils = require(ReplicatedStorage.SharedServices.Utilities.FunctionUtils)
local t = FunctionUtils.t
local GenerateRandoms = FunctionUtils.GenerateRandoms

local Entity = require(script.Parent._Entity)
local PlayerStatsService = require(ServerScriptService.Server.Services.PlayerStatsService)
local InventoryService = require(ServerScriptService.Server.Services.InventoryService)
local CharacterService = require(ServerScriptService.Server.Services.CharacterService)
local ActionService = require(ServerScriptService.Server.Services.ActionService)
local Items = require(ReplicatedStorage.Shared.Tables.Items)

local isPlayer = t.instanceIsA("Player")

type self = {
	_nextAction: GlobalTypes.EncounterAction,
	_characterData: GlobalTypes.Character?,
	_equipment: {InventoryService.PlayerItem}?
} & Entity.Entity

local CharacterEntity = {}
local MT = {}
MT.__index = MT
setmetatable(MT, { __index = Entity })
export type CharacterEntity = Entity.Entity & typeof(setmetatable({} :: self, MT))

CharacterEntity.MT = MT
local CharacterEntityCache = {} :: {CharacterEntity}

local function onClientActionReceived(player: Player, entityId: string, action: any?)
	local entity = CharacterEntity.new(player)
	if not entity then 
		return
	end
	
	if entity._owner ~= player then
		return
	end
	
	if action and entity._actionRequestConn then
		entity._actionRequestConn:Disconnect()
	end
end

function CharacterEntity.new(player: Player): CharacterEntity
	local entity = Entity.new()
	local self = setmetatable(entity :: self, MT) :: CharacterEntity
	self._type = "CharacterEntity"
	self._owner = player 
	self._stats = PlayerStatsService.GetPlayerStats(self._owner :: Player)
	self._characterData = nil
	self._equipment = {}
	table.insert(CharacterEntityCache, self)
	return self
end

function CharacterEntity.GetEntityByUniqueId(uniqueId: string): CharacterEntity
	local character = Entity.GetEntityByUniqueId(uniqueId)
	return character :: CharacterEntity
end

function MT.TakeTurn(self: CharacterEntity): GlobalTypes.EncounterAction
	if isPlayer(self._owner) then
		warn("Make owner take turn:", self._owner)
	end
	return self._nextAction
end

function MT.SetNextAction(self: CharacterEntity, action: GlobalTypes.EncounterAction)
	self._nextAction = action
end

function MT._AskOwnerForAction(self: CharacterEntity)
	self._actionRequestConn = self._trove:Connect(ChooseEncounterAction.OnServerEvent, onClientActionReceived)
	ChooseEncounterAction:FireClient(self._owner, self._actions)
end

function MT._Init(self: CharacterEntity)
	if isPlayer(self._owner) then
		local player = self._owner :: Player
		self._characterData = CharacterService.GetCharacterData(player)
		self._equipment = InventoryService.GetPlayerEquipment(player)
		self._actions = ActionService.GetAllPlayerActions(player)
	end
end

return CharacterEntity

Entity Module:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local HttpService = game:GetService("HttpService")

local FunctionUtils = require(ReplicatedStorage.SharedServices.Utilities.FunctionUtils)
local ModuleUtils = require(ReplicatedStorage.SharedServices.Utilities.ModuleUtils)
local GlobalTypes = require(ReplicatedStorage.SharedServices.Utilities.GlobalTypes)

local Trove = ModuleUtils.Trove
local t = FunctionUtils.t
local GenerateRandoms = FunctionUtils.GenerateRandoms

local isPlayer = t.instanceIsA("Player")

type alignment = GlobalTypes.EncounterEntityAlignment

type self = {
	_uniqueId: string,
	_type: string,
	_tookTurn: boolean,
	_stats: GlobalTypes.StatTable,
	_alignment: alignment,
	_index: number,
	_owner: Player?,
	_actions: {},
	_actionRequestConn: RBXScriptConnection,
	_trove: ModuleUtils.TroveType
}

local Entity = {}
local MT = {}
MT.__index = MT
export type Entity = typeof(setmetatable({} :: self, MT))

local entityCache = {} :: {Entity}

Entity.MT = MT

function Entity.new(): Entity
	local self = setmetatable({} :: self, MT)

	self._trove = Trove.new()
	self._uniqueId = GenerateRandoms.GenerateTimestampedGUID()
	self._type = "Entity"
	self._alignment = "Neutral"
	self._index = 0
	self._owner = nil
	self._stats = {
		Health = 10,
		Armor = 10,
		Speed = math.random(1, 10),
		Strength = 10,
		CritPower = 10,
		CritChance = 10,
		Dexterity = 10,
		Intelligence = 10,
		MovementSpeed = 10,
		JumpPower = 10
	}
	self._tookTurn = false
	self._actions = {}

	table.insert(entityCache, self)
	return self
end

function Entity.GetEntityByUniqueId(uniqueId: string): Entity?
	for _, entity in entityCache do
		if entity._uniqueId == uniqueId then
			return entity
		end
	end
	return nil
end

function MT.TakeTurn(self: Entity): boolean
	warn("FINISHED TURN FOR SELF")
	return true
end

function MT.SetAlignment(self: Entity, alignment: GlobalTypes.EncounterEntityAlignment)
	self._alignment = alignment 
end

function MT.GetPossibleActions(self: Entity)
	return self._actions
end

function MT.GetTimeoutAction(self: Entity)
end

function MT.Destroy(self: Entity)
	self._trove:Destroy()
	table.remove(entityCache, table.find(entityCache, self))
end

return Entity

Hello, you tried to make a class? I don’t understand why would you need to access MT methods without using constructor .new()

I dont’t think you understood my problem. It’s not that I cant acess the MT methods it’s that the engine does not know about the methods of MT in CharacterEntity like it does for Entity.
If I try to call an MT function on self in the Entity Module. the engine autocompletes the function but in the characterEntity Module it does nto autocomplete the MT functions, it only autocompletes the functions from Entity.MT which is not enough. So f.e it doesnt autocomplete :SetNextTurn() if I try to call that inside the .new(), but if I try to call :setAlignmeny() whiich is a method of Entity.MT then it autocompletes.
And in the Entity Module the MT functions do get autocompleted. so if I try to call SetAlignment in .new() it gets autocompleted.
The fix I found is getting rid of setmetatable(MT, { __index = Entity }) and getting rid of & Entity.Entity from type self. But that then doesnt autcomplete any properties of the base Entity class or methods anymore.

What if you write in CharacterEntity:

setmetatable(MT, { __index = Entity.MT })

type self = {

–CharacterEntity self stuff

}

type MTtype = {

–methods from CharacterEntity MT

}

export type Shared = Entity.Entity & self & MTtype

1 Like

Why are you using MT and Entity? You have 2 different class in the same module. Try merging both tables and repurposing 1 table for both.

Looks like you have a very strange way of setting up your superclass type. I can pitch in with how I do my own types:

Though, I handle inheritance a little differently.
Say that you have a superclass, and you would like subclasses to have typing for both the superclass and its own methods/etc. What I devised was using generics, and unioning it like so:

export type Superclass<T> =
{
    DoSomething: (self: Superclass<T>) -> ();
} & T -- This union allows subclass methods to be typechecked, see code block below
-- Also, if you're using my way of typing:
type Static =
{
    -- Yes, you specify generics in table functions like this. Yes, it looks really stupid
    new: <T>() -> Superclass<T>;
}

…so, if you inherit your superclass:

local Superclass = require("./")
export type Subclass = Superclass.Superclass<{
    DoSomethingInSubclass: (self: Subclass) -> ();
}>

Of course, you can’t always have nice things, so you’ll have to do a little gross typecasting for your superclass’ constructor so it’s easier to inherit, e.g:

function Superclass.new<T>(): Superclass<T>
    local self: Superclass<T> = {} :: never
    setmetatable(self :: any, Superclass)
    return self
end

Then, your subclass can do this with basically no issues:

function Subclass.new(): Subclass
    -- Since Subclass is just Superclass<{...}>, Roblox won't complain in strict mode
    local self: Subclass = Superclass.new()
    setmetatable(self, Subclass)
    return self
end

This setup will then allow your subclasses to have autocomplete for both its superclass methods/members and subclass methods/members, with little-to-no changes in the code itself outside of making the typechecker happy. If this is what you’re asking for, I hope it helped.

Edit: Note that anything using the superclass’ type for variables would have to be changed to Superclass<any>, e.g. a function with param entity: Entity would become entity: Entity<any>.

Well I took this structure from a youtuber called CrusherFire. His Modules follow this OOP sturcture and it basically separates the Static methods from the Class methods and I dont have to manually define methods.
I could very well manually define the methods like its been pitched by people here but I was looking for a solution that works like how my Entity module works where I don’t have to manually define the type methods

This does work, I’ve tried it out and it’s I guess an acceptable fix. I just need to define the methods manually which I was trying to avoid, because with my current structure if there’s not inheritance like in Entity then the methods get autocompleted even without having to manually type them.

Seems like the engine just doesn’t know how to show MT methods inside of CharEntity and Entity at the same time because of the inheritance chain. Im guessing this is just an engine limitation that I have to cope with.

Looks interesting.
I’ll keep this in mind if I run into other problems with my own structure.
It’s nice to have sth to default to if I fail at what I’m trying.
For now it seems I can just define an MTType in which I define the methods manually and merge the types like @Den_vers mentioned which seems to be the easiest solution. Although I was hesitant about manually defining methods as it’s time consuming and annoying.

I’m still going to leave this thread a little open to see if there’s anyone that may be able to fix it to which I don’t have to manually define methods but for now I guess I’ll just continue like this.

1 Like

I believe there isn’t a clearcut way unless you were to do more ugly type Whatever = typeof(setmetatable(...)) types/relying a lot on “typeof” on your tables. I also think that manually defining your methods helps nevertheless; you don’t have to scroll down and see method implementations to know what they do if you bother with commenting/documenting your methods and members in the type itself, e.g.

export type Class =
{
    --[=[
        This method does something awesome!
    ]=]
    DoSomethingAwesome: (self: Class) -> ();
}

So then, you don’t have to completely rely on your memory for every method, if your class gets exceedingly long.

Hmm I just tried reprompting chatGpt after the past 2 days and it actually game me a solution this time not like the last times.
I did:

local CharacterEntity = {}
local MT = {}
MT.__index = MT
setmetatable(MT, { __index = Entity })

export type CharacterEntity = Entity.Entity & self & typeof(MT)

And for now it seems to autocomplete both MT methods. I see that

typeof(setmetatable({} :: self, MT))

seems to also do the same thing if written as

self & typeof(MT)

Ill keep updated if I run into problems with this but for now it seems to be doing what I wanted it to do.

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