From 9d1eab0f1c11c23d849ef3e9c899eab28443fa7a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 10 Jan 2026 03:04:10 +0000 Subject: [PATCH 1/2] Add type definitions for ConfigService compatibility Co-authored-by: yasuatsu620 --- Modules/Config/Type.luau | 102 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Modules/Config/Type.luau diff --git a/Modules/Config/Type.luau b/Modules/Config/Type.luau new file mode 100644 index 0000000..ce179a8 --- /dev/null +++ b/Modules/Config/Type.luau @@ -0,0 +1,102 @@ +--!strict +--[[ + ConfigService対応のための型定義 + + RobloxのConfigService/ConfigSnapshot APIに対応し、 + 既存のConfiguration/Attributeベースの設定も引き続きサポート +]] + +export type ConfigValueType = "boolean" | "number" | "string" + +--[[ + 単一の設定キーの定義 + + @field Default - デフォルト値(ConfigServiceから取得できない場合に使用) + @field Type - 期待される値の型(バリデーション用、省略時はDefaultから推論) + @field Observe - 値の変更を監視するか(デフォルト: false) +]] +export type ConfigKeyDefinition = { + Default: boolean | number | string, + Type: ConfigValueType?, + Observe: boolean?, +} + +--[[ + 設定スキーマ全体 + キー名をキーとし、ConfigKeyDefinitionを値とする辞書 + + 使用例: + local schema: ConfigSchema = { + MaxPlayers = { Default = 10, Type = "number", Observe = true }, + GameMode = { Default = "classic", Type = "string" }, + DebugEnabled = { Default = false, Type = "boolean" }, + } +]] +export type ConfigSchema = { + [string]: ConfigKeyDefinition, +} + +--[[ + 設定ソースの種類 + + - ConfigService: 新しいRoblox ConfigService API + - Configuration: 従来のConfigurationインスタンス(ValueBase子要素) + - Attribute: インスタンスのAttribute +]] +export type ConfigSourceType = "ConfigService" | "Configuration" | "Attribute" + +--[[ + Config.new用のオプション + + @field Schema - 設定スキーマ(キー定義とデフォルト値) + @field Source - 設定ソースの種類(省略時は自動判定) + @field Player - プレイヤー固有の設定用(ConfigService + Experiment対応) + @field AutoRefresh - ConfigSnapshotの自動更新(デフォルト: false) + @field Instance - ConfigurationまたはAttributeを持つインスタンス(従来互換用) +]] +export type ConfigOptions = { + Schema: ConfigSchema, + Source: ConfigSourceType?, + Player: Player?, + AutoRefresh: boolean?, + Instance: (Configuration | Instance)?, +} + +--[[ + ConfigSnapshotのエラー状態 + Roblox公式のEnum.ConfigSnapshotErrorStateに対応 +]] +export type ConfigErrorState = "None" | "LoadFailed" + +--[[ + 設定値の変更イベント情報 +]] +export type ConfigChangeInfo = { + Key: string, + OldValue: boolean | number | string | nil, + NewValue: boolean | number | string, +} + +--[[ + Configインスタンスの型定義 +]] +export type Config = { + -- プロパティ + Source: ConfigSourceType, + Schema: ConfigSchema, + Error: ConfigErrorState?, + Outdated: boolean?, + + -- 値へのアクセス(動的キー) + [string]: any, + + -- メソッド + Get: (self: Config, key: string) -> (boolean | number | string)?, + GetWithDefault: (self: Config, key: string, default: any) -> any, + Observe: (self: Config) -> RBXScriptConnection, + ObserveKey: (self: Config, key: string) -> RBXScriptConnection, + Refresh: (self: Config) -> (), + Destroy: (self: Config) -> (), +} + +return nil From 548b53b584763af32498b9cadb60ac15c6eae898 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 10 Jan 2026 03:08:32 +0000 Subject: [PATCH 2/2] Refactor: Introduce new Config API and concrete implementations Co-authored-by: yasuatsu620 --- Modules/Config/AttributeConfig.luau | 132 +++++++++++++ Modules/Config/BaseConfig.luau | 192 ++++++++++++++++++ Modules/Config/ConfigServiceConfig.luau | 182 +++++++++++++++++ Modules/Config/ConfigurationConfig.luau | 154 +++++++++++++++ Modules/Config/Type.luau | 71 ++++--- Modules/Config/init.luau | 247 +++++++++++++++++------- 6 files changed, 884 insertions(+), 94 deletions(-) create mode 100644 Modules/Config/AttributeConfig.luau create mode 100644 Modules/Config/BaseConfig.luau create mode 100644 Modules/Config/ConfigServiceConfig.luau create mode 100644 Modules/Config/ConfigurationConfig.luau diff --git a/Modules/Config/AttributeConfig.luau b/Modules/Config/AttributeConfig.luau new file mode 100644 index 0000000..b9f118b --- /dev/null +++ b/Modules/Config/AttributeConfig.luau @@ -0,0 +1,132 @@ +--!strict +--[[ + AttributeConfig - インスタンスのAttributeを使用した実装 + + Roblox インスタンスのAttribute機能に対応 + 後方互換性のために維持 +]] + +local BaseConfig = require(script.Parent.BaseConfig) +local Types = require(script.Parent.Type) + +type ConfigValue = Types.ConfigValue +type ConfigSchema = Types.ConfigSchema + +local AttributeConfig = {} +AttributeConfig.__index = AttributeConfig +setmetatable(AttributeConfig, { __index = BaseConfig }) + +export type AttributeConfig = BaseConfig.BaseConfig & { + Instance: Instance, +} + +--[[ + 新しいAttributeConfigインスタンスを作成 + + @param instance - Attributeを持つインスタンス + @param schema - 設定スキーマ(オプション) + @return AttributeConfig +]] +function AttributeConfig.new( + instance: Instance, + schema: ConfigSchema? +): AttributeConfig + local self = BaseConfig._new("Attribute", schema) :: any + setmetatable(self, AttributeConfig) + + self.Instance = instance + + -- 初期値をロード + self:_loadValues() + + return self +end + +--[[ + インスタンスからAttributeの値を読み込み +]] +function AttributeConfig:_loadValues(): () + local attributes = self.Instance:GetAttributes() + + for key, value in pairs(attributes) do + -- スキーマがある場合はバリデーション + if self.Schema then + local definition = self.Schema[key] + if definition then + if self:_validateType(key, value) then + self:_setValue(key, value) + else + warn(string.format( + "[AttributeConfig] Type mismatch for key '%s': expected %s, got %s", + key, + definition.Type or typeof(definition.Default), + typeof(value) + )) + self:_setValue(key, definition.Default) + end + else + -- スキーマにないキーもロード + self:_setValue(key, value) + end + else + self:_setValue(key, value) + end + end + + -- デフォルト値を適用(スキーマにあるがAttributeにないキー) + self:_applyDefaults() +end + +--[[ + 変更監視を開始 + + @return RBXScriptSignal - 変更イベント +]] +function AttributeConfig:Observe(): RBXScriptSignal + local event = BaseConfig.Observe(self) + + -- AttributeChangedは全属性の変更を一つのイベントで受け取る + if self.Connections._attributeChanged == nil then + self.Connections._attributeChanged = self.Instance.AttributeChanged:Connect(function(attributeName: string) + -- スキーマがある場合は監視対象かチェック + if not self:_shouldObserve(attributeName) then + return + end + + local newValue = self.Instance:GetAttribute(attributeName) + if newValue ~= nil and self:_validateType(attributeName, newValue) then + self:_setValue(attributeName, newValue) + end + end) + end + + return event +end + +--[[ + 最新値に更新 +]] +function AttributeConfig:Refresh(): () + self:_loadValues() +end + +--[[ + Attributeの値を更新(双方向バインディング用) + + @param key - キー名 + @param value - 新しい値 + @return boolean - 成功したかどうか +]] +function AttributeConfig:Set(key: string, value: ConfigValue): boolean + local success = pcall(function() + self.Instance:SetAttribute(key, value) + end) + + if success then + self:_setValue(key, value) + end + + return success +end + +return AttributeConfig diff --git a/Modules/Config/BaseConfig.luau b/Modules/Config/BaseConfig.luau new file mode 100644 index 0000000..dac4155 --- /dev/null +++ b/Modules/Config/BaseConfig.luau @@ -0,0 +1,192 @@ +--!strict +--[[ + BaseConfig - 全てのConfig実装の基底クラス + + 共通のインターフェースと基本実装を提供する抽象クラス + 具象クラスはこれを継承して各ソースタイプ固有の実装を行う +]] + +local Types = require(script.Parent.Type) + +export type ConfigValue = Types.ConfigValue +export type ConfigSchema = Types.ConfigSchema +export type ConfigSourceType = Types.ConfigSourceType +export type ConfigErrorState = Types.ConfigErrorState +export type ConnectionMap = Types.ConnectionMap + +local BaseConfig = {} +BaseConfig.__index = BaseConfig + +export type BaseConfig = { + -- プロパティ + Source: ConfigSourceType, + Schema: ConfigSchema?, + Error: ConfigErrorState?, + Values: { [string]: ConfigValue }, + Connections: ConnectionMap, + Observer: BindableEvent?, + + -- 動的キーアクセス + [string]: any, +} + +--[[ + 新しいBaseConfigインスタンスを作成(サブクラス用) + + @param source - 設定ソースの種類 + @param schema - 設定スキーマ(オプション) + @return BaseConfig +]] +function BaseConfig._new(source: ConfigSourceType, schema: ConfigSchema?): BaseConfig + local self = setmetatable({}, BaseConfig) :: any + self.Source = source + self.Schema = schema + self.Error = "None" + self.Values = {} + self.Connections = {} + self.Observer = nil + return self +end + +--[[ + スキーマからデフォルト値を適用 +]] +function BaseConfig:_applyDefaults(): () + if self.Schema == nil then + return + end + + for key, definition in pairs(self.Schema) do + if self.Values[key] == nil then + self.Values[key] = definition.Default + end + end +end + +--[[ + 値の型をバリデーション + + @param key - キー名 + @param value - バリデーションする値 + @return boolean - バリデーション成功かどうか +]] +function BaseConfig:_validateType(key: string, value: any): boolean + if self.Schema == nil then + return true + end + + local definition = self.Schema[key] + if definition == nil then + return true + end + + local expectedType = definition.Type + if expectedType == nil then + -- Defaultから型を推論 + expectedType = typeof(definition.Default) :: Types.ConfigValueType + end + + return typeof(value) == expectedType +end + +--[[ + キーが監視対象かどうかを判定 + + @param key - キー名 + @return boolean +]] +function BaseConfig:_shouldObserve(key: string): boolean + if self.Schema == nil then + return true -- スキーマがなければ全て監視 + end + + local definition = self.Schema[key] + if definition == nil then + return false + end + + return definition.Observe == true +end + +--[[ + 値を設定し、必要に応じて変更を通知 + + @param key - キー名 + @param value - 新しい値 +]] +function BaseConfig:_setValue(key: string, value: ConfigValue): () + local oldValue = self.Values[key] + self.Values[key] = value + self[key] = value + + if self.Observer and oldValue ~= value then + self.Observer:Fire(key, value, oldValue) + end +end + +--[[ + 値を取得 + + @param key - キー名 + @return ConfigValue? - 値(存在しない場合はnil) +]] +function BaseConfig:Get(key: string): ConfigValue? + return self.Values[key] +end + +--[[ + デフォルト値付きで値を取得 + + @param key - キー名 + @param default - デフォルト値 + @return ConfigValue +]] +function BaseConfig:GetWithDefault(key: string, default: ConfigValue): ConfigValue + local value = self.Values[key] + if value == nil then + return default + end + return value +end + +--[[ + 変更監視を開始(サブクラスでオーバーライド) + + @return RBXScriptSignal - 変更イベント +]] +function BaseConfig:Observe(): RBXScriptSignal + if self.Observer == nil then + self.Observer = Instance.new("BindableEvent") + end + return self.Observer.Event +end + +--[[ + 最新値に更新(サブクラスでオーバーライド) +]] +function BaseConfig:Refresh(): () + -- サブクラスで実装 +end + +--[[ + リソースを解放 +]] +function BaseConfig:Destroy(): () + -- コネクションを切断 + for key, connection in pairs(self.Connections) do + connection:Disconnect() + self.Connections[key] = nil + end + + -- Observerを破棄 + if self.Observer then + self.Observer:Destroy() + self.Observer = nil + end + + -- テーブルをクリア + table.clear(self.Values) + table.clear(self) +end + +return BaseConfig diff --git a/Modules/Config/ConfigServiceConfig.luau b/Modules/Config/ConfigServiceConfig.luau new file mode 100644 index 0000000..3122401 --- /dev/null +++ b/Modules/Config/ConfigServiceConfig.luau @@ -0,0 +1,182 @@ +--!strict +--[[ + ConfigServiceConfig - RobloxのConfigService APIを使用した実装 + + 新しいRoblox ConfigService/ConfigSnapshot APIに対応 + Experiment機能によるプレイヤー固有の設定もサポート +]] + +local ConfigService = game:GetService("ConfigService") + +local BaseConfig = require(script.Parent.BaseConfig) +local Types = require(script.Parent.Type) + +type ConfigValue = Types.ConfigValue +type ConfigSchema = Types.ConfigSchema +type ConfigErrorState = Types.ConfigErrorState + +local ConfigServiceConfig = {} +ConfigServiceConfig.__index = ConfigServiceConfig +setmetatable(ConfigServiceConfig, { __index = BaseConfig }) + +export type ConfigServiceConfig = BaseConfig.BaseConfig & { + Player: Player?, + AutoRefresh: boolean, + Snapshot: any?, -- ConfigSnapshot(Roblox型) +} + +--[[ + 新しいConfigServiceConfigインスタンスを作成 + + @param schema - 設定スキーマ + @param player - プレイヤー(Experiment用、オプション) + @param autoRefresh - 自動更新を有効にするか + @return ConfigServiceConfig +]] +function ConfigServiceConfig.new( + schema: ConfigSchema, + player: Player?, + autoRefresh: boolean? +): ConfigServiceConfig + local self = BaseConfig._new("ConfigService", schema) :: any + setmetatable(self, ConfigServiceConfig) + + self.Player = player + self.AutoRefresh = autoRefresh or false + self.Snapshot = nil + + -- 初期ロード + self:_loadSnapshot() + + return self +end + +--[[ + ConfigSnapshotをロード +]] +function ConfigServiceConfig:_loadSnapshot(): () + local success, result = pcall(function() + if self.Player then + return ConfigService:GetConfigForPlayerAsync(self.Player) + else + return ConfigService:GetConfigAsync() + end + end) + + if not success then + warn("[ConfigServiceConfig] Failed to load snapshot:", result) + self.Error = "LoadFailed" + self:_applyDefaults() + return + end + + self.Snapshot = result + + -- エラー状態をチェック + if result.Error ~= Enum.ConfigSnapshotErrorState.None then + self.Error = "LoadFailed" + self:_applyDefaults() + return + end + + self.Error = "None" + self:_loadValuesFromSnapshot() +end + +--[[ + ConfigSnapshotから値を読み込み +]] +function ConfigServiceConfig:_loadValuesFromSnapshot(): () + if self.Snapshot == nil or self.Schema == nil then + return + end + + for key, definition in pairs(self.Schema) do + local success, value = pcall(function() + return self.Snapshot:GetValue(key) + end) + + if success and value ~= nil then + if self:_validateType(key, value) then + self:_setValue(key, value) + else + warn(string.format( + "[ConfigServiceConfig] Type mismatch for key '%s': expected %s, got %s", + key, + definition.Type or typeof(definition.Default), + typeof(value) + )) + self:_setValue(key, definition.Default) + end + else + self:_setValue(key, definition.Default) + end + end +end + +--[[ + 変更監視を開始 + + @return RBXScriptSignal - 変更イベント +]] +function ConfigServiceConfig:Observe(): RBXScriptSignal + local event = BaseConfig.Observe(self) + + if self.Snapshot == nil or self.Schema == nil then + return event + end + + -- 監視対象のキーに対してシグナルを接続 + for key, definition in pairs(self.Schema) do + if self:_shouldObserve(key) and self.Connections[key] == nil then + local success, signal = pcall(function() + return self.Snapshot:GetValueChangedSignal(key) + end) + + if success and signal then + self.Connections[key] = signal:Connect(function() + local newValue = self.Snapshot:GetValue(key) + if newValue ~= nil then + self:_setValue(key, newValue) + end + end) + end + end + end + + return event +end + +--[[ + 最新値に更新 +]] +function ConfigServiceConfig:Refresh(): () + if self.Snapshot == nil then + self:_loadSnapshot() + return + end + + local success, err = pcall(function() + self.Snapshot:Refresh() + end) + + if success then + self:_loadValuesFromSnapshot() + else + warn("[ConfigServiceConfig] Failed to refresh:", err) + end +end + +--[[ + Snapshotが古くなっているかチェック + + @return boolean +]] +function ConfigServiceConfig:IsOutdated(): boolean + if self.Snapshot == nil then + return true + end + return self.Snapshot.Outdated == true +end + +return ConfigServiceConfig diff --git a/Modules/Config/ConfigurationConfig.luau b/Modules/Config/ConfigurationConfig.luau new file mode 100644 index 0000000..bf0584b --- /dev/null +++ b/Modules/Config/ConfigurationConfig.luau @@ -0,0 +1,154 @@ +--!strict +--[[ + ConfigurationConfig - ConfigurationインスタンスのValueBaseを使用した実装 + + 従来のRoblox Configurationインスタンス(ValueBase子要素)に対応 + 後方互換性のために維持 +]] + +local BaseConfig = require(script.Parent.BaseConfig) +local Types = require(script.Parent.Type) + +type ConfigValue = Types.ConfigValue +type ConfigSchema = Types.ConfigSchema + +local ConfigurationConfig = {} +ConfigurationConfig.__index = ConfigurationConfig +setmetatable(ConfigurationConfig, { __index = BaseConfig }) + +export type ConfigurationConfig = BaseConfig.BaseConfig & { + Instance: Configuration, +} + +--[[ + 新しいConfigurationConfigインスタンスを作成 + + @param instance - Configurationインスタンス + @param schema - 設定スキーマ(オプション) + @return ConfigurationConfig +]] +function ConfigurationConfig.new( + instance: Configuration, + schema: ConfigSchema? +): ConfigurationConfig + local self = BaseConfig._new("Configuration", schema) :: any + setmetatable(self, ConfigurationConfig) + + self.Instance = instance + + -- 初期値をロード + self:_loadValues() + + return self +end + +--[[ + ConfigurationからValueBase子要素の値を読み込み +]] +function ConfigurationConfig:_loadValues(): () + for _, child in pairs(self.Instance:GetChildren()) do + if not child:IsA("ValueBase") then + continue + end + + local valueBase = child :: ValueBase + local key = valueBase.Name + local value = valueBase.Value + + -- スキーマがある場合はバリデーション + if self.Schema then + local definition = self.Schema[key] + if definition then + if self:_validateType(key, value) then + self:_setValue(key, value) + else + warn(string.format( + "[ConfigurationConfig] Type mismatch for key '%s': expected %s, got %s", + key, + definition.Type or typeof(definition.Default), + typeof(value) + )) + self:_setValue(key, definition.Default) + end + else + -- スキーマにないキーもロード + self:_setValue(key, value) + end + else + self:_setValue(key, value) + end + end + + -- デフォルト値を適用(スキーマにあるがInstanceにないキー) + self:_applyDefaults() +end + +--[[ + 変更監視を開始 + + @return RBXScriptSignal - 変更イベント +]] +function ConfigurationConfig:Observe(): RBXScriptSignal + local event = BaseConfig.Observe(self) + + for _, child in pairs(self.Instance:GetChildren()) do + if not child:IsA("ValueBase") then + continue + end + + local valueBase = child :: ValueBase + local key = valueBase.Name + + -- 既に接続済みならスキップ + if self.Connections[key] then + continue + end + + -- スキーマがある場合は監視対象かチェック + if not self:_shouldObserve(key) then + continue + end + + self.Connections[key] = valueBase:GetPropertyChangedSignal("Value"):Connect(function() + local newValue = valueBase.Value + if self:_validateType(key, newValue) then + self:_setValue(key, newValue) + end + end) + end + + return event +end + +--[[ + 最新値に更新 +]] +function ConfigurationConfig:Refresh(): () + self:_loadValues() +end + +--[[ + ValueBaseの値を更新(双方向バインディング用) + + @param key - キー名 + @param value - 新しい値 + @return boolean - 成功したかどうか +]] +function ConfigurationConfig:Set(key: string, value: ConfigValue): boolean + local valueBase = self.Instance:FindFirstChild(key) + if valueBase == nil or not valueBase:IsA("ValueBase") then + return false + end + + local success = pcall(function() + (valueBase :: any).Value = value + end) + + if success then + self:_setValue(key, value) + end + + return success +end + +return ConfigurationConfig diff --git a/Modules/Config/Type.luau b/Modules/Config/Type.luau index ce179a8..16357ca 100644 --- a/Modules/Config/Type.luau +++ b/Modules/Config/Type.luau @@ -6,8 +6,12 @@ 既存のConfiguration/Attributeベースの設定も引き続きサポート ]] +-- サポートする値の型 export type ConfigValueType = "boolean" | "number" | "string" +-- 設定値として許容される型 +export type ConfigValue = boolean | number | string + --[[ 単一の設定キーの定義 @@ -16,7 +20,7 @@ export type ConfigValueType = "boolean" | "number" | "string" @field Observe - 値の変更を監視するか(デフォルト: false) ]] export type ConfigKeyDefinition = { - Default: boolean | number | string, + Default: ConfigValue, Type: ConfigValueType?, Observe: boolean?, } @@ -46,7 +50,29 @@ export type ConfigSchema = { export type ConfigSourceType = "ConfigService" | "Configuration" | "Attribute" --[[ - Config.new用のオプション + ConfigSnapshotのエラー状態 + Roblox公式のEnum.ConfigSnapshotErrorStateに対応 +]] +export type ConfigErrorState = "None" | "LoadFailed" + +--[[ + 設定値の変更イベント情報 +]] +export type ConfigChangeInfo = { + Key: string, + OldValue: ConfigValue?, + NewValue: ConfigValue, +} + +--[[ + Config.new用のオプション(従来互換) +]] +export type LegacyConfigOptions = { + Instance: Configuration | Instance, +} + +--[[ + Config.new用のオプション(新API) @field Schema - 設定スキーマ(キー定義とデフォルト値) @field Source - 設定ソースの種類(省略時は自動判定) @@ -63,40 +89,31 @@ export type ConfigOptions = { } --[[ - ConfigSnapshotのエラー状態 - Roblox公式のEnum.ConfigSnapshotErrorStateに対応 -]] -export type ConfigErrorState = "None" | "LoadFailed" - ---[[ - 設定値の変更イベント情報 -]] -export type ConfigChangeInfo = { - Key: string, - OldValue: boolean | number | string | nil, - NewValue: boolean | number | string, -} - ---[[ - Configインスタンスの型定義 + Configインターフェース + 全ての具象クラスが実装すべきメソッド ]] -export type Config = { +export type IConfig = { -- プロパティ Source: ConfigSourceType, - Schema: ConfigSchema, + Schema: ConfigSchema?, Error: ConfigErrorState?, - Outdated: boolean?, -- 値へのアクセス(動的キー) [string]: any, -- メソッド - Get: (self: Config, key: string) -> (boolean | number | string)?, - GetWithDefault: (self: Config, key: string, default: any) -> any, - Observe: (self: Config) -> RBXScriptConnection, - ObserveKey: (self: Config, key: string) -> RBXScriptConnection, - Refresh: (self: Config) -> (), - Destroy: (self: Config) -> (), + Get: (self: IConfig, key: string) -> ConfigValue?, + GetWithDefault: (self: IConfig, key: string, default: ConfigValue) -> ConfigValue, + Observe: (self: IConfig) -> RBXScriptSignal, + Refresh: (self: IConfig) -> (), + Destroy: (self: IConfig) -> (), +} + +--[[ + 内部で使用するコネクション管理用の型 +]] +export type ConnectionMap = { + [string]: RBXScriptConnection, } return nil diff --git a/Modules/Config/init.luau b/Modules/Config/init.luau index b17ddc4..48e4a9b 100644 --- a/Modules/Config/init.luau +++ b/Modules/Config/init.luau @@ -1,81 +1,194 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local Config = {} -Config.__index = Config +--!strict +--[[ + Config - 設定管理モジュール(ファクトリ) + + 複数の設定ソースに対応した統一的なインターフェースを提供 + - ConfigService: Robloxの新しいConfigService API(Experiment対応) + - Configuration: 従来のConfigurationインスタンス(ValueBase子要素) + - Attribute: インスタンスのAttribute + + 使用例: + ```lua + local Config = require(path.to.Config) + + -- 新しいConfigService API + local config = Config.new({ + Schema = { + MaxPlayers = { Default = 10, Type = "number", Observe = true }, + GameMode = { Default = "classic", Type = "string" }, + }, + Source = "ConfigService", + Player = player, -- Experiment用(オプション) + }) + + -- 従来のConfiguration + local config = Config.new({ + Schema = schema, + Source = "Configuration", + Instance = workspace.GameConfig, + }) + + -- 従来のAttribute + local config = Config.new({ + Schema = schema, + Source = "Attribute", + Instance = workspace.GameSettings, + }) + + -- レガシー互換(自動判定) + local config = Config.fromInstance(workspace.GameConfig) + + -- 値へのアクセス + print(config.MaxPlayers) + print(config:Get("GameMode")) + + -- 変更監視 + config:Observe():Connect(function(key, newValue, oldValue) + print(key, "changed from", oldValue, "to", newValue) + end) + + -- クリーンアップ + config:Destroy() + ``` +]] -export type Config = { - BaseInstance: Configuration | Instance, - ObserveChange: boolean, - Observe: () -> RBXScriptConnection, - Destroy: () -> (), - Type: "Config" | "Attribute", -} +local Types = require(script.Type) +local ConfigServiceConfig = require(script.ConfigServiceConfig) +local ConfigurationConfig = require(script.ConfigurationConfig) +local AttributeConfig = require(script.AttributeConfig) -function Config.new(src: Configuration | Instance): Config - local self = setmetatable({}, Config) - self.BaseInstance = src +-- 型のエクスポート +export type ConfigValue = Types.ConfigValue +export type ConfigValueType = Types.ConfigValueType +export type ConfigKeyDefinition = Types.ConfigKeyDefinition +export type ConfigSchema = Types.ConfigSchema +export type ConfigSourceType = Types.ConfigSourceType +export type ConfigOptions = Types.ConfigOptions +export type ConfigErrorState = Types.ConfigErrorState +export type IConfig = Types.IConfig - local isConfiguration = src:IsA("Configuration") - if isConfiguration == true then - self:GetLatest() - else - self:GetAttributes() - end - self.Type = isConfiguration == true and "Config" or "Attribute" - return self -end +-- 具象クラスの型エクスポート +export type ConfigServiceConfig = ConfigServiceConfig.ConfigServiceConfig +export type ConfigurationConfig = ConfigurationConfig.ConfigurationConfig +export type AttributeConfig = AttributeConfig.AttributeConfig -function Config:GetAttributes() - for name, value in pairs(self.BaseInstance:GetAttributes()) do - self[name] = value - end -end +local Config = {} -function Config:GetLatest() - for _, valueBase: ValueBase in pairs(self.BaseInstance:GetChildren()) do - if not valueBase:IsA("ValueBase") then - continue - end - self[valueBase.Name] = valueBase.Value - end +--[[ + 設定ソースの種類を自動判定 + + @param options - 設定オプション + @return ConfigSourceType +]] +local function detectSourceType(options: ConfigOptions): Types.ConfigSourceType + -- 明示的に指定されていればそれを使用 + if options.Source then + return options.Source + end + + -- Instanceがなければ ConfigService + if options.Instance == nil then + return "ConfigService" + end + + -- Configuration インスタンスかどうか + if options.Instance:IsA("Configuration") then + return "Configuration" + end + + -- それ以外は Attribute + return "Attribute" end -function Config:Observe() - self.Connections = {} - self.Observer = Instance.new("BindableEvent") - if self.Type == "Config" then - for _, valueBase: ValueBase in pairs(self.BaseInstance:GetChildren()) do - if not valueBase:IsA("ValueBase") then - continue - end - self.Connections[valueBase.Name] = valueBase:GetPropertyChangedSignal("Value"):Connect(function() - warn("changed") - self.Observer:Fire(valueBase.Name, valueBase.Value) - end) - self[valueBase.Name] = valueBase.Value - end - else - self.Connections.attributeChangedConnection = self.BaseInstance.AttributeChanged:Connect(function(name) - self[name] = self.BaseInstance:GetAttribute(name) - self.Observer:Fire(name, self[name]) - end) - end +--[[ + 新しいConfigインスタンスを作成(ファクトリメソッド) + + 設定オプションに基づいて適切な具象クラスのインスタンスを生成 + + @param options - 設定オプション + @return IConfig - Configインスタンス +]] +function Config.new(options: ConfigOptions): IConfig + local sourceType = detectSourceType(options) + + if sourceType == "ConfigService" then + return ConfigServiceConfig.new( + options.Schema, + options.Player, + options.AutoRefresh + ) :: any + elseif sourceType == "Configuration" then + if options.Instance == nil then + error("[Config] Configuration source requires Instance option") + end + if not options.Instance:IsA("Configuration") then + error("[Config] Instance must be a Configuration for Configuration source") + end + return ConfigurationConfig.new( + options.Instance :: Configuration, + options.Schema + ) :: any + elseif sourceType == "Attribute" then + if options.Instance == nil then + error("[Config] Attribute source requires Instance option") + end + return AttributeConfig.new( + options.Instance, + options.Schema + ) :: any + end + + error("[Config] Unknown source type: " .. tostring(sourceType)) +end - return self.Observer.Event +--[[ + インスタンスからConfigを作成(レガシー互換) + + 自動的にConfigurationかAttributeかを判定 + スキーマなしで動作(全ての値を読み込み、全てを監視対象とする) + + @param instance - ConfigurationまたはAttributeを持つインスタンス + @return IConfig - Configインスタンス +]] +function Config.fromInstance(instance: Configuration | Instance): IConfig + if instance:IsA("Configuration") then + return ConfigurationConfig.new(instance :: Configuration, nil) :: any + else + return AttributeConfig.new(instance, nil) :: any + end end -function Config:Destroy() - if self.Connections == nil then - return - end - if next(self.Connections) == nil then - return - end +--[[ + ConfigServiceからConfigを作成(簡易版) + + @param schema - 設定スキーマ + @param player - プレイヤー(Experiment用、オプション) + @return IConfig - Configインスタンス +]] +function Config.fromConfigService(schema: ConfigSchema, player: Player?): IConfig + return ConfigServiceConfig.new(schema, player, false) :: any +end - for index, connection: RBXScriptConnection in pairs(self.Connections) do - connection:Disconnect() - self.Connections[index] = nil - end - table.clear(self) +--[[ + プレイヤー固有の設定を取得(Experiment対応) + + @param schema - 設定スキーマ + @param player - プレイヤー + @param autoRefresh - 自動更新を有効にするか + @return IConfig - Configインスタンス +]] +function Config.forPlayer( + schema: ConfigSchema, + player: Player, + autoRefresh: boolean? +): IConfig + return ConfigServiceConfig.new(schema, player, autoRefresh or false) :: any end +-- サブモジュールへの直接アクセス(上級者向け) +Config.Types = Types +Config.ConfigServiceConfig = ConfigServiceConfig +Config.ConfigurationConfig = ConfigurationConfig +Config.AttributeConfig = AttributeConfig + return Config