Openplanet Plugin Dev
Create, debug, and structure Openplanet AngelScript plugins for Trackmania/Maniaplanet. Comprehensive guide with API quirks, AngelScript language pitfalls, p...
tomekdot
@tomekdot
Install
$ openclaw skills install @tomekdot/openplanet-plugin-dev๐ฎ Openplanet Plugin Development
๐ Overview
Openplanet is a plugin/script development platform for Nadeo games (Trackmania 2020, Maniaplanet). Plugins are written in AngelScript (.as), a C++-like scripting language. This skill covers everything from project structure to deep API quirks, performance patterns, and debugging.
๐ก Hard-won lessons from building: Grid Explorer & Tracker, Event Calendar, Vehicle Detector, Apeiron Galaxy, Competition Companion.
๐๏ธ Plugin Architecture (callback-style)
Openplanet plugins can be either:
- Coroutine-style: one
void Main()that loops withyield()/sleep(ms) - Callback-style: optional
Main()+ auto-calledvoid Update(float dt)andvoid Render()
For most overlays, callback-style is simpler and lower-latency โ you don't need a Main() at all. Just define Update() and/or Render().
| Callback | When it fires |
|---|---|
void Main() | Plugin load. Yieldable coroutine. |
void Update(float dt) | Every frame. dt is delta in milliseconds. |
void Render() | Every frame, even with overlay closed. |
void RenderInterface() | Every frame, only when overlay is open. |
void RenderMenu() | For Openplanet menu items. |
void OnEnabled/OnDisabled/OnDestroyed() | Plugin lifecycle. |
void OnSettingsChanged() | After user changes any [Setting] value. |
๐ Project Layout
๐ Folder-based (development) โ PREFERRED
Openplanet4/Plugins/<plugin-name>/
โโโ info.toml # Metadata (required)
โโโ Main.as # Entry point (required)
โโโ src/ # Optional modules
โ โโโ core/
โ โโโ ui/
โ โโโ utils/
โโโ README.md
โโโ tests/ # Optional Python test scripts
All .as files in the folder are compiled together as a single module โ no manual imports needed.
๐๏ธ Filesystem layout
Openplanet4/
โโโ docs/ # API documentation
โโโ Plugins/ # Runtime plugins (what Openplanet loads)
โโโ Plugins-Developer/ # Source-of-truth development tree
โโโ PluginStorage/ # Per-plugin persistent data (IO::FromStorageFolder)
โโโ Openplanet.log # Debug log โ check for compilation errors
When iterating, edit Plugins-Developer/, then copy to Plugins/ for live test.
๐ฆ Packaged (.op) โ distribution
.op files are ZIP archives. Do NOT edit them directly โ extract, develop as folder, re-zip for release.
Build:
cd path/to/MyPlugin
7z a MyPlugin.zip info.toml Main.as src/
ren MyPlugin.zip MyPlugin.op # rename to .op
โ๏ธ info.toml
[meta]
name = "My Plugin"
author = "yourname"
version = "1.0.0"
category = "Tools"
[script]
timeout = 0
dependencies = [ "VehicleState", "Camera" ] # namespaces your code uses
defines = [] # Preprocessor defines for dev
๐ Common namespaces and which plugin owns them
| Namespace/function | Plugin to add to dependencies |
|---|---|
VehicleState::ViewingPlayerState() | VehicleState |
Camera::ToScreenSpace(vec3) -> vec2 | Camera |
Camera::IsBehind(vec3) -> bool | Camera |
NadeoServices::AddAudience(...) | NadeoServices |
Dashboard::ViewingPlayerState() | Dashboard (optional) |
โ ๏ธ Symptom of missing dependency: ERR : No matching symbol 'X::Y' at compile time.
๐๏ธ Settings
[Setting name="Display name" description="Tooltip"]
bool S_MySetting = true;
[Setting name="Slider value" min=0 max=100]
int S_Slider = 50;
[Setting hidden]
string S_InternalData = "";
๐จ CRITICAL โ API Quirks & Pitfalls
๐ค AngelScript Language Quirks
These bite everyone. AngelScript โ C++, โ C#, โ Java.
๐ข Integer literal suffixes
u, l, ul, ull suffixes are not supported.
// โ ERR: Unexpected token '<identifier>'
const uint64 MASK = 0x3FFFFFFu;
const int BIG = 1ul;
// โ
OK: bare literal; compiler picks the type
const uint64 MASK = 0x3FFFFFF;
const int BIG = 1;
const uint64 KEY = uint64(0x123); // explicit cast
๐ const on value-type arrays
const works for primitives, but NOT for fixed-size arrays of value types like int2[] or vec3[].
// โ ERR: Expected '('
const int2 EDGES[12] = { int2(0,1), ... };
const vec3 CORNERS[8] = { ... };
// โ
OK: dynamic, no const
int2[] g_Edges = { int2(0,1), ... };
Also: const on array<T> is unreliable โ keep globals un-const and use a g_ prefix instead.
๐ Fixed-size local arrays
Local fixed-size arrays are not supported:
void Foo() {
// โ ERR: Expected ';' Instead found identifier 'pts'
vec2 pts[8];
float depth[8];
}
// โ
WORKAROUND: individual variables
void Foo() {
vec2 p0, p1, p2, p3, p4, p5, p6, p7;
// compiler will register-allocate them
}
Or use dynamic arrays:
vec2[] GetCorners() {
vec2[] cs = { vec2(0,0), vec2(1,0), ... };
return cs;
}
๐๏ธ dictionary key types
Most surprising limitation: dictionary only accepts const string&in keys.
dictionary d;
// โ ERR: No matching signatures to 'dictionary::Exists(uint64)'
d[uint64(123)] = true;
d[int(456)] = true;
// โ
OK: build a string key
d["123"] = true;
d[gx + "," + gy + "," + gz] = true;
If you need a fast non-string key, roll your own hash table (parallel arrays + linear scan, or small open-addressed probe table). For most plugins, string keys are fine.
๐ uint vs int in comparisons
array<T>.Length returns uint. Comparing a signed int loop counter produces a warning treated as error in strict mode:
// โ ๏ธ WARN: Signed/Unsigned mismatch
for (int e = 0; e < arr.Length; e++) { ... }
// โ
CLEAN:
for (uint e = 0; e < arr.Length; e++) { ... }
๐ค out parameter naming
Match the parameter name exactly:
// โ ERR: No matching symbol 'outDepth'
bool Project(vec3 &in p, vec2 &out screen, float &out depth) {
outDepth = 0.0f;
}
// โ
OK:
bool Project(vec3 &in p, vec2 &out screen, float &out depth) {
depth = 0.0f;
}
๐จ int2, vec2, vec3, vec4 constructors
int2 a = int2(1, 2);
vec3 v = vec3(1.0f, 2.0f, 3.0f);
vec4 red = vec4(1.0f, 0.0f, 0.0f, 0.5f);
They are value types โ no @ needed for storage.
๐ &inout on primitive types is NOT allowed
// โ ERR:
void ConfigRow(const string &in label, bool &inout value) { }
// โ
OK:
void ConfigRow(const string &in label, bool value) { }
๐ Array initialization โ inline int t[] = {...} fails inside functions
// โ ERR inside functions:
// int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
// โ
OK: use array<T> with InsertLast
array<int64> items;
items.InsertLast(123);
// โ
OK: pre-allocate at global scope
int[] g_Array;
void Main() { g_Array.Resize(16); }
// โ
OK: inline array init works at global scope
int[] monthDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
๐ string::IndexOf โ takes exactly ONE parameter
// โ ERR:
int idx = text.IndexOf("[", startPos);
// โ
OK:
int idx = text.IndexOf("[");
// For offset: use SubStr first
int idx = text.SubStr(startPos).IndexOf("[");
๐ Text::Format takes exactly ONE value argument
// โ ERR:
Text::Format("%.6f (%.2f%%)", sidereal, sidereal * 100.0);
// โ
OK:
Text::Format("%.6f", sidereal) + " (" + Text::Format("%.2f%%", sidereal * 100.0) + ")";
โฐ Time API Quirks
๐ ฐ๏ธ Time::Info uses PascalCase, NOT lowercase
'year' is not a member of 'Time::Info'
// โ ERR:
info.year, info.month, info.day, info.hour, info.minute, info.second
// โ
OK:
info.Year, info.Month, info.Day, info.Hour, info.Minute, info.Second
๐ Weekday is NOT a member of Time::Info
info.Weekday will fail. Use Zeller's formula (0=Sun..6=Sat):
int GetDayOfWeek(int y, int m, int d) {
if (m < 3) { m += 12; y -= 1; }
int K = y % 100;
int J = y / 100;
int h = (d + (13 * (m + 1)) / 5 + K + K / 4 + J / 4 + 5 * J) % 7;
return (h + 6) % 7; // 0=Sun
}
๐ Time functions
int64 now = Time::Stamp; // Epoch seconds
uint64 gameTime = Time::Now; // ms since game start
string formatted = Time::FormatString("%H:%M", now); // strftime format
Time::Info info = Time::Parse(now); // Local time
๐ฅ๏ธ UI API Quirks
๐ค No UI::Font enum โ use PushFontSize
// โ
OK:
UI::PushFontSize(22.0);
UI::Text("Big text");
UI::PopFontSize();
// โ ERR (does not exist):
UI::PushFont(UI::Font::OpenSansBold);
๐จ No UI::TextColored โ use PushStyleColor
// โ
OK:
UI::PushStyleColor(UI::Col::Text, vec4(0.3f, 1.0f, 0.5f, 1.0f));
UI::Text("Green text");
UI::PopStyleColor();
// โ ERR:
UI::TextColored(color, "text");
๐ Window position uses int coords
// โ
OK (cast floats to int):
UI::SetNextWindowPos(int(posX), int(posY), UI::Cond::Appearing);
๐ช UI::Begin takes a bool reference
bool S_WindowOpen = false;
if (!UI::Begin("My Window", S_WindowOpen, UI::WindowFlags::NoSavedSettings)) {
UI::End();
return;
}
โจ๏ธ UI::InputText โ return type vs. bool&out
// โ ERR โ InputText ALWAYS returns string, can't use in if():
if (UI::InputText("##Input", g_Text, changed, flags)) { }
// โ
OK:
bool changed = false;
UI::InputText("##Input", g_Text, changed, flags);
if (changed) { /* Enter was pressed */ }
๐๏ธ NanoVG Essentials
The drawing API is per-frame immediate-mode. State persists until changed.
nvg::BeginPath();
nvg::MoveTo(p1);
nvg::LineTo(p2);
nvg::Stroke(); // โ actually draws the path
nvg::FontSize(13.0f);
nvg::FillColor(vec4(1.0f, 1.0f, 1.0f, 0.9f));
nvg::TextAlign(nvg::Align::Middle | nvg::Align::Center);
nvg::Text(p, "label");
nvg::BeginPath()is required forLineTo/Rect/Circle/etc.nvg::Text()does NOT needBeginPath.- For minimum state changes, batch draws that share a state.
๐ท Camera API
๐ฏ Camera::ToScreenSpace returns vec2, not vec3
vec2 screenPos = Camera::ToScreenSpace(worldPos); // 2D only
// NO z/depth component!
For behind-camera test:
if (Camera::IsBehind(worldPos)) {
// skip this point
}
โ ๏ธ Don't assume a vec3 overload exists. The vec2 return is the only signature.
๐ VehicleState::ViewingPlayerState() returns CSmPlayer
auto state = VehicleState::ViewingPlayerState();
if (state is null) return; // null when not in a vehicle / not in a map
vec3 pos = state.Position; // world-space player position
Use is null โ AngelScript's null-comparison operator for handles.
๐งฑ Trackmania Block Dimensions
Standard blocks are 32 ร 32 ร 8 (X ร Z ร Y) in world units.
const float BLOCK_XZ = 32.0f;
const float BLOCK_Y = 8.0f;
int gx = int(Math::Floor(worldPos.x / BLOCK_XZ));
int gy = int(Math::Floor(worldPos.y / BLOCK_Y));
int gz = int(Math::Floor(worldPos.z / BLOCK_XZ));
โก Performance Patterns
๐จ Minimize NVG state changes
Batch draws by state โ the single biggest FPS win:
// โ BAD: stroke state changes per block
for (each block) {
nvg::StrokeColor(...);
nvg::StrokeWidth(...);
DrawEdgesOfBlock(...);
}
// โ
GOOD: sort by state, then batch
nvg::StrokeColor(green); // once per "visited" pass
for (each visited block) DrawEdgesOfBlock(...);
nvg::StrokeColor(red); // once per "unvisited" pass
for (each unvisited block) DrawEdgesOfBlock(...);
State-change guard:
bool g_LastDrewVisited = false;
void ApplyStroke(bool visited) {
if (visited == g_LastDrewVisited) return;
g_LastDrewVisited = visited;
nvg::StrokeColor(visited ? green : red);
nvg::StrokeWidth(visited ? 3.0f : 2.0f);
}
๐ง Don't recompute in Render()
Anything that can be computed in Update() and cached as a global is one less per-frame allocation.
๐๏ธ Spatial culling
For 3D grids:
- Per-block AABB cull: project all 8 corners; if 0 in front of camera, skip block
- Edge-level cull: only draw edges whose two endpoints were both visible
๐ค Avoid string concatenation in hot loops
"a" + "b" + "c" allocates 3 new strings. For cache keys hit thousands of times per frame, cache the per-frame key. For 4โ8 char keys, the cost is acceptable โ the lookup hash dominates.
๐ Diagnostic UI
The UI::* namespace is immediate-mode (like Dear ImGui):
UI::SetNextWindowSize(width, height, UI::Cond::FirstUseEver);
if (UI::Begin("My Diagnostics")) {
UI::Text("Static label: " + value);
if (UI::Button("Reset")) {
// handle click
}
UI::Separator();
}
UI::End();
Window titles with icons: UI::Begin(Icons::Eye + " Diagnostics")
The Icons:: namespace has 600+ Unicode glyphs (Icons::Eye, Icons::Cog, Icons::Trash, Icons::Clock, Icons::Car, Icons::Info, Icons::Calendar, Icons::Star, Icons::QuestionCircle, etc.).
๐๏ธ Config & Debug Window Pattern
void RenderDebugWindow() {
UI::SetNextWindowSize(580, 520, UI::Cond::FirstUseEver);
if (!UI::Begin("Config", g_ShowDebugWindow)) { UI::End(); return; }
UI::BeginTabBar("Tabs");
if (UI::BeginTabItem("Config")) { RenderConfigTab(); UI::EndTabItem(); }
if (UI::BeginTabItem("Status")) { RenderStatusTab(); UI::EndTabItem(); }
if (UI::BeginTabItem("History")) { RenderHistoryTab(); UI::EndTabItem(); }
UI::EndTabBar(); UI::End();
}
| Tab | Content |
|---|---|
| โ๏ธ Config | All [Setting] toggles in a table with ON/OFF |
| ๐ Status | Live values, cache queues, computed data |
| ๐ History | Data tables, map visits, recorded events |
| ๐ Reference | Static reference data (nodes, calibration tables) |
๐ Lunar Calendar & Date Conversion
๐ Gregorian โ Day of Year
// Day of year from Gregorian (1-based)
int GetDayOfYear(int year, int month, int day) {
int[] daysBefore = {0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
int doy = daysBefore[month] + day;
if (month > 2 && IsLeapYear(year)) doy++;
return doy;
}
// Day of year back to Gregorian
void DayOfYearToGregorian(int year, int dayOfYear, int &out month, int &out day) {
int[] mdays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (IsLeapYear(year)) mdays[1] = 29;
month = 1;
int remaining = dayOfYear;
for (int i = 0; i < 12; i++) {
if (remaining <= mdays[i]) { month = i + 1; day = remaining; return; }
remaining -= mdays[i];
}
month = 12; day = 31;
}
// Unix timestamp from Gregorian date (midnight UTC)
uint64 UnixFromGregorian(int year, int month, int day) {
Time::Info info;
info.Year = year;
info.Month = month;
info.Day = day;
info.Hour = 0;
info.Minute = 0;
info.Second = 0;
return Time::Unix(info);
}
// Gregorian from Unix timestamp
void GetGregorianFromUnix(uint64 unixTime, int &out year, int &out month, int &out day) {
Time::Info info = Time::Parse(unixTime);
year = info.Year;
month = info.Month;
day = info.Day;
}
bool IsLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
๐ Moon phase (synodic ~29.53 days)
// Returns 0.0 (new moon) to 1.0 (next new moon)
float GetMoonPhase(uint64 unixTime) {
// Known new moon: 2000-01-06 18:14 UTC = 947182440
const float SYNODIC = 29.53058867;
double daysSince = double(unixTime - 947182440) / 86400.0;
return float(fmod(daysSince, SYNODIC) / SYNODIC);
}
// Moon texture for current phase (requires Moon plugin textures)
auto@ GetMoonTexture(uint64 unixTime) {
float phase = GetMoonPhase(unixTime);
// 0.0=new, 0.25=first quarter, 0.5=full, 0.75=last quarter
// Map to your texture array index
int idx = int(phase * 8.0) % 8;
return Moon::GetTexture(idx);
}
๐ Calendar Day Indicators
๐ข Event dots โ mark days that have events
bool[] g_DaysWithEvents;
void Main() {
g_DaysWithEvents.Resize(31);
while (true) {
RebuildEventDays();
yield();
}
}
void RebuildEventDays() {
for (int i = 0; i < 31; i++) g_DaysWithEvents[i] = false;
Time::Info now = Time::Parse(Time::Stamp);
int curDOW = GetDayOfWeek(now.Year, now.Month, now.Day);
// Convert Sun=0 to Mon=1..Sun=7 convention if needed
curDOW = (curDOW == 0) ? 7 : curDOW;
int curDay = now.Day;
for (int i = 0; i < g_EventCount; i++) {
int diff = g_WeekDay[i] - curDOW;
int eventDay = curDay + diff;
if (eventDay >= 1 && eventDay <= 31) g_DaysWithEvents[eventDay - 1] = true;
}
}
In your calendar draw loop:
// Highlight days with event dots
if (g_DaysWithEvents[day - 1])
UI::PushStyleColor(UI::Col::Button, vec4(0.4f, 0.8f, 0.4f, 0.25f));
UI::Button(dayStr);
if (g_DaysWithEvents[day - 1])
UI::PopStyleColor();
๐ Inline data in cells โ show computed value without hover
// In each calendar cell, after drawing the day number button:
UI::TextDisabled(Text::Format("%.0f%%", progress * 100.0)); // e.g. "23%" = 23%
// Moon phase icon alongside:
auto@ tex = GetMoonTexture(Time::Stamp);
if (tex !is null) {
UI::SameLine();
UI::Image(tex, vec2(14, 14));
}
โฑ๏ธ Upcoming Events Countdown
array<int64> eTs; array<string> eLabel;
void BuildUpcomingList() {
eTs.Resize(0);
eLabel.Resize(0);
int64 now = Time::Stamp;
for (int i = 0; i < g_Count; i++) {
int64 ets = GetNextEventTs(g_WeekDay[i], g_Hour[i], g_Min[i]);
if (ets > now) {
eTs.InsertLast(ets);
eLabel.InsertLast(g_Label[i]);
}
}
// Bubble sort by timestamp (ascending)
for (uint i = 0; i < eTs.Length - 1; i++) {
for (uint j = 0; j < eTs.Length - 1 - i; j++) {
if (eTs[j] > eTs[j + 1]) {
int64 tmpT = eTs[j]; eTs[j] = eTs[j + 1]; eTs[j + 1] = tmpT;
string tmpL = eLabel[j]; eLabel[j] = eLabel[j + 1]; eLabel[j + 1] = tmpL;
}
}
}
}
// Display first 5 in UI:
void RenderUpcoming() {
BuildUpcomingList();
uint count = eTs.Length;
if (count > 5) count = 5;
for (uint i = 0; i < count; i++) {
int64 ago = eTs[i] - Time::Stamp;
string countdown = FormatCountdown(ago);
UI::Text(eLabel[i] + " โ " + countdown);
}
}
string FormatCountdown(int64 seconds) {
int h = int(seconds / 3600);
int m = int((seconds % 3600) / 60);
return Text::Format("%dh %dm", h, m);
}
๐ Recurring Events Pattern
const int MAX_EVENTS = 16;
int g_Count = 0;
int[] g_WeekDay; int[] g_Hour; int[] g_Min; string[] g_Label;
void AddEvent(int d, int h, int m, const string &in l) {
if (g_Count >= MAX_EVENTS) return;
g_WeekDay[g_Count] = d; g_Hour[g_Count] = h;
g_Min[g_Count] = m; g_Label[g_Count] = l; g_Count++;
}
โฑ๏ธ Upcoming events with countdown:
array<int64> eTs; array<string> eLabel;
for (int i = 0; i < g_EventCount; i++) {
int64 ets = GetNextEventTs(g_WeekDay[i], g_Hour[i], g_Min[i]);
if (ets > now) { eTs.InsertLast(ets); eLabel.InsertLast(g_Label[i]); }
}
// Bubble sort, display first 5
๐ Preprocessor Directives
#if TMNEXT
// Trackmania (2020) only
#elif MP4
// Maniaplanet 4 only
#endif
๐ Common Build/Compile Errors
| Error message | Cause | Fix |
|---|---|---|
ERR : No matching symbol 'X::Y' at function call | Missing dependencies in info.toml | Add the owning plugin to [script].dependencies |
ERR : Unexpected token '<identifier>' after numeric literal | Integer suffix (u, l, etc.) not supported | Drop suffix or use uint64(x) cast |
ERR : Expected '(' Instead found '[' on const TYPE name[N] | const on fixed-size value-type array | Use TYPE[] name = { ... }; (dynamic, no const) |
ERR : Expected ';' Instead found identifier 'pts' on vec2 pts[8]; | Local fixed-size array not supported | Use individual variables |
ERR : No matching signatures to 'dictionary::Exists(uint64)' | Dictionary only takes string keys | Convert to string key |
WARN : Signed/Unsigned mismatch in for loop | int i vs uint Length | Use uint i |
ERR : Can't implicitly convert from 'vec2' to 'vec3' | Camera::ToScreenSpace returns vec2 | Use vec2; test Camera::IsBehind for cull |
'year' is not a member of 'Time::Info' | Wrong case on member | Use PascalCase: info.Year, info.Month, info.Day, etc. |
'Weekday' is not a member of 'Time::Info' | Weekday doesn't exist on Time::Info | Use Zeller's formula (see Time API section) |
No matching symbol 'UI::Font::...' | Font enum doesn't exist | Use PushFontSize/PopFontSize |
No matching symbol 'UI::TextColored' | Function doesn't exist | Use PushStyleColor(UI::Col::Text, ...) |
Float value truncated in implicit conversion | float where int expected | Cast: int(value) |
ERR : No matching symbol 'outDepth' | out param name mismatch | Match parameter name exactly |
ERR : Can't implicitly convert from 'string' to 'bool' | UI::InputText return in if() | Call separately, check changed bool after |
ERR on '&inout' with primitive | &inout not allowed on primitives | Pass by value for reads |
ERR on 'IndexOf' with 2 args | string::IndexOf takes 1 param | Use SubStr first for offset |
ERR on 'Text::Format' with 2 values | Text::Format takes 1 value arg | Chain multiple Text::Format calls |
๐ง Debugging Tips
- ๐
F3 โ Logโprint("hello")lands here - ๐ Reload scripts after every save via
F3 โ Developer โ Reload Scriptsโ no restart needed - ๐ F3 โ Developer โ Plugin Manager โ shows load order and compile errors
- ๐ Nod Explorer (
F3 โ Developer โ Nod Explorer) โ browse liveCGameCtnApptree - ๐ Openplanet.log (
%USERPROFILE%\OpenplanetNext\Openplanet.log) โ stack traces for runtime crashes
๐ Quick-Start Template
# info.toml
[meta]
name = "My Plugin"
author = "Me"
category = "Tools"
version = "1.0.0"
[script]
timeout = 0
dependencies = [ "VehicleState" ]
// main.as
[Setting name="Enabled" category="General"]
bool S_Enabled = true;
void Update(float dt) {
if (!S_Enabled) return;
auto state = VehicleState::ViewingPlayerState();
if (state is null) return;
// ... per-frame work ...
}
void Render() {
if (!S_Enabled) return;
if (UI::Begin(Icons::Cog + " My Plugin")) {
UI::Text("Hello world");
}
UI::End();
}
๐งน Cleanup When Removing a Feature
Check EVERY .as file:
grep -rn "DeletedName" Plugins/<name>/
๐ Reference
Full API reference: ๐ https://openplanet.dev/docs
Hermes skills repo reference files: ๐ https://github.com/tomekdot/hermes-skills/tree/main/skills/software-development/openplanet-plugin-dev/references
Last updated: 2026-06-12. Covers AngelScript build as of OpenplanetNext 2026.
Top skills in this category
google-slides
@byungkyuGoogle Slides API integration with managed OAuth. Create presentations, add slides, insert content, and manage slide formatting. Use this skill when users want to interact with Google Slides. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway). Calls run through the `maton` CLI with OAuth login; default to read and list calls, and confirm every write or new connection with the user.
google-workspace-admin
@byungkyuGoogle Workspace Admin SDK integration with managed OAuth. This is a write-capable administrative integration for users, groups, organizational units, roles, and domain settings. Only connect with a least-privileged Google admin account, restrict OAuth scopes to the specific resources needed, and revoke the connection after use. All write operations require explicit user approval showing the exact HTTP method, endpoint path, and target resource identifier before execution. Use this skill only when users need Google Workspace administration. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway). Calls run through the `maton` CLI with OAuth login; default to read and list calls, and confirm every write or new connection with the user.
Gifgrep
@steipeteSearch GIF providers with CLI/TUI, download results, and extract stills/sheets.
Monday.com
@byungkyuMonday.com API integration with managed OAuth. Manage boards, items, columns, groups, and workspaces using GraphQL. Use this skill when users want to create, update, or query Monday.com boards and items, manage tasks, or automate workflows. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway). Requires network access and valid Maton API key. Calls run through the `maton` CLI with OAuth login; default to read and list calls, and confirm every write or new connection with the user.
Gaode Map JSAPI - ้ซๅพทๅฎๆน JavaScript SDK Skill
@lbs-amap้ซๅพทๅฐๅพ JSAPI v2.0 (WebGL) ๅผๅๆ่ฝใๆถต็ๅฐๅพ็ๅฝๅจๆ็ฎก็ใๅผบๅถๅฎๅ จ้ ็ฝฎใ3D ่งๅพๆงๅถใ่ฆ็็ฉ็ปๅถๅ LBS ๆๅก้ๆใ