yukinovel
Version:
Yukinovel is a simple web visual novel engine.
152 lines (151 loc) • 5.56 kB
JavaScript
export class PluginManager {
constructor(game) {
this.plugins = {};
this.isInitialized = false;
this.game = game;
}
async register(plugin) {
if (this.plugins[plugin.metadata.name]) {
throw new Error(`Plugin "${plugin.metadata.name}" is already registered`);
}
this.validatePlugin(plugin);
await this.checkDependencies(plugin);
if (this.isInitialized && plugin.initialize) {
await plugin.initialize(this.game, this);
}
this.plugins[plugin.metadata.name] = plugin;
console.log(`Plugin "${plugin.metadata.name}" v${plugin.metadata.version} registered successfully`);
}
async unregister(pluginName) {
const plugin = this.plugins[pluginName];
if (!plugin) {
console.warn(`Plugin "${pluginName}" is not registered`);
return;
}
const dependentPlugins = Object.values(this.plugins).filter(p => p.metadata.dependencies?.includes(pluginName));
if (dependentPlugins.length > 0) {
const dependentNames = dependentPlugins.map(p => p.metadata.name).join(', ');
throw new Error(`Cannot unregister plugin "${pluginName}" because it is required by: ${dependentNames}`);
}
if (plugin.dispose) {
await plugin.dispose();
}
delete this.plugins[pluginName];
console.log(`Plugin "${pluginName}" unregistered successfully`);
}
async initialize() {
if (this.isInitialized) {
console.warn('PluginManager is already initialized');
return;
}
const pluginList = Object.values(this.plugins);
pluginList.sort((a, b) => (b.metadata.priority || 0) - (a.metadata.priority || 0));
for (const plugin of pluginList) {
if (plugin.initialize) {
try {
await plugin.initialize(this.game, this);
console.log(`Plugin "${plugin.metadata.name}" initialized`);
}
catch (error) {
console.error(`Failed to initialize plugin "${plugin.metadata.name}":`, error);
}
}
}
this.isInitialized = true;
console.log('All plugins initialized successfully');
}
async executeHooks(eventType, context) {
const pluginList = Object.values(this.plugins);
pluginList.sort((a, b) => (b.metadata.priority || 0) - (a.metadata.priority || 0));
for (const plugin of pluginList) {
if (plugin.hooks && plugin.hooks[eventType]) {
try {
const hook = plugin.hooks[eventType];
await hook(context);
}
catch (error) {
console.error(`Error in plugin "${plugin.metadata.name}" hook "${eventType}":`, error);
}
}
}
}
async executeCustomHooks(eventName, context) {
const customContext = { ...context, eventName };
for (const plugin of Object.values(this.plugins)) {
if (plugin.hooks?.onCustomEvent) {
try {
await plugin.hooks.onCustomEvent(customContext);
}
catch (error) {
console.error(`Error in plugin "${plugin.metadata.name}" custom hook "${eventName}":`, error);
}
}
}
}
getPlugin(name) {
return this.plugins[name];
}
getAllPlugins() {
return { ...this.plugins };
}
getPluginAPI(name) {
const plugin = this.plugins[name];
return plugin?.api || {};
}
hasPlugin(name) {
return !!this.plugins[name];
}
getPluginMetadata(name) {
return this.plugins[name]?.metadata;
}
getPluginNames() {
return Object.keys(this.plugins);
}
async dispose() {
for (const plugin of Object.values(this.plugins)) {
if (plugin.dispose) {
try {
await plugin.dispose();
}
catch (error) {
console.error(`Error disposing plugin "${plugin.metadata.name}":`, error);
}
}
}
this.plugins = {};
this.isInitialized = false;
console.log('All plugins disposed');
}
validatePlugin(plugin) {
if (!plugin.metadata) {
throw new Error('Plugin must have metadata');
}
if (!plugin.metadata.name) {
throw new Error('Plugin must have a name');
}
if (!plugin.metadata.version) {
throw new Error('Plugin must have a version');
}
const versionRegex = /^\d+\.\d+\.\d+/;
if (!versionRegex.test(plugin.metadata.version)) {
console.warn(`Plugin "${plugin.metadata.name}" has invalid version format. Expected semantic versioning (x.y.z)`);
}
}
async checkDependencies(plugin) {
if (!plugin.metadata.dependencies) {
return;
}
for (const dependency of plugin.metadata.dependencies) {
if (!this.plugins[dependency]) {
throw new Error(`Plugin "${plugin.metadata.name}" requires dependency "${dependency}" which is not registered`);
}
}
}
createHookContext(additionalContext = {}) {
return {
game: this.game,
state: this.game.getState(),
...additionalContext
};
}
}