From c84c5a57b681e890cf044ddc2ee239bc962e81e6 Mon Sep 17 00:00:00 2001 From: "Tristan Poland (Trident_For_U)" <34868944+tristanpoland@users.noreply.github.com> Date: Sat, 19 Oct 2024 19:35:38 -0400 Subject: [PATCH] Added docs --- app/components/DocsCards.tsx | 202 +++++++++++++ app/components/TableOfContents.tsx | 39 +++ app/docs/DocsList.tsx | 42 +++ app/docs/[slug]/TableOfContents.tsx | 30 ++ app/docs/[slug]/markdown-styles.module.css | 91 ++++++ app/docs/[slug]/page.tsx | 44 +++ app/docs/page.tsx | 63 +++++ app/navbar.tsx | 123 ++++---- docs/about-horizon.md | 128 +++++++++ docs/api-experimental.md | 313 +++++++++++++++++++++ hooks/use-outside-click.ts | 23 ++ package-lock.json | 122 ++++++++ package.json | 2 + 13 files changed, 1161 insertions(+), 61 deletions(-) create mode 100644 app/components/DocsCards.tsx create mode 100644 app/components/TableOfContents.tsx create mode 100644 app/docs/DocsList.tsx create mode 100644 app/docs/[slug]/TableOfContents.tsx create mode 100644 app/docs/[slug]/markdown-styles.module.css create mode 100644 app/docs/[slug]/page.tsx create mode 100644 app/docs/page.tsx create mode 100644 docs/about-horizon.md create mode 100644 docs/api-experimental.md create mode 100644 hooks/use-outside-click.ts diff --git a/app/components/DocsCards.tsx b/app/components/DocsCards.tsx new file mode 100644 index 0000000..155ced4 --- /dev/null +++ b/app/components/DocsCards.tsx @@ -0,0 +1,202 @@ +"use client"; + +import React, { useEffect, useId, useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { useOutsideClick } from "@/hooks/use-outside-click"; +import { marked } from "marked"; +import Link from "next/link"; +import Image from "next/image"; + +interface DocFile { + slug: string; + title: string; + excerpt: string; + content: string; + firstImage?: string; +} + +function parseFileContent(content: string): DocFile { + const [, frontMatter, markdownContent] = content.split('---'); + const metadata = Object.fromEntries( + frontMatter.trim().split('\n').map(line => line.split(': ')) + ); + + const firstParagraph = markdownContent.trim().split('\n\n')[0]; + + return { + slug: '', // You'll need to set this elsewhere + title: metadata.title || '', + excerpt: metadata.excerpt || '', + content: markdownContent.trim(), + firstImage: metadata.image || undefined + }; +} + +export function DocsCards({ docs }: { docs: DocFile[] }) { + const [active, setActive] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const ref = useRef(null); + const id = useId(); + + useEffect(() => { + function onKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") { + setActive(null); + } + } + + if (active) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "auto"; + } + + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [active]); + + useOutsideClick(ref, () => setActive(null)); + + const filteredDocs = docs.filter((doc) => + doc.title.toLowerCase().includes(searchTerm.toLowerCase()) || + doc.excerpt.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const renderExcerpt = (content: string) => { + const firstParagraph = content.split('\n\n')[0]; + return marked(firstParagraph); + }; + + return ( + <> +
+ setSearchTerm(e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + {active && ( + + )} + + + {active && ( +
+ setActive(null)} + > + + + + {active.firstImage && ( + + {active.title} + + )} +
+ + {active.title} + + +
+ + Read full article + + + Join Discord + +
+
+
+
+ )} +
+ + + ); +} + +const CloseIcon = () => { + return ( + + + + + ); +}; \ No newline at end of file diff --git a/app/components/TableOfContents.tsx b/app/components/TableOfContents.tsx new file mode 100644 index 0000000..f817daf --- /dev/null +++ b/app/components/TableOfContents.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Link from 'next/link'; + +interface TocItem { + level: number; + title: string; + id: string; +} + +interface TableOfContentsProps { + toc: TocItem[]; +} + +export function TableOfContents({ toc }: TableOfContentsProps) { + return ( + + ); +} + +function getIndentClass(level: number): string { + return `ml-${(level - 1) * 4}`; +} \ No newline at end of file diff --git a/app/docs/DocsList.tsx b/app/docs/DocsList.tsx new file mode 100644 index 0000000..72e5fc0 --- /dev/null +++ b/app/docs/DocsList.tsx @@ -0,0 +1,42 @@ +'use client'; + +import React, { useState } from 'react'; +import Link from 'next/link'; + +interface Doc { + slug: string; + title: string; +} + +interface DocsListProps { + docs: Doc[]; +} + +export function DocsList({ docs }: DocsListProps) { + const [searchTerm, setSearchTerm] = useState(''); + + const filteredDocs = docs.filter(doc => + doc.title.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( + <> + setSearchTerm(e.target.value)} + /> + + + ); +} \ No newline at end of file diff --git a/app/docs/[slug]/TableOfContents.tsx b/app/docs/[slug]/TableOfContents.tsx new file mode 100644 index 0000000..661e552 --- /dev/null +++ b/app/docs/[slug]/TableOfContents.tsx @@ -0,0 +1,30 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; + +interface TocItem { + level: number; + title: string; +} + +interface TableOfContentsProps { + toc: TocItem[]; +} + +export function TableOfContents({ toc }: TableOfContentsProps) { + return ( + + ); +} \ No newline at end of file diff --git a/app/docs/[slug]/markdown-styles.module.css b/app/docs/[slug]/markdown-styles.module.css new file mode 100644 index 0000000..962b70f --- /dev/null +++ b/app/docs/[slug]/markdown-styles.module.css @@ -0,0 +1,91 @@ +.markdownContent { + /* Add your styles here */ + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + line-height: 1.6; + color: #fff; + } + + .markdownContent h1 { + font-size: 2.5em; + border-bottom: 1px solid #eaecef; + padding-bottom: 0.3em; + margin-top: 1em; + margin-bottom: 16px; + } + + .markdownContent h2 { + font-size: 2em; + border-bottom: 1px solid #eaecef; + padding-bottom: 0.3em; + margin-top: 24px; + margin-bottom: 16px; + } + + .markdownContent h3 { + font-size: 1.5em; + margin-top: 24px; + margin-bottom: 16px; + } + + .markdownContent p { + margin-top: 0; + margin-bottom: 16px; + } + + .markdownContent ul, .markdownContent ol { + padding-left: 2em; + margin-top: 0; + margin-bottom: 16px; + } + + .markdownContent code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27,31,35,0.05); + border-radius: 3px; + } + + .markdownContent pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #202020; + border-radius: 10px; + } + + .markdownContent blockquote { + padding: 0 1em; + color: #6a737d; + border-left: 0.25em solid #dfe2e5; + margin: 0 0 16px 0; + } + + .markdownContent a { + color: #0366d6; + text-decoration: none; + } + + .markdownContent a:hover { + text-decoration: underline; + } + + .markdownContent img { + max-width: 100%; + box-sizing: content-box; + } + + + .toc a { + text-decoration: none; + } + + .markdownContent h1, + .markdownContent h2, + .markdownContent h3, + .markdownContent h4, + .markdownContent h5, + .markdownContent h6 { + scroll-margin-top: 100px; /* Adjust based on your header height */ + } \ No newline at end of file diff --git a/app/docs/[slug]/page.tsx b/app/docs/[slug]/page.tsx new file mode 100644 index 0000000..68343d7 --- /dev/null +++ b/app/docs/[slug]/page.tsx @@ -0,0 +1,44 @@ +import fs from 'fs'; +import path from 'path'; +import matter from 'gray-matter'; +import { marked } from 'marked'; +import styles from './markdown-styles.module.css'; + +export async function generateStaticParams() { + const docsDirectory = path.join(process.cwd(), 'docs'); + const filenames = fs.readdirSync(docsDirectory); + return filenames.map((filename) => ({ + slug: filename.replace('.md', ''), + })); +} + +function processMarkdown(markdown: string): string { + // Use marked to render the markdown to HTML + const htmlContent = marked(markdown); + + console.log('HTML content preview:', htmlContent.substring(0, 500) + '...'); + + return htmlContent; +} + +async function getDocContent(slug: string) { + const filePath = path.join(process.cwd(), 'docs', `${slug}.md`); + const fileContents = fs.readFileSync(filePath, 'utf8'); + const { content } = matter(fileContents); + const htmlContent = processMarkdown(content); + return { content: htmlContent }; +} + +export default async function DocPage({ params }: { params: { slug: string } }) { + const { content } = await getDocContent(params.slug); + return ( +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/docs/page.tsx b/app/docs/page.tsx new file mode 100644 index 0000000..624ab22 --- /dev/null +++ b/app/docs/page.tsx @@ -0,0 +1,63 @@ +// File: app/docs/page.tsx +import fs from 'fs'; +import path from 'path'; +import matter from 'gray-matter'; +import { DocsCards } from '../components/DocsCards'; +import { marked } from 'marked'; + +interface DocFile { + slug: string; + title: string; + excerpt: string; + content: string; + firstImage?: string; + htmlContent: string; + } + + function parseFileContent(fileContents: string, slug: string): DocFile | null { + const { data, content } = matter(fileContents); + + if (!data.title) { + console.warn(`Document ${slug} does not have a valid title and will be skipped.`); + return null; + } + + const excerpt = data.excerpt || content.slice(0, 150) + '...'; + + return { + slug, + title: data.title, + excerpt, + content, + firstImage: data.image || undefined, + htmlContent: marked(content) + }; + } + + async function getDocs(): Promise { + const docsDirectory = path.join(process.cwd(), 'docs'); + const filenames = fs.readdirSync(docsDirectory); + + const docs = filenames + .map((filename) => { + const filePath = path.join(docsDirectory, filename); + const fileContents = fs.readFileSync(filePath, 'utf8'); + const slug = filename.replace('.md', ''); + + return parseFileContent(fileContents, slug); + }) + .filter((doc): doc is DocFile => doc !== null); + + return docs; + } + +export default async function DocsPage() { + const docs = await getDocs(); + + return ( +
+

Documentation

+ +
+ ); +} \ No newline at end of file diff --git a/app/navbar.tsx b/app/navbar.tsx index 4298aca..4e956e0 100644 --- a/app/navbar.tsx +++ b/app/navbar.tsx @@ -1,62 +1,63 @@ -"use client"; -import React, { useState } from "react"; -import { HoveredLink, Menu, MenuItem, ProductItem } from "./components/ui/navbar-menu"; -import { cn } from "@/app/components/lib/utils"; -import './styles.css'; - -export function NavbarDemo() { - return ( -
- -

- The Navbar will show on top of the page -

-
- ); -} - -function Navbar({ className }: { className?: string }) { - const [active, setActive] = useState(null); - return ( -
- - Home - Enterprise - -
- - - - - -
-
- Metal -
-
- ); -} - +"use client"; +import React, { useState } from "react"; +import { HoveredLink, Menu, MenuItem, ProductItem } from "./components/ui/navbar-menu"; +import { cn } from "@/app/components/lib/utils"; +import './styles.css'; + +export function NavbarDemo() { + return ( +
+ +

+ The Navbar will show on top of the page +

+
+ ); +} + +function Navbar({ className }: { className?: string }) { + const [active, setActive] = useState(null); + return ( +
+ + Home + Docs + Enterprise + +
+ + + + + +
+
+ Metal +
+
+ ); +} + export default Navbar \ No newline at end of file diff --git a/docs/about-horizon.md b/docs/about-horizon.md new file mode 100644 index 0000000..84d1d8c --- /dev/null +++ b/docs/about-horizon.md @@ -0,0 +1,128 @@ +--- +title: About Horizon +image: https://github.com/Far-Beyond-Dev/Horizon-Community-Edition/raw/main/branding/horizon-server-high-resolution-logo-transparent.png +excerpt: A brief introduction to Horizon +--- + +![horizon-server-high-resolution-logo-transparent](https://github.com/Far-Beyond-Dev/Horizon-Community-Edition/raw/main/branding/horizon-server-high-resolution-logo-transparent.png) +
+

+ Build + + Size + Website + GitHub License + GitHub Discussions + GitHub Sponsors + GitHub forks + GitHub Repo stars +

+
+ +# Join Discord +[Far Beyond LLC Discrord](https://discord.gg/NM4awJWGWu) + +# Table Of Contents + +- [๐Ÿš€ Introduction](#introduction) + - [Synchronized Game Server Architecture](#synchronized-game-server-architecture) + - [How it Works](#how-it-works) + - [Benefits](#benefits) + - [Implementation Details](#implementation-details) + - [Event Propagation and Multicasting](#event-propagation-and-multicasting) + - [Coordinate Management and Region Mapping](#coordinate-management-and-region-mapping) + +
+ +

๐Ÿš€ Introduction

+ +Horizon is a custom game server software designed to facilitate seamless interaction between Unreal Engine 5 (UE5) and client applications through socket.io. It provides a scalable and customizable solution for hosting multiplayer games and managing real-time communication between players and a limitless number of game servers or "Hosts". + +## Synchronized Game Server Architecture + +Horizon offers two distinct architectural models for game server synchronization: + +--- + +## 1. Peer-to-Peer Model (Community Edition) + +In the Community Edition, Horizon utilizes a peer-to-peer model for synchronizing multiple game server instances. This approach allows for efficient communication and coordination between servers without the need for a central authority. + +### How it Works + +- Each Horizon server instance operates as an equal peer in the network. +- Servers communicate directly with each other to share game state updates, player actions, and other relevant information. +- The peer-to-peer model enables horizontal scalability, allowing new server instances to be added seamlessly to the network. + +### Benefits + +- Decentralized architecture, reducing single points of failure. +- Lower operational complexity, ideal for smaller deployments or community-driven projects. +- Efficient resource utilization across all participating servers. + +--- + +## 2. Parent-Child-Master Architecture (Enterprise Edition) + +For larger-scale deployments and enterprise use cases, Horizon offers an advanced Parent-Child-Master architecture. This model provides enhanced control, scalability, and management capabilities. + +### How it Works + +- **Master Node**: Oversees the entire network, managing global state and coordination. +- **Parent Nodes**: Act as regional coordinators, managing a subset of Child nodes. +- **Child Nodes**: Handle individual game instances or regions, reporting to their Parent node. + +This hierarchical structure allows for more sophisticated load balancing, fault tolerance, and centralized management as well as limitless scalability. + +![Diagram](https://github.com/user-attachments/assets/96bdd2a1-e17a-44a2-b07b-04eacbdec4eb) +(Server PNG By FreePik) + +### Benefits + +- Highly scalable architecture suitable for massive multiplayer environments. +- Advanced load balancing and resource allocation capabilities. +- Centralized monitoring and management through the Master node. +- Enhanced fault tolerance and redundancy options. +--- +## Choosing the Right Architecture + +- The Peer-to-Peer model (Community Edition) is ideal for smaller projects, community servers, or deployments that prioritize simplicity and decentralization. +- The Parent-Child-Master architecture (Enterprise Edition) is designed for large-scale commercial games, MMOs, or any project requiring advanced management and scalability features. + +Both architectures leverage Horizon's core strengths in real-time synchronization and efficient data propagation, ensuring a consistent and responsive gaming experience regardless of the chosen model. + +--- + +## Implementation Details + +#### Configuration + +Administrators can fine-tune synchronization parameters via the `server-config.json` file, adjusting settings such as synchronization frequency and data prioritization to suit specific requirements. + +#### Monitoring + +Horizon provides built-in monitoring tools to track synchronization performance, allowing administrators to identify and address any potential bottlenecks or issues promptly. + +## Event Propagation and Multicasting + +Horizon implements a robust event propagation mechanism to facilitate communication between servers based on spatial proximity and event origin. + +#### Multicast System + +Events are multicast from the Parent node to Child nodes based on their geographical proximity and relevance to the event origin. This ensures that only necessary servers receive and process the events, optimizing network bandwidth and computational resources. + +#### Propagation Distance + +Each event carries a propagation distance parameter, allowing servers to determine whether they should propagate the event further or handle it locally based on their position relative to the event origin. + +## Coordinate Management and Region Mapping + +### Spatial Coordinates + +Horizon uses a 64-bit floating-point coordinate system to manage server positions within a simulated universe. Each server instance covers a cubic light year, and coordinates are stored relativistically to avoid overflow issues. + +### Region Mapping + +Servers are organized into a grid-based region map, where each region corresponds to a specific set of spatial coordinates. This mapping enables efficient routing of events between servers, as servers can quickly determine which neighboring servers should receive specific events based on their region coordinates. + +
\ No newline at end of file diff --git a/docs/api-experimental.md b/docs/api-experimental.md new file mode 100644 index 0000000..2bfb379 --- /dev/null +++ b/docs/api-experimental.md @@ -0,0 +1,313 @@ +# ๐Ÿงช Experimental Plugin API + +> [!WARNING] +> The Plugin API is an experimental feature of Horizon and is not recommended for use in production environments. It may undergo significant changes in future releases. + +![Experimental Feature](https://img.shields.io/badge/Status-Experimental-yellow) + +## Table of Contents + +- [๐Ÿš€ Introduction](#introduction) +- [๐Ÿ”ง Implementation](#implementation) + - [RPC System](#rpc-system) + - [Event Handling](#event-handling) +- [๐Ÿ“ˆ Usage](#usage) + - [Creating a Plugin](#creating-a-plugin) + - [Registering a Plugin](#registering-a-plugin) +- [๐Ÿ’ป Development](#development) + - [Best Practices](#best-practices) + - [Known Limitations](#known-limitations) +- [๐Ÿž Troubleshooting](#troubleshooting) + +

๐Ÿš€ Introduction

+ +The Experimental Plugin API is a powerful new feature of Horizon that allows developers to extend the functionality of the server through custom plugins. This API leverages a Remote Procedure Call (RPC) system and event handling mechanism to provide a flexible and efficient way of adding new features to your Horizon server. + +

๐Ÿ”ง Implementation

+ +### RPC System + +The Plugin API utilizes an RPC (Remote Procedure Call) system to enable communication between the core server and plugins. This system allows plugins to register functions that can be called by the server or other plugins, providing a seamless way to extend server functionality. + +#### Key Components: + +- `RpcFunction`: A type alias for plugin-defined functions that can be called remotely. +- `RpcPlugin` trait: Defines the interface for RPC-enabled plugins. +- `RpcEnabledPlugin` struct: A concrete implementation of the `RpcPlugin` trait. + +### Event Handling + +The API includes an event handling system that allows plugins to respond to various game events. This system uses a publisher-subscriber model, where plugins can register for specific event types and receive notifications when those events occur. + +#### Key Components: + +- `GameEvent` enum: Represents different types of game events. +- `BaseAPI` trait: Defines methods for handling game events and ticks. + +

๐Ÿ“ˆ Usage

+ +### Creating a Plugin + +To create a plugin using the Experimental Plugin API, follow these steps: + +1. Implement the `RpcPlugin` trait for your plugin struct. +2. Define RPC functions that your plugin will expose. +3. Implement the `BaseAPI` trait to handle game events and ticks. + +Example: + +```rust +use plugin_api::{RpcPlugin, BaseAPI, GameEvent}; + +struct MyPlugin { + // Plugin state +} + +#[async_trait] +impl RpcPlugin for MyPlugin { + // Implement required methods +} + +#[async_trait] +impl BaseAPI for MyPlugin { + async fn on_game_event(&self, event: &GameEvent) { + // Handle game events + } + + async fn on_game_tick(&self, delta_time: f64) { + // Perform periodic tasks + } +} +``` + +### Registering a Plugin + +To register your plugin with the Horizon server: + +1. Create an instance of your plugin. +2. Wrap it in an `Arc>` for thread-safe sharing. +3. Use the `PluginContext::register_rpc_plugin` method to register your plugin. + +Example: + +```rust +let my_plugin = Arc::new(RwLock::new(MyPlugin::new())); +context.register_rpc_plugin(my_plugin).await; +``` + + +

๐ŸŽฎ Examples

+ +To illustrate the power and flexibility of the Experimental Plugin API, let's create two plugins: a Calculator Plugin and a Game Plugin. These examples will demonstrate how plugins can interact with each other and how game-specific logic can be separated from the core Horizon server. + +> [!IMPORTANT] +> In Horizon, all game-specific server logic is implemented as plugins. This design choice allows for easier updates to the core Horizon code without affecting game-specific functionality. + +### Calculator Plugin + +First, let's create a simple Calculator Plugin that provides basic arithmetic operations: + +```rust +use std::sync::Arc; +use tokio::sync::RwLock; +use async_trait::async_trait; +use plugin_api::{RpcPlugin, RpcFunction, BaseAPI, GameEvent, PluginContext}; + +struct CalculatorPlugin { + id: Uuid, + name: String, + rpc_functions: HashMap, +} + +impl CalculatorPlugin { + fn new() -> Self { + let mut plugin = Self { + id: Uuid::new_v4(), + name: "CalculatorPlugin".to_string(), + rpc_functions: HashMap::new(), + }; + + plugin.register_rpc("add", Arc::new(CalculatorPlugin::add)); + plugin.register_rpc("subtract", Arc::new(CalculatorPlugin::subtract)); + plugin + } + + fn add(params: &(dyn Any + Send + Sync)) -> Box { + if let Some((a, b)) = params.downcast_ref::<(f64, f64)>() { + Box::new(a + b) + } else { + Box::new(f64::NAN) + } + } + + fn subtract(params: &(dyn Any + Send + Sync)) -> Box { + if let Some((a, b)) = params.downcast_ref::<(f64, f64)>() { + Box::new(a - b) + } else { + Box::new(f64::NAN) + } + } +} + +#[async_trait] +impl RpcPlugin for CalculatorPlugin { + // Implement required methods (get_id, get_name, register_rpc, call_rpc) +} + +#[async_trait] +impl BaseAPI for CalculatorPlugin { + async fn on_game_event(&self, _event: &GameEvent) { + // Calculator doesn't need to handle game events + } + + async fn on_game_tick(&self, _delta_time: f64) { + // Calculator doesn't need periodic updates + } +} +``` + +### Game Plugin + +Now, let's create a Game Plugin that uses the Calculator Plugin for some game logic: + +```rust +use std::sync::Arc; +use tokio::sync::RwLock; +use async_trait::async_trait; +use plugin_api::{RpcPlugin, RpcFunction, BaseAPI, GameEvent, PluginContext, Player}; + +struct GamePlugin { + id: Uuid, + name: String, + rpc_functions: HashMap, + context: Arc>, +} + +impl GamePlugin { + fn new(context: Arc>) -> Self { + let mut plugin = Self { + id: Uuid::new_v4(), + name: "GamePlugin".to_string(), + rpc_functions: HashMap::new(), + context, + }; + + plugin.register_rpc("calculate_damage", Arc::new(GamePlugin::calculate_damage)); + plugin + } + + async fn calculate_damage(params: &(dyn Any + Send + Sync), context: &PluginContext) -> Box { + if let Some((base_damage, armor)) = params.downcast_ref::<(f64, f64)>() { + // Use the Calculator Plugin to compute the final damage + if let Some(calculator_id) = context.get_rpc_plugin_id_by_name("CalculatorPlugin").await { + if let Some(result) = context.call_rpc_plugin(calculator_id, "subtract", &(*base_damage, *armor)).await { + if let Some(final_damage) = result.downcast_ref::() { + return Box::new(final_damage.max(0.0)); // Ensure damage is not negative + } + } + } + } + Box::new(0.0) // Default to no damage if calculation fails + } +} + +#[async_trait] +impl RpcPlugin for GamePlugin { + // Implement required methods (get_id, get_name, register_rpc, call_rpc) +} + +#[async_trait] +impl BaseAPI for GamePlugin { + async fn on_game_event(&self, event: &GameEvent) { + match event { + GameEvent::DamageDealt { attacker, target, amount } => { + let context = self.context.read().await; + if let Some(result) = self.call_rpc("calculate_damage", &(*amount, target.armor)).await { + if let Some(final_damage) = result.downcast_ref::() { + println!("Player {} dealt {} damage to Player {}", attacker.id, final_damage, target.id); + // Apply damage to target, update game state, etc. + } + } + }, + // Handle other game events... + _ => {} + } + } + + async fn on_game_tick(&self, delta_time: f64) { + // Implement game logic that needs to run every tick + // For example: update positions, check for collisions, etc. + } +} +``` + +### Registering and Using the Plugins + +To use these plugins in your Horizon server: + +```rust +async fn setup_plugins(context: &mut PluginContext) { + // Register the Calculator Plugin + let calculator_plugin = Arc::new(RwLock::new(CalculatorPlugin::new())); + context.register_rpc_plugin(calculator_plugin.clone()).await; + + // Register the Game Plugin + let game_plugin = Arc::new(RwLock::new(GamePlugin::new(Arc::new(RwLock::new(context.clone()))))); + context.register_rpc_plugin(game_plugin.clone()).await; + + // Register for game events + let base_api: Arc = game_plugin; + context.register_for_custom_event("damage_dealt", base_api).await; +} +``` + +### Explanation + +In this example, we've created two plugins: + +1. **Calculator Plugin**: A utility plugin that provides basic arithmetic operations. +2. **Game Plugin**: Implements game-specific logic, such as damage calculation. + +The Game Plugin uses the Calculator Plugin to perform damage calculations, demonstrating how plugins can interact with each other. This modular approach allows for: + +- **Separation of Concerns**: Game logic is kept separate from utility functions and core server code. +- **Reusability**: The Calculator Plugin could be used by multiple game plugins or even different games. +- **Easier Maintenance**: Updates to the core Horizon server won't directly affect game-specific logic. +- **Flexibility**: New game features can be added or modified without changing the core server code. + +By implementing all game-specific server logic as plugins, Horizon maintains a clear separation between its core functionality and game-specific features. This architecture allows developers to: + +- Update the core Horizon code without breaking game functionality. +- Develop and test game features independently of the core server. +- Easily switch between different game modes or rulesets by loading different plugins. +- Share common functionality (like the Calculator Plugin) across multiple projects. + +

๐Ÿ’ป Development

+ +### Best Practices + +- Keep plugin functionality modular and focused. +- Use appropriate error handling and logging within your plugins. +- Avoid blocking operations in event handlers and RPC functions. +- Test your plugins thoroughly before deployment. + +### Known Limitations + +- The API is still experimental and may change in future releases. +- Complex inter-plugin dependencies may lead to performance issues. +- There's currently no built-in versioning system for plugins. + +

๐Ÿž Troubleshooting

+ +Common issues when working with the Experimental Plugin API: + +- **Plugin Not Loading**: Ensure that your plugin is correctly registered with the `PluginContext`. +- **RPC Function Not Found**: Verify that the function name in `call_rpc` matches the registered name. +- **Type Mismatch Errors**: Check that the types used in RPC function signatures match the actual data being passed. + +For more detailed troubleshooting and support, please refer to the [Horizon Troubleshooting Guide](troubleshooting.md) or join our community Discord server. + +--- + +> [!NOTE] +> As this is an experimental feature, we encourage developers to provide feedback and report any issues they encounter while using the Plugin API. Your input is valuable in shaping the future of this feature! \ No newline at end of file diff --git a/hooks/use-outside-click.ts b/hooks/use-outside-click.ts new file mode 100644 index 0000000..efb1010 --- /dev/null +++ b/hooks/use-outside-click.ts @@ -0,0 +1,23 @@ +import React, { useEffect } from "react"; + +export const useOutsideClick = ( + ref: React.RefObject, + callback: Function +) => { + useEffect(() => { + const listener = (event: any) => { + if (!ref.current || ref.current.contains(event.target)) { + return; + } + callback(event); + }; + + document.addEventListener("mousedown", listener); + document.addEventListener("touchstart", listener); + + return () => { + document.removeEventListener("mousedown", listener); + document.removeEventListener("touchstart", listener); + }; + }, [ref, callback]); +}; diff --git a/package-lock.json b/package-lock.json index 6b50d8e..03a602e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "clsx": "^2.1.1", "cobe": "^0.6.3", "framer-motion": "^11.3.30", + "gray-matter": "^4.0.3", + "marked": "^14.1.3", "next": "14.2.10", "react": "^18", "react-dom": "^18", @@ -2786,6 +2788,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -2832,6 +2847,18 @@ "node": ">=0.10.0" } }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3278,6 +3305,43 @@ "dev": true, "license": "MIT" }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -3591,6 +3655,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3988,6 +4061,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -4081,6 +4163,18 @@ "dev": true, "license": "ISC" }, + "node_modules/marked": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.3.tgz", + "integrity": "sha512-ZibJqTULGlt9g5k4VMARAktMAjXoVnnr+Y3aCqW1oDftcV4BA3UmrBifzXoZyenHRk75csiPu9iwsTj4VNBT0g==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5086,6 +5180,19 @@ "loose-envify": "^1.1.0" } }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -5212,6 +5319,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -5441,6 +5554,15 @@ "node": ">=4" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/package.json b/package.json index c429af0..d0c67e5 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "clsx": "^2.1.1", "cobe": "^0.6.3", "framer-motion": "^11.3.30", + "gray-matter": "^4.0.3", + "marked": "^14.1.3", "next": "14.2.10", "react": "^18", "react-dom": "^18",