Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/luaSpecific/defineMessage.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
local luaSpecific = script.Parent
local source = luaSpecific.Parent.source

local skillLib = require(source.skill)
local holdable = require(source.holdableSkill)
local skillLib = require(source.skill)
local statusLib = require(source.statusEffect)

local message = require(source.message)
local utility = require(source.utility)
Expand All @@ -20,10 +21,22 @@ function DefineMessage(fn, options)
end
end
end
for _, ctor in statusLib.GetRegisteredStatusEffects() do
if foundCtor then
break
end
for name, method in ctor do
if method == fn then
foundCtor, methodName = ctor, name
break
end
end
end

local isInvalid = skillLib.SkillBase[methodName] ~= nil
or skillLib.Skill[methodName] ~= nil
or holdable.HoldableSkill[methodName] ~= nil
or statusLib.StatusEffect[methodName] ~= nil

if not foundCtor or isInvalid or not rawget(foundCtor, methodName) then
utility.logError("Provided function is not a valid skill method.")
Expand Down
9 changes: 9 additions & 0 deletions src/source/character.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,15 @@ export class Character {
return mapToArray(this.statusEffects).filter((T) => T.GetState().IsActive);
}

/**
* @internal
* @hidden
* Retrieves a status effect from the status effects map based on a given id.
*/
public GetStatusEffectFromId(Id: string) {
return this.statusEffects.get(Id);
}

/**
* Retrieves all status effects of a given type.
*/
Expand Down
56 changes: 30 additions & 26 deletions src/source/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,64 +114,68 @@ class Client {
});

const eventHandler = (serialized: SerializedData) => {
const [Name, MethodName, PackedArgs] = messageSerializer.deserialize(
serialized.buffer,
serialized.blobs,
);
const [ContentType, Name, MethodName, PackedArgs] =
messageSerializer.deserialize(serialized.buffer, serialized.blobs);
const character = Character.GetLocalCharacter();
if (!character) return;

const skill = character.GetSkillFromString(Name);
if (!skill) return;
const messageClass =
ContentType === "Skill"
? character.GetSkillFromString(Name)
: character.GetStatusEffectFromId(Name);
if (!messageClass) return;

const args = RestoreArgs(PackedArgs);

const config = Reflect.getMetadata(skill, `Config_${MethodName}`) as
| MessageOptions
| undefined;
if (config?.OnlyWhenActive && !skill.GetState().IsActive) return;
const config = Reflect.getMetadata(
messageClass,
`Config_${MethodName}`,
) as MessageOptions | undefined;
if (config?.OnlyWhenActive && !messageClass.GetState().IsActive) return;

if (config?.Validators) {
if (!ValidateArgs(config.Validators, args)) return;
}

const method = skill[MethodName as never] as (
self: UnknownSkill,
const method = messageClass[MethodName as never] as (
self: UnknownSkill | UnknownStatus,
...args: unknown[]
) => unknown;
method(skill, ...args);
method(messageClass, ...args);
};
ClientEvents.messageToClient.connect(eventHandler);
ClientEvents.messageToClient_urel.connect(eventHandler);

ClientFunctions.messageToClient.setCallback((serialized) => {
const [Name, MethodName, PackedArgs] = messageSerializer.deserialize(
serialized.buffer,
serialized.blobs,
);
const [ContentType, Name, MethodName, PackedArgs] =
messageSerializer.deserialize(serialized.buffer, serialized.blobs);
const character = Character.GetLocalCharacter();
if (!character) return;

const skill = character.GetSkillFromString(Name);
if (!skill) return;
const messageClass =
ContentType === "Skill"
? character.GetSkillFromString(Name)
: character.GetStatusEffectFromId(Name);
if (!messageClass) return;

const args = RestoreArgs(PackedArgs);

const config = Reflect.getMetadata(skill, `Config_${MethodName}`) as
| MessageOptions
| undefined;
if (config?.OnlyWhenActive && !skill.GetState().IsActive)
const config = Reflect.getMetadata(
messageClass,
`Config_${MethodName}`,
) as MessageOptions | undefined;
if (config?.OnlyWhenActive && !messageClass.GetState().IsActive)
return INVALID_MESSAGE_STR;

if (config?.Validators) {
if (!ValidateArgs(config.Validators, args)) return INVALID_MESSAGE_STR;
}

const method = skill[MethodName as never] as (
self: UnknownSkill,
const method = messageClass[MethodName as never] as (
self: UnknownSkill | UnknownStatus,
...args: unknown[]
) => Promise<unknown> | unknown;
const returnedValue = method(skill, ...args);
const returnedValue = method(messageClass, ...args);
if (Promise.is(returnedValue)) {
const [success, value] = returnedValue.await();
return success ? value : INVALID_MESSAGE_STR;
Expand Down
19 changes: 13 additions & 6 deletions src/source/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import {
} from "./networking";
import { messageSerializer } from "./serdes";
import { SkillBase } from "./skill";
import { StatusEffect } from "./statusEffect";
import {
instanceofConstructor,
isClientContext,
isServerContext,
logError,
logWarning,
} from "./utility";

/**
Expand Down Expand Up @@ -59,9 +59,11 @@ export function Message<T extends MessageOptions>(Options: T) {
methodName: string,
_: TypedPropertyDescriptor<GetDesiredMethodType<ValidateUnreliable<T>>>,
) => {
if (!instanceofConstructor(ctor as never, SkillBase as never)) {
logError(`${ctor} is not a valid skill constructor.`);
}
const contentType = instanceofConstructor(ctor as never, SkillBase as never)
? "Skill"
: instanceofConstructor(ctor as never, StatusEffect as never)
? "StatusEffect"
: logError(`${ctor} is not a valid skill/ status effect constructor.`);
if (!rawget(ctor, methodName)) {
logError(`${ctor} does not have a method named ${methodName}.`);
}
Expand Down Expand Up @@ -97,10 +99,15 @@ export function Message<T extends MessageOptions>(Options: T) {

if (current === Options.Destination) return;

ctor[methodName] = function (this: SkillBase, ...args: unknown[]) {
ctor[methodName] = function (
this: SkillBase | StatusEffect,
...args: unknown[]
) {
if (!this.Player) return;

const serialized = messageSerializer.serialize([
this.Name,
contentType,
contentType === "Skill" ? this.Name : this.GetId(),
methodName,
ConvertArgs(args),
]);
Expand Down
7 changes: 6 additions & 1 deletion src/source/serdes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ export const skillRequestSerializer =
>();
export const messageSerializer =
createBinarySerializer<
[Name: string, MethodName: string, Args: Map<number, unknown>]
[
ContentType: "Skill" | "StatusEffect",
Name: string,
MethodName: string,
Args: Map<number, unknown>,
]
>();

export type SerializedData = {
Expand Down
59 changes: 33 additions & 26 deletions src/source/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { atom } from "@rbxts/charm";
import CharmSync from "@rbxts/charm-sync";
import { Players } from "@rbxts/services";
import { t } from "@rbxts/t";
import type { UnknownStatus } from "exports";
import {
isClientContext,
logError,
Expand Down Expand Up @@ -146,68 +147,74 @@ class Server {
});

const eventHandler = (Player: Player, serialized: SerializedData) => {
const [Name, MethodName, PackedArgs] = messageSerializer.deserialize(
serialized.buffer,
serialized.blobs,
);
const [ContentType, Name, MethodName, PackedArgs] =
messageSerializer.deserialize(serialized.buffer, serialized.blobs);
const playerCharacter = Player.Character;
if (!playerCharacter) return;

const character = Character.GetCharacterFromInstance(playerCharacter);
if (!character || character.Player !== Player) return;

const skill = character.GetSkillFromString(Name);
if (!skill) return;
let messageClass: UnknownSkill | UnknownStatus | undefined;
if (ContentType === "Skill") {
messageClass = character.GetSkillFromString(Name);
} else if (ContentType === "StatusEffect") {
messageClass = character.GetStatusEffectFromId(Name);
}
if (!messageClass) return;

const args = RestoreArgs(PackedArgs);
const config = Reflect.getMetadata(skill, `Config_${MethodName}`) as
| MessageOptions
| undefined;
if (config?.OnlyWhenActive && !skill.GetState().IsActive) return;
const config = Reflect.getMetadata(
messageClass,
`Config_${MethodName}`,
) as MessageOptions | undefined;
if (config?.OnlyWhenActive && !messageClass.GetState().IsActive) return;

if (config?.Validators) {
if (!ValidateArgs(config.Validators, args)) return;
}

const method = skill[MethodName as never] as (
self: UnknownSkill,
const method = messageClass[MethodName as never] as (
self: UnknownSkill | UnknownStatus,
...args: unknown[]
) => unknown;
method(skill, ...args);
method(messageClass, ...args);
};
ServerEvents.messageToServer.connect(eventHandler);
ServerEvents.messageToServer_urel.connect(eventHandler);

ServerFunctions.messageToServer.setCallback((Player, serialized) => {
const [Name, MethodName, PackedArgs] = messageSerializer.deserialize(
serialized.buffer,
serialized.blobs,
);
const [ContentType, Name, MethodName, PackedArgs] =
messageSerializer.deserialize(serialized.buffer, serialized.blobs);
const playerCharacter = Player.Character;
if (!playerCharacter) return;

const character = Character.GetCharacterFromInstance(playerCharacter);
if (!character || character.Player !== Player) return;

const skill = character.GetSkillFromString(Name);
if (!skill) return;
const messageClass =
ContentType === "Skill"
? character.GetSkillFromString(Name)
: character.GetStatusEffectFromId(Name);
if (!messageClass) return;

const args = RestoreArgs(PackedArgs);
const config = Reflect.getMetadata(skill, `Config_${MethodName}`) as
| MessageOptions
| undefined;
if (config?.OnlyWhenActive && !skill.GetState().IsActive)
const config = Reflect.getMetadata(
messageClass,
`Config_${MethodName}`,
) as MessageOptions | undefined;
if (config?.OnlyWhenActive && !messageClass.GetState().IsActive)
return INVALID_MESSAGE_STR;

if (config?.Validators) {
if (!ValidateArgs(config.Validators, args)) return INVALID_MESSAGE_STR;
}

const method = skill[MethodName as never] as (
self: UnknownSkill,
const method = messageClass[MethodName as never] as (
self: UnknownSkill | UnknownStatus,
...args: unknown[]
) => Promise<unknown> | unknown;
const returnedValue = method(skill, ...args);
const returnedValue = method(messageClass, ...args);
if (Promise.is(returnedValue)) {
const [_, value] = returnedValue.await();
return value;
Expand Down
2 changes: 1 addition & 1 deletion src/source/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export abstract class SkillBase<
};
private destroyed = false;
private metadata?: Metadata;
protected readonly Name = tostring(getmetatable(this));
public readonly Name = tostring(getmetatable(this));
/**
* A table of arguments provided after the Character in .new().
*/
Expand Down
11 changes: 11 additions & 0 deletions src/source/statusEffect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,3 +613,14 @@ export function StatusEffectDecorator<T extends StatusEffectConstructor>(
export function GetRegisteredStatusEffectConstructor(Name: string) {
return registeredStatuses.get(Name);
}

/**
* @internal
* @hidden
*/
export function GetRegisteredStatusEffects() {
return table.freeze(table.clone(registeredStatuses)) as ReadonlyMap<
string,
StatusEffectConstructor
>;
}