Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions config/pool.lua
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ OPX.Config.MODULES.pool = {
MODELS = {
cue = 'recreation.tent',
object = 'recreation.sleeping_bag', -- retained legacy profile field
-- Object balls use the fifteen vanilla ball_c..ball_q meshes by default.
-- Optional objects = { ... } must name exactly fifteen distinct models.
-- Object balls register fifteen cooked hosts from opx_pool_assets >=0.3.0.
-- Optional objects = { ... } must name fifteen distinct registered model IDs.
stick = 'recreation.gym_equipment',
-- Host-specific heading/pivot calibration, in degrees/metres.
stickYaw = 0, stickOffset = 0.7,
Expand Down
22 changes: 15 additions & 7 deletions docs/MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -950,11 +950,18 @@ Contract `bar` v1 (server): `Venues()`, `Items()`, `DrunkLevel(player)`. Bus:
`modules/pool` + `config/pool.lua` offers three original modes using one white
cue ball and fifteen distinct vanilla object balls, arranged in a five-row triangle.
The object balls use the game’s `ball_c_billiard.mesh` through `ball_q_billiard.mesh`
models via the documented [raw mesh prop backend](https://open2077.net/docs/props).
An optional `MODELS.objects` array supplies exactly fifteen model paths. Existing
configs without that field receive the full vanilla set automatically. Cue/white
hosts, recorded audio and cleanup
patches live in the separate `opx_pool_assets` resource. Table actions use menu-wide
models through fifteen cooked hosts in `opx_pool_assets` **0.3.0 or later**. Infinity
registers each mesh/visual-host pair using the resource-owned model registry on
**op77.152 or later**, then renders the opaque model ID returned by the native.
Raw mesh names alone displayed fallback cylinders on the deployed client. Missing
or refused registration now leaves the table unavailable with a friendly notice,
rather than spawning placeholders. Existing configs automatically use the full
registered set. Optional `MODELS.objects` overrides must supply fifteen distinct,
already registered model IDs or supported aliases. Cue/white hosts, recorded audio
and cleanup patches remain in the same asset resource. After upgrading the pack,
**fully close the game and launch it again through the launcher**; reconnecting
inside a running game does not mount the updated archive. See
[the version-specific native contract](POOL_MODELS.md). Table actions use menu-wide
`closeOnSelect`, so selecting a mode dispatches its command before releasing
focus. Confirmation submenus dispatch only their confirmed selection. The modes
have no wagers,
Expand Down Expand Up @@ -1059,7 +1066,7 @@ cannot take a shot. These modes have no economic effects or persisted game state
The native baseline checked is Devkit `2.50.0+op77.148`; the test VPS uses
`2.50.0+op77.152`. Install
[`opx_pool_assets`](https://github.com/opx77-framework/opx_pool_assets) before Infinity.
Use asset resource version `0.2.0` or later. Its client script publishes the recorded
Use asset resource version `0.3.0` or later. Its client script publishes the recorded
audio service and makes the resource available in the signed client set to satisfy
Infinity's dependency. The service alone declares `audio.play`; Infinity does not
request audio-network authority.
Expand All @@ -1082,7 +1089,8 @@ required-mod trust/review policy; do not silently enable unsecured mode to insta
a loader. Restart the server, fully quit Cyberpunk, and reconnect through the
launcher: cooked archives cannot mount during a running game session.

The asset package deliberately reserves three native aliases globally:
The fifteen new `opx/pool/ball_*.ent` hosts have their own namespace and reserve
no additional global aliases. The legacy asset package reserves three aliases globally:
`recreation.tent` (white), `recreation.sleeping_bag` (yellow), and
`recreation.gym_equipment` (horizontal cue). Other resources must not use them for
their original objects. Six normal/attached hosts are repacked from the supplied
Expand Down
44 changes: 44 additions & 0 deletions docs/POOL_MODELS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Pool model registry contract

The native reference available during this fix describes op77.148 and does not
list model registration. The deployed, signed Open77 client is **2.50.0+op77.152**.
The full-rack renderer therefore checks the native exists and refuses the table
with an operator diagnostic and friendly notification on older clients.

The inspected client DLL has SHA-256:
`f34f74c9c9c3267bc2fc6bb32fccf940772ecf608aba52c5c58202dd2afde74e`.
It came from the signed core manifest; no modified client executable is shipped.

The Lua `Open77.props.registerModel` binding is registered at VA `0x180792657`
and implemented at VA `0x180759398` (preferred image base `0x180000000`). The
binding checks `world.props`, accepts exactly two arguments (string ID, table),
and accepts only the string fields `mesh`, `visualEntity`, `physicalEntity`.
The latter is optional; the mesh and visual entity must be depot `.mesh` and
`.ent` paths. It returns an opaque resource-owned model ID string on success,
or nil and a refusal such as `invalid_model_arguments`, `invalid_model_fields`,
`invalid_model_paths` or `model_registration_conflict`. Infinity uses the returned
string, never invents its format, and supplies no physical entity for pool balls.

Each default pair is registered once per pool module start:

```lua
local model, reason = Open77.props.registerModel('pool_ball_1', {
mesh = 'base\\environment\\decoration\\recreation\\sports\\ball\\ball_c_billiard.mesh',
visualEntity = 'opx\\pool\\ball_01.ent',
})
```

`opx_pool_assets` 0.3.0 includes all fifteen visual hosts. They are deterministic
derivatives of the supplied, previously rendered object-ball template, each with
one changed vanilla mesh import. Their modified compiled-data buffer is stored
uncompressed; unchanged buffers, offsets, buffer/table/header CRC32 and archive
segment metadata are checked by the asset package tests. Only the original six
legacy host paths and the fifteen new `opx/pool/` paths are allowed in the archive.
No game meshes or textures, DLLs, unrelated alias overrides or legacy gameplay
scripts are added.

A successful registration diagnostic proves the registry accepted the mapping,
not that the engine rendered it. The package requires a fresh game launch through
the launcher. Actual materials, visual scale and removal/rejoin behavior still
require a live game test; native doubles and binary validation do not establish
those visual results.
2 changes: 1 addition & 1 deletion modules/pool/client/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ end
function M.Start()
R.Reset(); G.Reset()
spec = Model.Table()
if not spec then return end
if not spec or not R.Register() then return end
target, prompts = OPX.Api.Get('target'), OPX.Api.Get('prompts')
if target then target.RegisterSpheres('pool', { { x = spec.x, y = spec.y, z = spec.z, radius = 2 } }, {
id = 'practice', label = locale('pool.tableMenu'), icon = 'interact', distance = 3,
Expand Down
30 changes: 26 additions & 4 deletions modules/pool/client/render.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ local Model = M.Model
M.Render = {}
local R = M.Render
local ids, stick, created, failure, stroke = {}, nil, {}, nil, nil
local models
local models, registered
function R.Clear()
for index, id in pairs(ids) do Open77.props.remove(id); ids[index] = nil end
if stick then Open77.props.remove(stick); stick = nil end
created, stroke = {}, nil
end
function R.Reset() R.Clear(); failure = nil; models = Model.Models() end
function R.Reset() R.Clear(); failure = nil; models = Model.Models(); registered = false end
local function fail(why)
if not failure then
failure = tostring(why)
Expand All @@ -21,6 +21,29 @@ local function fail(why)
R.Clear()
return false
end
-- op77.152 registerModel(id, {mesh, visualEntity, physicalEntity?}) returns
-- an opaque resource-owned model ID. Cooked visual hosts are shipped by assets
-- 0.3.0; a raw .mesh name alone falls back to a marker on this client.
function R.Register()
if failure then return false end
if registered then return true end
if not models then return fail('invalid model configuration') end
if models.registerObjects then
if type(Open77.props.registerModel) ~= 'function' then return fail('prop model registration requires op77.152 or later') end
for number = 1, Model.BALLS-1 do
local ok, model, why = pcall(Open77.props.registerModel, 'pool_ball_' .. number, {
mesh = models.objects[number], visualEntity = ('opx\\pool\\ball_%02d.ent'):format(number),
})
if not ok or type(model) ~= 'string' or #model == 0 or #model > 256 or model:find('%c') then
return fail('registerModel ball ' .. number .. ': ' .. tostring(ok and (why or model) or model))
end
models.objects[number] = model
end
end
registered = true
if models.registerObjects then OPX.Note('pool', 'registered fifteen cooked visual ball hosts') end
return true
end
local function make(model, position, yaw)
local id, why = Open77.props.create({ model = model, position = position, yaw = yaw or 0, collision = false, physics = "none", visible = true })
if not id then return nil, why end
Expand Down Expand Up @@ -48,8 +71,7 @@ function R.StickTransform(spec, ball, angle, power, forward)
end
function R.Draw(spec, positions, aiming, angle, power)
if failure then return false end
models = models or Model.Models()
if not models then return fail('invalid model configuration') end
if not R.Register() then return false end
for index, ball in ipairs(positions) do
if ball[3] then
local position = Model.World(spec, ball[1], ball[2])
Expand Down
1 change: 1 addition & 0 deletions modules/pool/shared/model.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function Model.Models()
result[name] = path
end
result.objects = {}
result.registerObjects = raw.objects == nil
local used = {}
if raw.objects ~= nil and (type(raw.objects) ~= 'table' or #raw.objects ~= Model.BALLS-1) then return nil end
for number = 1, Model.BALLS-1 do
Expand Down
2 changes: 1 addition & 1 deletion open77.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ auto_start true
-- and puts it on `OPX.Lib`; see that file for why only the client half uses it.
dependency "opx_lib"
-- Cooked pool hosts and table cleanup are mounted by the launcher before boot.
dependency "opx_pool_assets >=0.2.0"
dependency "opx_pool_assets >=0.3.0"

reload_policy "reconnect"

Expand Down
31 changes: 29 additions & 2 deletions tests/audit-pool-client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,22 @@ end
env.Open77.players.localId = function() return 7 end
env.Open77.character.position = function() return 99,10,1 end
env.Open77.input.axis = function() return mouse end
local registrations, registeredModels, registrationRefusal = {}, {}, nil
env.Open77.props = {
registerModel = function(id, definition)
if registrationRefusal then return nil,registrationRefusal end
assert(id:match('^pool_ball_%d+$') and type(definition)=='table','invalid registration arguments')
local number=tonumber(id:match('%d+$'))
assert(definition.mesh:match('ball_'..string.char(98+number)..'_billiard%.mesh$'),'wrong mesh in registration')
assert(definition.visualEntity==('opx\\pool\\ball_%02d.ent'):format(number),'wrong cooked host')
assert(definition.physicalEntity==nil,'visual pool ball acquired a physical host')
local result='opaque-registered-model:'..id
registrations[id]=definition;registeredModels[result]=true
return result
end,
create = function(options)
assert(not options.model:match('%.mesh$'),'raw mesh bypassed its cooked host')
assert(options.model=='recreation.tent' or options.model=='recreation.gym_equipment' or registeredModels[options.model],'renderer invented a registration ID')
local modes={static=true,kinematic=true,dynamic=true,none=true}
if not modes[options.physics] then return nil,'invalid_physics' end
assert(options.collision==false and options.physics=='none' and options.visible==true,'pool visual prop acquired collision/physics')
Expand Down Expand Up @@ -65,19 +79,21 @@ local drawnModels={}
for _,prop in pairs(props)do drawnModels[prop.model]=true end
local distinct=0;for _ in pairs(drawnModels)do distinct=distinct+1 end
assert(distinct==16,'object balls reused the same yellow model')
local registeredCount=0;for _ in pairs(registrations) do registeredCount=registeredCount+1 end
assert(registeredCount==15,'not all fifteen cooked hosts were registered')
assert(creates==16 and state.allowShoot==false and state.allowAim==false,'practice did not render/hold weapons')
M.Client.Frame(); M.Client.Guide(); M.Client.Strip()
assert(cameraHeld and state.freezePosition==true and guideShown,'entry did not arm a camera/guide')
control.input.down.V=true;control.Advance(20);M.Client.Frame();control.input.down.V=false
assert(cameraFov==52,'inventory V key switched pool view')
control.input.down.mouse3=true;control.Advance(20);M.Client.Frame();control.input.down.mouse3=false
assert(cameraFov>52,'middle click did not switch pool view')
local function measured(fn)
local function measured(fn, ceiling)
local cost=0
debug.sethook(function() cost=cost+100 end,'',100)
local ok,why=pcall(fn)
debug.sethook()
assert(ok,why); assert(cost<5000,'client resume exceeds full-rack budget: '..cost)
assert(ok,why); assert(cost<(ceiling or 5000),'client resume exceeds full-rack budget: '..cost)
return cost
end
local worst=0
Expand Down Expand Up @@ -173,6 +189,17 @@ local accepted=packet(2,50);accepted.epoch=13;accepted.game=M.Match.New(choice.m
M.Client.Receive(accepted);M.Client.Frame();M.Client.Frame()
assert(cameraHeld,'real menu selection failed to arm camera')
M.Stop()
-- Registration of all fifteen hosts stays inside one client resume.
M.Stop();M.Init();worst=math.max(worst,measured(M.Start,8000));M.Stop()
-- A missing/refused registry must never spawn raw meshes or placeholders.
M.Stop();registrationRefusal='model_registration_conflict'
local beforeRefusal=creates
M.Init();M.Start();M.Client.Watch();M.Client.Draw()
assert(M.Render.Failed() and creates==beforeRefusal,'registration refusal spawned fallback cylinders')
M.Stop();registrationRefusal=nil
local registry=env.Open77.props.registerModel;env.Open77.props.registerModel=nil
M.Init();M.Start();assert(M.Render.Failed(),'old client silently spawned unsupported models');M.Stop()
env.Open77.props.registerModel=registry
-- A just-created native entity may not be transformable until streaming ends.
M.Render.Reset(); failed=false
local geometry={x=100,y=10,z=1,yaw=0}
Expand Down
Loading