feat(rbxl): XML-������ ������ .rbxl + Day/Night + Tool/Mouse/Backpack flow #38
100
RUBLOX_LUA_API_CHANGELOG.md
Normal file
100
RUBLOX_LUA_API_CHANGELOG.md
Normal file
@ -0,0 +1,100 @@
|
||||
# Lua API — журнал изменений
|
||||
|
||||
Файл фиксирует **что было добавлено в Lua-runtime** при работе с реальными
|
||||
Roblox-играми. Цель — потом продублировать тот же API для **JS-движка**
|
||||
(на будущее, сейчас работаем только с Lua).
|
||||
|
||||
Формат: дата + что и почему + куда добавлено + надо ли портировать в JS.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-08 — Итерация 1: RayGun (проект 2792, 9 скриптов)
|
||||
|
||||
**Контекст:** Roblox-Tool пушка-стрелялка, использует Tool-API, Lighting,
|
||||
Mouse, Welds, BodyForce, BrickColor, IntValue для leaderboard.
|
||||
|
||||
### Добавлено в `RobloxShim.js`
|
||||
|
||||
**Глобалы:**
|
||||
- `BrickColor.new("Bright red")` + ~25 цветов (White, Black, Bright red/blue/green,
|
||||
Pink, Brown, Reddish brown, Cyan, Magenta и др.). Возвращает `{Color, Name, R, G, B}`.
|
||||
- `Ray.new(origin, direction)` — для raycast (заглушка структуры).
|
||||
- `Region3.new(min, max)` — куб (заглушка).
|
||||
- `TweenInfo.new(time, easingStyle, easingDirection, repeatCount, reverses, delayTime)`
|
||||
- `NumberSequence`, `ColorSequence`, `NumberRange`, `Rect` — конструкторы-стабы.
|
||||
|
||||
**Enum расширения:** InfoType, SortOrder, FillDirection, Font,
|
||||
TextXAlignment/TextYAlignment, ScaleType, AspectType, PartType, SurfaceType,
|
||||
ContextActionResult, UserInputState, BorderMode, FormFactor.
|
||||
|
||||
**`game` методы:**
|
||||
- `game:service(name)` (lowercase alias на GetService) — старый Roblox API.
|
||||
- `game.GetServiceFromName` = alias.
|
||||
- `game.JobId/PlaceId/GameId/CreatorId/CreatorType` — stub fields.
|
||||
|
||||
**Lighting:**
|
||||
- `Brightness`, `ClockTime`, `TimeOfDay`, `OutdoorAmbient`, `FogStart/End/Color`.
|
||||
- `GetMinutesAfterMidnight()`, `SetMinutesAfterMidnight(m)`.
|
||||
- `GetMoonDirection()`, `GetSunDirection()`.
|
||||
|
||||
**Players:**
|
||||
- `GetPlayers()`, `GetPlayerFromCharacter(char)`, `playerFromCharacter` alias.
|
||||
- `PlayerAdded`, `PlayerRemoving`, `ChildAdded` signals.
|
||||
|
||||
**Instance.new новые типы:**
|
||||
- `Tool` / `HopperBin` — Equipped/Unequipped/Activated/Deactivated signals,
|
||||
GripForward/Right/Up/Pos, CanBeDropped, RequiresHandle, ToolTip.
|
||||
- `IntValue` / `NumberValue` / `BoolValue` / `StringValue` / `ObjectValue` /
|
||||
`CFrameValue` / `Vector3Value` / `Color3Value` / `BrickColorValue` / `RayValue`
|
||||
— `.Value` + `.Changed` сигнал.
|
||||
- `BodyForce` / `BodyVelocity` / `BodyPosition` / `BodyGyro` / `BodyAngularVelocity`
|
||||
/ `BodyThrust` — `.force`, `.Velocity`, `.MaxForce`, `.P/.D`.
|
||||
- `Weld` / `WeldConstraint` / `Motor6D` / `Snap` / `HingeConstraint` /
|
||||
`BallSocketConstraint` / `RopeConstraint` / `SpringConstraint` — Part0/Part1/C0/C1/Enabled.
|
||||
- `Sparkles` / `ParticleEmitter` / `Smoke` / `Fire` / `Trail` / `Beam` /
|
||||
`PointLight` / `SurfaceLight` / `SpotLight` — Enabled/Color/Rate/Lifetime/Brightness/Range.
|
||||
- `Mouse` — Button1Down/Up, Button2Down/Up, Move, KeyDown/Up, WheelForward/Backward,
|
||||
Icon, Hit (Position), Target, TargetSurface, X/Y, ViewSizeX/Y.
|
||||
|
||||
### Исправлено
|
||||
|
||||
- `rbx_wait(sec)`: минимум 0.016с (1 кадр). `while true do wait() end` без
|
||||
аргумента раньше делал tight loop без yield → WASM stack overflow
|
||||
("memory access out of bounds").
|
||||
|
||||
### Надо ли портировать в JS-движок?
|
||||
|
||||
✅ **Да, всё** — это базовый Roblox-совместимый API, который должен работать
|
||||
независимо от языка скриптов.
|
||||
|
||||
**JS-эквивалент будет такой же структурой:**
|
||||
- `BrickColor.new("Bright red")` → `new BrickColor("Bright red")`
|
||||
- `Tool` Equipped/Unequipped → JS-EventEmitter методы
|
||||
- BodyForce/Weld/Sparkles → JS-классы с теми же полями
|
||||
- Mouse — глобальный объект `game.mouse` или через `player:GetMouse()`.
|
||||
|
||||
---
|
||||
|
||||
## Куда добавляется API
|
||||
|
||||
| Источник | Файл | Что туда идёт |
|
||||
|----------|------|---------------|
|
||||
| Глобалы (Vector3, Color3, BrickColor, Enum) | `RobloxShim.js` через `global.set` | Конструкторы, Enum-таблицы |
|
||||
| Instance.new типы | `RobloxShim.js` в ветке `global.set('Instance', {new: ...})` | Tool, BodyForce, Weld, Sparkles и т.д. |
|
||||
| Сервисы | `RobloxShim.js` через `makeService(name)` | Lighting, Players, RunService и т.д. |
|
||||
| Wait/Task | `RobloxShim.js` в Lua-prelude (`lua.doStringSync`) | rbx_wait, task.wait |
|
||||
| Setter Part-свойств | `newPart()` через `Object.defineProperty` | Position, Color, Anchored шлют partSet |
|
||||
| Команды от Lua к Babylon | `rbxl-lua-integration.js` `handleLuaCommand` | partSet, sceneCreate, sceneDelete |
|
||||
|
||||
---
|
||||
|
||||
## Принципы расширения API
|
||||
|
||||
1. **No-op > Падение.** Лучше пустой stub-метод чем `nil error`.
|
||||
2. **Сигналы (`Connect`/`Fire`) всегда есть на любом объекте.**
|
||||
3. **Coloncall совместимость.** Если есть `Foo.Bar`, обычно делаем и `Foo:Bar`
|
||||
(lowercase) как alias.
|
||||
4. **При добавлении нового Instance-типа** — давай ему **все типичные поля**
|
||||
сразу, не только те что нужны прямо сейчас (Equipped + Unequipped + Activated
|
||||
вместе, даже если скрипт юзает только Equipped).
|
||||
5. **Логировать сюда после каждой итерации** — что было добавлено и из какой игры.
|
||||
@ -518,6 +518,53 @@ export function registerRobloxShim(lua, opts) {
|
||||
fromHSV: (h, s, v) => RbxColor3.fromHSV(h, s, v),
|
||||
fromHex: (hex) => RbxColor3.fromHex(hex),
|
||||
});
|
||||
// BrickColor — старая система цветов Roblox по имени
|
||||
const BRICK_COLORS = {
|
||||
'White': [1, 1, 1], 'Black': [0.1, 0.1, 0.1], 'Grey': [0.6, 0.6, 0.6],
|
||||
'Bright red': [0.77, 0.2, 0.2], 'Bright blue': [0.05, 0.4, 0.7],
|
||||
'Bright green': [0.3, 0.8, 0.2], 'Bright yellow': [1, 0.85, 0.1],
|
||||
'Bright orange': [0.85, 0.5, 0.15], 'Bright violet': [0.45, 0.2, 0.65],
|
||||
'Dark blue': [0.05, 0.15, 0.4], 'Dark green': [0.15, 0.4, 0.2],
|
||||
'Dark red': [0.4, 0.1, 0.1], 'Lime green': [0.7, 0.95, 0.3],
|
||||
'Pink': [1, 0.55, 0.7], 'Brown': [0.4, 0.25, 0.15],
|
||||
'Reddish brown': [0.45, 0.2, 0.15], 'Sand red': [0.85, 0.6, 0.55],
|
||||
'Medium blue': [0.4, 0.65, 0.85], 'Cyan': [0, 0.8, 0.8],
|
||||
'Magenta': [0.85, 0, 0.85], 'Really red': [1, 0, 0], 'Really blue': [0, 0, 1],
|
||||
'Really black': [0, 0, 0], 'Really white': [1, 1, 1],
|
||||
};
|
||||
function _brickToColor3(name) {
|
||||
const rgb = BRICK_COLORS[name] || [0.5, 0.5, 0.5];
|
||||
return new RbxColor3(rgb[0], rgb[1], rgb[2]);
|
||||
}
|
||||
global.set('BrickColor', {
|
||||
new(nameOrR, g, b) {
|
||||
// BrickColor.new("Bright red") или BrickColor.new(r, g, b)
|
||||
const name = typeof nameOrR === 'string' ? nameOrR : 'White';
|
||||
const c = typeof nameOrR === 'string'
|
||||
? _brickToColor3(nameOrR)
|
||||
: new RbxColor3(nameOrR, g, b);
|
||||
return { Color: c, Name: name, Number: 1, R: c.R, G: c.G, B: c.B,
|
||||
r: c.R, g: c.G, b: c.B };
|
||||
},
|
||||
random() { return { Color: new RbxColor3(Math.random(), Math.random(), Math.random()), Name: 'Random' }; },
|
||||
White() { return this.new('White'); },
|
||||
Black() { return this.new('Black'); },
|
||||
Gray() { return this.new('Grey'); },
|
||||
Red() { return this.new('Bright red'); },
|
||||
Yellow() { return this.new('Bright yellow'); },
|
||||
Green() { return this.new('Bright green'); },
|
||||
Blue() { return this.new('Bright blue'); },
|
||||
DarkGray() { return this.new('Dark stone grey'); },
|
||||
palette(n) { return this.new('White'); },
|
||||
});
|
||||
// Ray — луч, используется в raycast
|
||||
global.set('Ray', {
|
||||
new(origin, direction) { return { Origin: origin, Direction: direction }; },
|
||||
});
|
||||
// Region3 — куб в пространстве
|
||||
global.set('Region3', {
|
||||
new(min, max) { return { Min: min, Max: max, CFrame: { Position: min }, Size: max }; },
|
||||
});
|
||||
global.set('UDim', { new: (s, o) => new RbxUDim(s, o) });
|
||||
global.set('UDim2', {
|
||||
new: (sx, ox, sy, oy) => new RbxUDim2(sx, ox, sy, oy),
|
||||
@ -807,7 +854,23 @@ export function registerRobloxShim(lua, opts) {
|
||||
const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 3 | 8)).toString(16);
|
||||
});
|
||||
|
||||
makeService('Lighting').Ambient = new RbxColor3(0.5, 0.5, 0.5);
|
||||
const lighting = makeService('Lighting');
|
||||
lighting.Ambient = new RbxColor3(0.5, 0.5, 0.5);
|
||||
lighting.Brightness = 1;
|
||||
lighting.ClockTime = 14;
|
||||
lighting.TimeOfDay = "14:00:00";
|
||||
lighting.OutdoorAmbient = new RbxColor3(0.5, 0.5, 0.5);
|
||||
lighting.FogEnd = 100000;
|
||||
lighting.FogStart = 0;
|
||||
lighting.FogColor = new RbxColor3(0.75, 0.75, 0.75);
|
||||
lighting._minutes = 14 * 60;
|
||||
lighting.GetMinutesAfterMidnight = function () { return lighting._minutes; };
|
||||
lighting.SetMinutesAfterMidnight = function (m) {
|
||||
lighting._minutes = (Number(m) || 0) % 1440;
|
||||
lighting.ClockTime = lighting._minutes / 60;
|
||||
};
|
||||
lighting.GetMoonDirection = function () { return new RbxVector3(0, 1, 0); };
|
||||
lighting.GetSunDirection = function () { return new RbxVector3(0, 1, 0); };
|
||||
makeService('Chat');
|
||||
const soundService = makeService('SoundService');
|
||||
soundService.PlayLocalSound = function (sound) {
|
||||
@ -843,7 +906,28 @@ export function registerRobloxShim(lua, opts) {
|
||||
if (name === 'Players') return players;
|
||||
return services[name] || makeService(name);
|
||||
};
|
||||
// Старый Roblox API: game:service(name) lowercase
|
||||
game.service = game.GetService;
|
||||
game.GetServiceFromName = game.GetService;
|
||||
game.FindService = function (name) { return services[name] || null; };
|
||||
game.JobId = '';
|
||||
game.PlaceId = 0;
|
||||
game.GameId = 0;
|
||||
game.CreatorId = 0;
|
||||
game.CreatorType = 'User';
|
||||
|
||||
// Players API extensions
|
||||
players.GetPlayers = function () { return [players.LocalPlayer].filter(Boolean); };
|
||||
players.GetPlayerFromCharacter = function (character) {
|
||||
if (character && players.LocalPlayer && players.LocalPlayer.Character === character) {
|
||||
return players.LocalPlayer;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
players.playerFromCharacter = players.GetPlayerFromCharacter;
|
||||
players.PlayerAdded = makeSignal();
|
||||
players.PlayerRemoving = makeSignal();
|
||||
players.ChildAdded = makeSignal();
|
||||
|
||||
global.set('game', game);
|
||||
global.set('Game', game);
|
||||
@ -1090,6 +1174,80 @@ export function registerRobloxShim(lua, opts) {
|
||||
|| className === 'ScrollingFrame') {
|
||||
inst = newGuiInstance(className);
|
||||
guiByLocalRef.set(inst.__guiLocalRef, inst);
|
||||
} else if (className === 'Tool' || className === 'HopperBin') {
|
||||
// Tool — оружие в Roblox. У нас нет инвентаря, поэтому это
|
||||
// просто контейнер с правильными событиями + Handle (заглушка).
|
||||
inst = newInstance(className, 'Tool');
|
||||
inst.Equipped = makeSignal();
|
||||
inst.Unequipped = makeSignal();
|
||||
inst.Activated = makeSignal();
|
||||
inst.Deactivated = makeSignal();
|
||||
inst.GripForward = new RbxVector3(0, -1, 0);
|
||||
inst.GripRight = new RbxVector3(1, 0, 0);
|
||||
inst.GripUp = new RbxVector3(0, 1, 0);
|
||||
inst.GripPos = new RbxVector3(0, 0, 0);
|
||||
inst.CanBeDropped = true;
|
||||
inst.Enabled = true;
|
||||
inst.RequiresHandle = true;
|
||||
inst.TextureId = '';
|
||||
inst.ToolTip = '';
|
||||
} else if (className === 'IntValue' || className === 'NumberValue'
|
||||
|| className === 'BoolValue' || className === 'StringValue'
|
||||
|| className === 'ObjectValue' || className === 'CFrameValue'
|
||||
|| className === 'Vector3Value' || className === 'Color3Value'
|
||||
|| className === 'BrickColorValue' || className === 'RayValue') {
|
||||
inst = newInstance(className, className);
|
||||
inst.Value = className === 'BoolValue' ? false
|
||||
: className === 'StringValue' ? ''
|
||||
: (className === 'IntValue' || className === 'NumberValue') ? 0
|
||||
: undefined;
|
||||
inst.Changed = makeSignal();
|
||||
} else if (className === 'BodyForce' || className === 'BodyVelocity'
|
||||
|| className === 'BodyPosition' || className === 'BodyGyro'
|
||||
|| className === 'BodyAngularVelocity' || className === 'BodyThrust') {
|
||||
inst = newInstance(className, className);
|
||||
inst.force = new RbxVector3(0, 0, 0);
|
||||
inst.Force = inst.force;
|
||||
inst.Velocity = new RbxVector3(0, 0, 0);
|
||||
inst.MaxForce = new RbxVector3(0, 0, 0);
|
||||
inst.P = 1000; inst.D = 100;
|
||||
} else if (className === 'Weld' || className === 'WeldConstraint'
|
||||
|| className === 'Motor6D' || className === 'Snap'
|
||||
|| className === 'HingeConstraint' || className === 'BallSocketConstraint'
|
||||
|| className === 'RopeConstraint' || className === 'SpringConstraint') {
|
||||
inst = newInstance(className, className);
|
||||
inst.Part0 = undefined; inst.Part1 = undefined;
|
||||
inst.C0 = { Position: new RbxVector3(0, 0, 0) };
|
||||
inst.C1 = { Position: new RbxVector3(0, 0, 0) };
|
||||
inst.Enabled = true;
|
||||
} else if (className === 'Sparkles' || className === 'ParticleEmitter'
|
||||
|| className === 'Smoke' || className === 'Fire' || className === 'Trail'
|
||||
|| className === 'Beam' || className === 'PointLight'
|
||||
|| className === 'SurfaceLight' || className === 'SpotLight') {
|
||||
inst = newInstance(className, className);
|
||||
inst.Enabled = true;
|
||||
inst.Color = new RbxColor3(1, 1, 1);
|
||||
inst.Rate = 20;
|
||||
inst.Lifetime = { Min: 1, Max: 1 };
|
||||
inst.Brightness = 1;
|
||||
inst.Range = 8;
|
||||
} else if (className === 'Mouse') {
|
||||
inst = newInstance('Mouse', 'Mouse');
|
||||
inst.Button1Down = makeSignal();
|
||||
inst.Button1Up = makeSignal();
|
||||
inst.Button2Down = makeSignal();
|
||||
inst.Button2Up = makeSignal();
|
||||
inst.Move = makeSignal();
|
||||
inst.KeyDown = makeSignal();
|
||||
inst.KeyUp = makeSignal();
|
||||
inst.WheelForward = makeSignal();
|
||||
inst.WheelBackward = makeSignal();
|
||||
inst.Icon = '';
|
||||
inst.Hit = { Position: new RbxVector3(0, 0, 0), p: new RbxVector3(0, 0, 0) };
|
||||
inst.Target = undefined;
|
||||
inst.TargetSurface = 'Top';
|
||||
inst.X = 0; inst.Y = 0;
|
||||
inst.ViewSizeX = 1920; inst.ViewSizeY = 1080;
|
||||
} else {
|
||||
inst = newInstance(className, className);
|
||||
}
|
||||
@ -1142,6 +1300,10 @@ export function registerRobloxShim(lua, opts) {
|
||||
lua.doStringSync(`
|
||||
local function rbx_wait(sec)
|
||||
sec = sec or 0
|
||||
-- Минимум 1 кадр (≈0.0166с). wait() и wait(0) в Roblox ждут до
|
||||
-- следующего Heartbeat — без этого while true do wait() end
|
||||
-- стал бы tight loop без yield и упёрся в WASM stack overflow.
|
||||
if sec < 0.016 then sec = 0.016 end
|
||||
local ret = coroutine.yield(sec)
|
||||
return ret or sec
|
||||
end
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user