Skip to content

newcore-network/opencore

Repository files navigation

CI npm license typescript cli website

OpenCore Framework - Stable v1

OpenCore is a TypeScript multiplayer runtime framework targeting CitizenFX runtimes (Cfx/RageMP) via adapters.

It is not a gamemode or RP framework. It provides:

  • A stable execution model (server and client)
  • Dependency Injection and metadata-driven wiring
  • An event/command system
  • Security primitives (validation, access control, rate limiting)

License: MPL-2.0

Discord Community | Docs | OpenCore CLI

Installation

pnpm add @open-core/framework reflect-metadata tsyringe zod uuid

This framework uses TypeScript decorators. Ensure your project has decorators enabled.

Imports and entry points

The package exposes subpath entry points:

  • @open-core/framework (root)
  • @open-core/framework/server
  • @open-core/framework/client

Architecture

OpenCore follows a Ports & Adapters (Hexagonal) architecture.

  • Kernel (src/kernel): engine-agnostic infrastructure (DI, logger, metadata scanning)
  • Runtime (src/runtime): multiplayer execution model (controllers, processors, security, lifecycle)
  • Adapters (src/adapters): platform integration (Cfx, Node testing)

The runtime never auto-detects the platform. Adapters are selected explicitly at bootstrap time.

Cfx game profiles

OpenCore treats CitizenFX (cfx) as the platform and supports game profiles (gta5 and rdr3).

  • Shared runtime APIs (events, exports, transport, DI) are registered through the Cfx adapter.
  • Game-specific behavior is controlled through platform capabilities/config (gameProfile, defaultSpawnModel, etc.).
  • Optional RedM-specific enhancements can be layered as external libraries without changing core runtime contracts.

Operating modes

Each instance runs in exactly one mode configured via Server.init():

  • CORE: authoritative runtime. Typically provides identity/auth/players via exports.
  • RESOURCE: a normal Cfx resource using CORE as provider for some features.
  • STANDALONE: a self-contained runtime (useful for tooling, simulations, or small servers).

Server bootstrap

Initialize the server runtime:

import { Server } from '@open-core/framework/server'

await Server.init({
  mode: 'CORE'
})

Some features require providers (depending on your mode and configuration). Configure them before calling init():

import { Server } from '@open-core/framework/server'

Server.setPrincipalProvider(MyPrincipalProvider)
Server.setSecurityHandler(MySecurityHandler)
Server.setPersistenceProvider(MyPlayerPersistence)
Server.setNetEventSecurityObserver(MyNetEventSecurityObserver)

Controllers and decorators

OpenCore uses a decorator + processor pattern.

Decorators store metadata with Reflect.defineMetadata(). During bootstrap, the MetadataScanner reads metadata and processors register handlers.

Commands

import { Controller, Command, Guard, Throttle, Player } from '@open-core/framework/server'
import { z } from 'zod'

const TransferSchema = z.tuple([z.coerce.number().int().positive(), z.coerce.number().min(1)])

@Controller()
export class BankController {
  @Command({
    command: 'transfer',
    usage: '/transfer <id> <amount>',
    schema: TransferSchema,
  })
  @Guard({ rank: 1 })
  @Throttle(1, 2000)
  async transfer(player: Player, args: z.infer<typeof TransferSchema>) {
    const [targetId, amount] = args
    player.emit('chat:message', `transfer -> ${targetId} (${amount})`)
  }
}

Network events

@OnNet() handlers always receive Player as the first parameter.

import { Controller, OnNet, Player } from '@open-core/framework/server'
import { z } from 'zod'

const PayloadSchema = z.object({ action: z.string(), amount: z.number().int().positive() })

@Controller()
export class ExampleNetController {
  @OnNet('bank:action', { schema: PayloadSchema })
  async onBankAction(player: Player, payload: z.infer<typeof PayloadSchema>) {
    player.emit('chat:message', `action=${payload.action} amount=${payload.amount}`)
  }
}

Security decorators

  • @Guard({ rank }) or @Guard({ permission })
  • @Throttle(limit, windowMs)
  • @RequiresState({ missing: [...] })

Library events

Use library wrappers to emit domain events and @OnLibraryEvent() to observe them.

@OnLibraryEvent() listens to events emitted through library.emit(...) only. It does not listen to emitExternal, emitNetExternal, or emitServer.

import { Server } from '@open-core/framework/server'

const characters = Server.createServerLibrary('characters')

@Controller()
export class CharacterListeners {
  @OnLibraryEvent('characters', 'session:created')
  onSessionCreated(payload: { sessionId: string; playerId: number }) {
    // optional listener for library domain events
  }
}

characters.emit('session:created', { sessionId: 's-1', playerId: 10 })

Client usage follows the same pattern with Client.createClientLibrary(...) and @Client.OnLibraryEvent(...).

Plugins

Plugin contracts are exposed by runtime entrypoint, not by root:

  • Server plugins: @open-core/framework/server
  • Client plugins: @open-core/framework/client
import { Server, type OpenCorePlugin } from '@open-core/framework/server'
import { Client, type OpenCoreClientPlugin } from '@open-core/framework/client'

const serverPlugin: OpenCorePlugin = {
  name: 'server-example',
  install(ctx) {
    ctx.server.registerApiExtension('ExampleServerDecorator', () => {})
  },
}

const clientPlugin: OpenCoreClientPlugin = {
  name: 'client-example',
  install(ctx) {
    ctx.client.registerApiExtension('ExampleClientDecorator', () => {})
  },
}

await Server.init({ mode: 'CORE', plugins: [serverPlugin] })
await Client.init({ mode: 'CORE', plugins: [clientPlugin] })

Module augmentation for plugin APIs:

declare module '@open-core/framework/server' {
  interface ServerPluginApi {
    ExampleServerDecorator: () => void
  }
}

declare module '@open-core/framework/client' {
  interface ClientPluginApi {
    ExampleClientDecorator: () => void
  }
}

Testing

Tests run with Vitest.

pnpm test
pnpm test:unit
pnpm test:integration
pnpm test:coverage

Note: pnpm test does not run benchmarks.

Benchmarks

Benchmarks are split by value, so the default run focuses on framework features that matter for real servers.

pnpm bench
pnpm bench:value
pnpm bench:gold
pnpm bench:startup
pnpm bench:diagnostic
pnpm bench:soak
pnpm bench:load
pnpm bench:all
  • bench / bench:value: value-focused suite. Commands, net events, RPC, lifecycle, ticks, binary path, bootstrap.
  • bench:gold: hot-path load scenarios only.
  • bench:startup: startup and registration cost.
  • bench:diagnostic: internal and low-level synthetic benchmarks.
  • bench:soak: long-running stress scenario.

Snapshot (latest local run)

Use benchmark/reports/ as the source of truth. Results vary by machine and should be compared relatively, not treated as product guarantees.

  • Primary benchmark targets:
    • full command execution
    • full net event handling
    • RPC processing
    • player lifecycle churn
    • tick budget impact
    • bootstrap cost
    • binary transport cost

Full reports and methodology are available in benchmark/README.md.

Reports

Benchmark reports are generated under benchmark/reports/.

  • pnpm bench:all generates aggregated reports (text/json/html)
  • Load metrics used by load benchmarks are persisted in benchmark/reports/.load-metrics.json

For details about the benchmark system, see benchmark/README.md.

Development scripts

pnpm build
pnpm watch
pnpm lint
pnpm lint:fix
pnpm format

License

MPL-2.0. See LICENSE.

About

Secure, event-driven TypeScript Framework & Runtime engine (FiveM and RageMP)

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors