Jump to content

Recommended Posts

Posted

.md file format, almost created by Opus (High), this may contain errors, best regards.

# Issues found in the First-Person Shooter template (Leadwerks 5, engineversion 5.2.0)

These were found while adding volume settings to the Options dialog of a project created
from the **First-Person Shooter** template. All of them exist in the unmodified template,
so they are reported separately from that work.

Line numbers refer to the **unmodified** template files. The quoted code identifies each
site unambiguously in case the numbering has since changed.

Each item says how it was confirmed:

- **Verified at runtime** — reproduced in the running game.
- **Verified by inspection** — the code path is unambiguous, but it was not run.
- **Needs checking** — reported because it looks wrong; the author should confirm.

---

## 1. `EVENT_WIDGETSELECT` sends a zero-based tab index, but the documentation says one-based

**Engine or documentation issue, not template. Verified at runtime.**

The `CreateTabber` documentation example shows a one-based index:

```lua
if ev.id == EVENT_WIDGETSELECT then
    if ev.source == tabber then
        for n = 1, #tabber.items do
            if n == ev.data then
                panels[n]:SetHidden(false)
            else
                panels[n]:SetHidden(true)
            end
        end
    end
```

On engineversion 5.2.0 the engine sends a **zero-based** index. Logging the event while
clicking a two-tab tabber gives:

```
event id=22 data=0 src_is_tabber=true   <- first tab
event id=22 data=1 src_is_tabber=true   <- second tab
```

Consequences of the mismatch:

- The documented example **does not work as written**. Selecting the first tab hides every
  page (no `n` equals 0), and selecting the second tab shows the first page.
- The failure is silent: no error, the tabs highlight correctly, only the content is wrong.
  This is hard to diagnose, since the natural assumption is that the event is not firing.

Either the engine should send a one-based index to match the rest of the widget API
(`Widget:SelectItem()` and `GetSelectedItem()` are one-based) or the documentation and its
example should be corrected. The inconsistency with the one-based item API is itself worth
resolving.

Affected page: <https://github.com/Leadwerks/Documentation/blob/master/Lua/CreateTabber.md>

---

## 2. `piscale` typo prevents Display Scale 150% from being restored

**`Scripts/Game/GameMenu.lua:422`, in `menu:UpdateSettings()`. Verified by inspection.**

```lua
local dpiscale = self.game.settings.video.dpiscale
...
if isnumber(dpiscale) then
    if dpiscale == 1 then
        self.dpilist:SelectItem(2)
    elseif dpiscale == 1.25 then
        self.dpilist:SelectItem(3)
    elseif piscale == 1.5 then          -- <-- should be dpiscale
        self.dpilist:SelectItem(4)
    elseif dpiscale == 1.75 then
```

`piscale` is an undeclared global, so it is always `nil` and the comparison is never true.
The setting is saved and applied correctly; only the combo box fails to show 150% when the
Options dialog is reopened, so it silently reverts to "Auto" in the UI.

The surrounding branches are correct, which makes this a plain typo.

---

## 3. Screen-space Reflection is a ComboBox but is handled as a checkbox

**`Scripts/Game/GameMenu.lua:211` and `676`. Needs checking.**

`menu.ssrbutton` is created as a four-item combo box:

```lua
menu.ssrbutton = CreateComboBox(x + cw, y, sz.x - 2 * x - cw, 32, menu.videopanel)
menu.ssrbutton:AddItem("Disabled", true)
menu.ssrbutton:AddItem("Low")
menu.ssrbutton:AddItem("Medium")
menu.ssrbutton:AddItem("High")
```

but in `menu.EvaluateEvent` it is grouped with the checkboxes, which overwrite the widget
text with "Enabled" / "Disabled":

```lua
if event.source == menu.fullscreenbutton or event.source == menu.vsyncbutton
   or event.source == menu.ssrbutton or event.source == menu.vsyncbutton
   or event.source == menu.terrainshadowsbutton or event.source == menu.refractionbutton then
    local widget = Widget(event.source)
    if event.data == 1 then
        widget:SetText("Enabled")
    else
        widget:SetText("Disabled")
    end
    return false
```

Changing the SSR selection therefore calls `SetText("Enabled")` or `SetText("Disabled")` on
a combo box. The `button` suffix in the name suggests this was a checkbox at some point and
the handler was not updated when it became a combo box. The other combo boxes
(`resolutionlist`, `dpilist`, `msaalist`, `anisotropylist`, `shadowqualitylist`,
`tessellationlist`) are correctly absent from this branch.

Note also that `menu.vsyncbutton` appears **twice** in the same `or` chain. One of the two
was probably meant to be a different widget.

---

## 4. Screen-space Reflection "High" is never restored (off-by-one in the range guard)

**`Scripts/Game/GameMenu.lua:411` vs `520`. Verified by inspection.**

`menu:ApplySettings()` stores the zero-based selection of a four-item list, so the value
ranges over 0..3:

```lua
self.game.settings.video.ssr = self.ssrbutton:GetSelectedItem() - 1
```

`menu:UpdateSettings()` guards with `< 3`, which excludes 3:

```lua
if isnumber(self.game.settings.video.ssr) and self.game.settings.video.ssr >= 0
   and self.game.settings.video.ssr < 3 then
    self.ssrbutton:SelectItem(self.game.settings.video.ssr + 1)
end
```

So choosing **High**, applying, and reopening the Options dialog leaves the combo box showing
whatever it showed before instead of "High". The setting itself is stored and honoured —
`menu:ApplyCameraSettings()` handles `ssr == 3` explicitly — so only the UI is out of sync.

The guard should be `<= 3`, or better, `< #self.ssrbutton.items`.

---

## 5. Three `ListenEvent` registrations pass a widget that is never created

**`Scripts/Game/GameMenu.lua:757, 758, 760`. Verified by inspection.**

```lua
ListenEvent(EVENT_WIDGETACTION, menu.bloombutton, menu.EvaluateEvent, menu)
ListenEvent(EVENT_WIDGETACTION, menu.ssaobutton, menu.EvaluateEvent, menu)
ListenEvent(EVENT_WIDGETACTION, menu.ssrbutton, menu.EvaluateEvent, menu)            -- this one is real
ListenEvent(EVENT_WIDGETACTION, menu.volumetriclightingbutton, menu.EvaluateEvent, menu)
```

`menu.bloombutton`, `menu.ssaobutton` and `menu.volumetriclightingbutton` are never assigned
anywhere in the file, so all three calls pass `nil` as the event source. Post-processing
effects are created dynamically by `menu:AddPostEffect()`, which registers its own listener
for the widget it creates, so nothing is lost — these three lines are simply leftovers.

Passing `nil` as the source is also how the template registers **global** listeners
(`ListenEvent(EVENT_MOUSEMOVE, nil, menu.PassEvent, menu)`), so these lines may be
registering `menu.EvaluateEvent` for every `EVENT_WIDGETACTION` in the program three times
over, rather than doing nothing. That is worth confirming.

---

## 6. Duplicate, unreachable branch for the Quit button

**`Scripts/Game/GameMenu.lua:713` and `726`. Verified by inspection.**

`menu.EvaluateEvent` tests `event.source == menu.quitbutton` twice in the same if/elseif
chain. The second can never be reached:

```lua
elseif event.source == menu.quitbutton then
    menu.optionspanel:SetHidden(true)
    menu.quitpanel:SetHidden(false)
    return false
...
elseif event.source == menu.cancelquitbutton then
    menu.quitpanel:SetHidden(true)
    return false

elseif event.source == menu.quitbutton then     -- unreachable
    menu.quitpanel:SetHidden(false)
    return false
```

The two bodies differ — the first also hides the options panel — which suggests the second
is an older version that was left behind.

---

## 7. `menu:AddPostEffect()` mixes scaled and unscaled coordinates

**`Scripts/Game/GameMenu.lua:270`. Needs checking.**

`AddPostEffect` multiplies its own layout values by the current UI scale, but the width comes
from `sz`, an upvalue captured before `menu.ui:SetScale()` runs at the end of
`CreateGameMenu`:

```lua
local scale = self.ui:GetScale()
local x = 8 * scale
...
CreateLabel(t.name, x, y, sz.x - 2 * x, lh * scale, menu.videopanel)
t.widget = CreateButton("Disabled", x + cw * scale, y, bw * scale, lh * scale, self.videopanel, BUTTON_CHECKBOX)
```

At a display scale of 200%, `sz.x` still holds the unscaled client width, so the label is
created about half as wide as it should be. Left-aligned text hides this in practice, but
the width is wrong.

The same function refers to the panel as both `menu.videopanel` and `self.videopanel` on
consecutive lines.

---

## 8. The Apply button closes the Options dialog

**`Scripts/Game/GameMenu.lua:685`. Verified at runtime. Behaviour, not a defect.**

```lua
elseif event.source == menu.okbutton then
    menu.optionspanel:SetHidden(true)
    menu:ApplySettings()
    return false
```

With a dialog that offers both **Apply** and **Close**, the usual convention is that Apply
applies and leaves the dialog open, and Close dismisses it. Closing on Apply is surprising,
and it prevents the common pattern of changing a video setting, looking at the result, and
adjusting again. It also means the relayout performed at the end of `ApplySettings()`
(`UpdateLayout()` and `UpdateHeight()`) always runs on a hidden dialog, so it is never
exercised.

Removing the `SetHidden(true)` line is sufficient; leaving the dialog open across a
resolution change and a DPI scale change was tested and works correctly.

---

## 9. Settings are only written on a clean exit

**`Scripts/Main.lua`, at `::Exit::`. By design, but worth reconsidering.**

`game:SaveSettings()` is called only at the normal exit path. A crash, a kill from the task
manager, or any exit that does not reach `::Exit::` discards every setting the player
changed during the session, including ones they explicitly applied.

Calling `SaveSettings()` from the Apply handler as well would make applied settings durable
at the moment the player commits them, at the cost of one small file write per click.

---

## 10. Feature request: there is no global or per-category volume control

**Engine API. Verified against the documentation and by implementation.**

There is no equivalent of `SetMasterVolume()` in the Lua API. The only volume controls are
`Speaker:SetVolume()` and the `volume` argument of `Entity:EmitSound()`, both of which act on
a single speaker. In addition, `Sound:Play()` takes **no volume argument at all**, so a 2D
sound cannot be started at a chosen volume — it has to be started and then adjusted:

```lua
local speaker = sound:Play()
speaker:SetVolume(gain)         -- the attack is rendered before this takes effect
```

The practical consequence is that **any** game wanting a volume slider must wrap every place
a sound is started and keep its own registry of playing speakers so that a volume change can
reach sounds that are already playing (looping ambience in particular). In this template that
meant touching 22 call sites across 10 files.

Two things would remove most of that work:

1. A global volume, and ideally named categories or buses, applied by the audio engine.
2. A `volume` argument on `Sound:Play()`, matching `Entity:EmitSound()`.

A related documentation gap: `CreateSpeaker()`, `Speaker.md` and `Speaker:SetPosition()` do
not say whether a speaker that was never positioned plays as a non-attenuated 2D sound. That
determines whether `CreateSpeaker()` can be used to start a 2D sound at a given volume, which
is exactly the workaround for the missing argument above.

---

## 11. Unused locals and dead state

**Verified by inspection. Cosmetic.**

| Location | Item |
|---|---|
| `GameMenu.lua:131` | `local videopanel` is declared and never used; `menu.videopanel` is used instead |
| `GameMenu.lua:230`, `272` | `menu.maxy` is assigned and incremented but never read — `scrollpanel:UpdateHeight()` computes its own extent |
| `GameMenu.lua:100`, `104` | `local w` in `CreateScrollPanel:UpdateHeight()` is assigned twice and never read |
| `GameMenu.lua:87-96` | `indent` is computed in a loop over the child widgets, then unconditionally overwritten with `indent = 0` before use, so the loop's contribution is discarded |

---

## Environment

- Leadwerks 5, `engineversion` 5.2.0
- Template: First-Person Shooter, Lua 5.4
- Windows 11 Pro 26200
 

Posted

Hi, 
> Is this a bug report generated by AI?
Yes. 

The desired volume setting itself came from me (a human), but how to design and implement it was decided by the AI. I'm sorry this got mixed in with the bug reports.

Posted
2 hours ago, mskurumi said:

There is no equivalent of `SetMasterVolume()` in the Lua API. The only volume controls are
`Speaker:SetVolume()` and the `volume` argument of `Entity:EmitSound()`, both of which act on
a single speaker. In addition, `Sound:Play()` takes **no volume argument at all**, so a 2D
sound cannot be started at a chosen volume — it has to be started and then adjusted:

okay, this. I am not going to read the rest, sorry.

Let's build cool stuff and have fun. :)

  • Josh changed the title to Global sound volume setting

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...