lfs-insim 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lfs_insim-0.1.0/.gitignore +22 -0
- lfs_insim-0.1.0/CHANGELOG.md +14 -0
- lfs_insim-0.1.0/LICENSE +21 -0
- lfs_insim-0.1.0/PKG-INFO +94 -0
- lfs_insim-0.1.0/README.md +70 -0
- lfs_insim-0.1.0/docs/README.md +10 -0
- lfs_insim-0.1.0/docs/getting-started.md +82 -0
- lfs_insim-0.1.0/docs/insim.md +102 -0
- lfs_insim-0.1.0/docs/outsim-outgauge.md +71 -0
- lfs_insim-0.1.0/docs/roster-and-text.md +51 -0
- lfs_insim-0.1.0/examples/buttons.py +71 -0
- lfs_insim-0.1.0/examples/gauge.py +33 -0
- lfs_insim-0.1.0/examples/hello.py +23 -0
- lfs_insim-0.1.0/examples/who.py +38 -0
- lfs_insim-0.1.0/pyproject.toml +67 -0
- lfs_insim-0.1.0/scripts/close_check.py +359 -0
- lfs_insim-0.1.0/scripts/ctx_log_last.py +63 -0
- lfs_insim-0.1.0/scripts/ctx_plan_active.py +80 -0
- lfs_insim-0.1.0/scripts/fetch_spec.py +194 -0
- lfs_insim-0.1.0/scripts/generate_enums.py +140 -0
- lfs_insim-0.1.0/scripts/generate_packets.py +434 -0
- lfs_insim-0.1.0/scripts/session_start_check.py +268 -0
- lfs_insim-0.1.0/scripts/spec_overrides.py +136 -0
- lfs_insim-0.1.0/scripts/spec_parser.py +291 -0
- lfs_insim-0.1.0/scripts/verify.py +113 -0
- lfs_insim-0.1.0/spec/checked.json +12 -0
- lfs_insim-0.1.0/src/lfs_insim/__init__.py +12 -0
- lfs_insim-0.1.0/src/lfs_insim/_commands.py +70 -0
- lfs_insim-0.1.0/src/lfs_insim/_connection.py +406 -0
- lfs_insim-0.1.0/src/lfs_insim/_handlers.py +81 -0
- lfs_insim-0.1.0/src/lfs_insim/_requests.py +131 -0
- lfs_insim-0.1.0/src/lfs_insim/_run.py +47 -0
- lfs_insim-0.1.0/src/lfs_insim/_stream.py +56 -0
- lfs_insim-0.1.0/src/lfs_insim/_udp.py +183 -0
- lfs_insim-0.1.0/src/lfs_insim/packets/__init__.py +295 -0
- lfs_insim-0.1.0/src/lfs_insim/packets/_codec.py +525 -0
- lfs_insim-0.1.0/src/lfs_insim/packets/_enums.py +1712 -0
- lfs_insim-0.1.0/src/lfs_insim/packets/_extras.py +24 -0
- lfs_insim-0.1.0/src/lfs_insim/packets/_insim.py +2807 -0
- lfs_insim-0.1.0/src/lfs_insim/packets/_outgauge.py +99 -0
- lfs_insim-0.1.0/src/lfs_insim/packets/_outsim.py +261 -0
- lfs_insim-0.1.0/src/lfs_insim/packets/_text.py +142 -0
- lfs_insim-0.1.0/src/lfs_insim/packets/_types.py +29 -0
- lfs_insim-0.1.0/src/lfs_insim/py.typed +0 -0
- lfs_insim-0.1.0/src/lfs_insim/state.py +167 -0
- lfs_insim-0.1.0/src/lfs_insim/text.py +51 -0
- lfs_insim-0.1.0/tests/fake_lfs.py +210 -0
- lfs_insim-0.1.0/tests/lfs_screen.ps1 +59 -0
- lfs_insim-0.1.0/tests/live_lfs.py +385 -0
- lfs_insim-0.1.0/tests/test_codec.py +225 -0
- lfs_insim-0.1.0/tests/test_commands.py +152 -0
- lfs_insim-0.1.0/tests/test_docs.py +42 -0
- lfs_insim-0.1.0/tests/test_every.py +242 -0
- lfs_insim-0.1.0/tests/test_examples.py +148 -0
- lfs_insim-0.1.0/tests/test_fake_lfs.py +147 -0
- lfs_insim-0.1.0/tests/test_fetch_spec.py +71 -0
- lfs_insim-0.1.0/tests/test_insim.py +489 -0
- lfs_insim-0.1.0/tests/test_package.py +12 -0
- lfs_insim-0.1.0/tests/test_packets.py +379 -0
- lfs_insim-0.1.0/tests/test_readme.py +15 -0
- lfs_insim-0.1.0/tests/test_request.py +143 -0
- lfs_insim-0.1.0/tests/test_roster.py +258 -0
- lfs_insim-0.1.0/tests/test_say.py +109 -0
- lfs_insim-0.1.0/tests/test_spec_parser.py +168 -0
- lfs_insim-0.1.0/tests/test_stream.py +73 -0
- lfs_insim-0.1.0/tests/test_text.py +175 -0
- lfs_insim-0.1.0/tests/test_udp.py +248 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
.mypy_cache/
|
|
11
|
+
|
|
12
|
+
# Especificación oficial descargada de lfs.net (W1.4): no se versiona
|
|
13
|
+
spec/cache/
|
|
14
|
+
|
|
15
|
+
# Informes de la prueba en vivo (W3.1): llevan nombres de jugadores
|
|
16
|
+
tests/live_reports/
|
|
17
|
+
|
|
18
|
+
# Editores y sistema
|
|
19
|
+
.vscode/
|
|
20
|
+
.idea/
|
|
21
|
+
Thumbs.db
|
|
22
|
+
.DS_Store
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release (alpha). The API may still change before 1.0.
|
|
6
|
+
|
|
7
|
+
- InSim over TCP: every packet of the protocol, generated from the specification published at
|
|
8
|
+
lfs.net; handlers with `@insim.on`, chat commands, `say()`/`command()`, `request()`/
|
|
9
|
+
`request_all()`, `every()`, and automatic reconnection.
|
|
10
|
+
- OutSim (`OutSimPack` and `OutSimPack2`) and OutGauge over UDP, alone or together with InSim
|
|
11
|
+
through `run()`.
|
|
12
|
+
- `lfs_insim.state.Roster` (connections and players) and `lfs_insim.text` (`plain`,
|
|
13
|
+
`strip_colours`).
|
|
14
|
+
- Checked against a real LFS 0.8C, and by the tests against a fake LFS.
|
lfs_insim-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adrian Pascarella
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lfs_insim-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lfs-insim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python library for the Live for Speed InSim protocol (InSim, OutSim, OutGauge).
|
|
5
|
+
Project-URL: Homepage, https://github.com/AdrianPascarella/AP_LFS_InSim
|
|
6
|
+
Project-URL: Issues, https://github.com/AdrianPascarella/AP_LFS_InSim/issues
|
|
7
|
+
Author: Adrian Pascarella
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: insim,lfs,live for speed,outgauge,outsim,racing,simulator
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Games/Entertainment :: Simulation
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# lfs_insim
|
|
26
|
+
|
|
27
|
+
A Python library for the [Live for Speed](https://www.lfs.net) InSim protocol: **InSim** (TCP),
|
|
28
|
+
**OutSim** and **OutGauge** (UDP). Plain `import`, no framework, no dependencies.
|
|
29
|
+
|
|
30
|
+
> **Status: alpha.** The library is checked against a real LFS (0.8C), but its API may still
|
|
31
|
+
> change before 1.0.
|
|
32
|
+
|
|
33
|
+
- Python 3.11 or newer, no dependencies, fully typed.
|
|
34
|
+
- Every packet class is generated from the official specification published at lfs.net, so the
|
|
35
|
+
fields are named exactly as in `InSim.txt`.
|
|
36
|
+
- Handlers with decorators, `await insim.request(...)`, chat commands, a roster of connections
|
|
37
|
+
and players, and automatic reconnection if LFS restarts.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pip install lfs-insim
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## A first program
|
|
46
|
+
|
|
47
|
+
In LFS, type `/insim=29999` to let it listen for InSim. Then run this and type `!hello` in the
|
|
48
|
+
game:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from lfs_insim import Command, InSim
|
|
52
|
+
from lfs_insim.packets import IS_VER
|
|
53
|
+
|
|
54
|
+
insim = InSim(name="hello", prefix="!") # admin_password="..." if LFS has one
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@insim.on(IS_VER)
|
|
58
|
+
def connected(packet: IS_VER) -> None:
|
|
59
|
+
print(f"Connected to LFS {packet.Version}, InSim {packet.InSimVer}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@insim.on_command("hello")
|
|
63
|
+
def hello(command: Command) -> None:
|
|
64
|
+
command.reply("Hello from Python")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
insim.run() # blocks until Ctrl+C; reconnects on its own if LFS restarts
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## What is in the box
|
|
72
|
+
|
|
73
|
+
| You want to… | Use |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| Receive a packet | `@insim.on(IS_XXX)` |
|
|
76
|
+
| Answer a chat command | `@insim.on_command("name")` |
|
|
77
|
+
| Send packets, chat or `/commands` | `insim.send(...)`, `insim.say(...)`, `insim.command(...)` |
|
|
78
|
+
| Ask LFS and wait for the answer | `await insim.request(IS_STA)`, `request_all(...)` |
|
|
79
|
+
| Run something on a rhythm | `@insim.every(seconds)` |
|
|
80
|
+
| Know who is connected and who is driving | `lfs_insim.state.Roster` |
|
|
81
|
+
| Read car physics or the dashboard | `OutSim` and `OutGauge` |
|
|
82
|
+
| Show LFS text without colour codes | `lfs_insim.text.plain(...)` |
|
|
83
|
+
|
|
84
|
+
The guide is in [`docs/`](docs/README.md). Short, runnable programs are in [`examples/`](examples): `hello.py`, `who.py`, `buttons.py` and
|
|
85
|
+
`gauge.py`.
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
This repository is worked on with the notes in [`docs/dev`](docs/dev) (in Spanish). To check
|
|
90
|
+
everything: `python scripts/verify.py`.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# lfs_insim
|
|
2
|
+
|
|
3
|
+
A Python library for the [Live for Speed](https://www.lfs.net) InSim protocol: **InSim** (TCP),
|
|
4
|
+
**OutSim** and **OutGauge** (UDP). Plain `import`, no framework, no dependencies.
|
|
5
|
+
|
|
6
|
+
> **Status: alpha.** The library is checked against a real LFS (0.8C), but its API may still
|
|
7
|
+
> change before 1.0.
|
|
8
|
+
|
|
9
|
+
- Python 3.11 or newer, no dependencies, fully typed.
|
|
10
|
+
- Every packet class is generated from the official specification published at lfs.net, so the
|
|
11
|
+
fields are named exactly as in `InSim.txt`.
|
|
12
|
+
- Handlers with decorators, `await insim.request(...)`, chat commands, a roster of connections
|
|
13
|
+
and players, and automatic reconnection if LFS restarts.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
pip install lfs-insim
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## A first program
|
|
22
|
+
|
|
23
|
+
In LFS, type `/insim=29999` to let it listen for InSim. Then run this and type `!hello` in the
|
|
24
|
+
game:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from lfs_insim import Command, InSim
|
|
28
|
+
from lfs_insim.packets import IS_VER
|
|
29
|
+
|
|
30
|
+
insim = InSim(name="hello", prefix="!") # admin_password="..." if LFS has one
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@insim.on(IS_VER)
|
|
34
|
+
def connected(packet: IS_VER) -> None:
|
|
35
|
+
print(f"Connected to LFS {packet.Version}, InSim {packet.InSimVer}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@insim.on_command("hello")
|
|
39
|
+
def hello(command: Command) -> None:
|
|
40
|
+
command.reply("Hello from Python")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
insim.run() # blocks until Ctrl+C; reconnects on its own if LFS restarts
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## What is in the box
|
|
48
|
+
|
|
49
|
+
| You want to… | Use |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| Receive a packet | `@insim.on(IS_XXX)` |
|
|
52
|
+
| Answer a chat command | `@insim.on_command("name")` |
|
|
53
|
+
| Send packets, chat or `/commands` | `insim.send(...)`, `insim.say(...)`, `insim.command(...)` |
|
|
54
|
+
| Ask LFS and wait for the answer | `await insim.request(IS_STA)`, `request_all(...)` |
|
|
55
|
+
| Run something on a rhythm | `@insim.every(seconds)` |
|
|
56
|
+
| Know who is connected and who is driving | `lfs_insim.state.Roster` |
|
|
57
|
+
| Read car physics or the dashboard | `OutSim` and `OutGauge` |
|
|
58
|
+
| Show LFS text without colour codes | `lfs_insim.text.plain(...)` |
|
|
59
|
+
|
|
60
|
+
The guide is in [`docs/`](docs/README.md). Short, runnable programs are in [`examples/`](examples): `hello.py`, `who.py`, `buttons.py` and
|
|
61
|
+
`gauge.py`.
|
|
62
|
+
|
|
63
|
+
## Development
|
|
64
|
+
|
|
65
|
+
This repository is worked on with the notes in [`docs/dev`](docs/dev) (in Spanish). To check
|
|
66
|
+
everything: `python scripts/verify.py`.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# lfs_insim documentation
|
|
2
|
+
|
|
3
|
+
1. [Getting started](getting-started.md): install, let LFS listen, write and run a first program.
|
|
4
|
+
2. [InSim](insim.md): handlers, chat commands, sending, asking LFS, running on a rhythm, buttons.
|
|
5
|
+
3. [Who is who, and text](roster-and-text.md): `Roster`, and how LFS text (colours, escapes) arrives.
|
|
6
|
+
4. [OutSim and OutGauge](outsim-outgauge.md): the UDP data, alone or together with InSim.
|
|
7
|
+
|
|
8
|
+
Runnable programs are in [`examples/`](../examples). Every packet class and field is named as in
|
|
9
|
+
the official [InSim.txt](https://www.lfs.net/programmer/insim) specification (`IS_NPL.PName`,
|
|
10
|
+
`IS_STA.NumP`...); the editor shows them, and their docstrings come from the specification.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pip install lfs-insim
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Python 3.11 or newer. There are no dependencies.
|
|
10
|
+
|
|
11
|
+
## Let LFS listen
|
|
12
|
+
|
|
13
|
+
LFS only accepts InSim connections when you ask it to. In the game, type:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
/insim=29999
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Port 29999 is the default of `InSim`. If LFS has an admin password, pass it as
|
|
20
|
+
`InSim(admin_password="...")`.
|
|
21
|
+
|
|
22
|
+
## A first program
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from lfs_insim import Command, InSim
|
|
26
|
+
from lfs_insim.packets import IS_VER
|
|
27
|
+
|
|
28
|
+
insim = InSim(name="hello", prefix="!")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@insim.on(IS_VER)
|
|
32
|
+
def connected(packet: IS_VER) -> None:
|
|
33
|
+
print(f"Connected to LFS {packet.Version}, InSim {packet.InSimVer}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@insim.on_command("hello")
|
|
37
|
+
def hello(command: Command) -> None:
|
|
38
|
+
command.reply("Hello from Python")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
insim.run()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `InSim(name=..., prefix="!")` prepares the connection; nothing is sent yet. `name` is how your
|
|
46
|
+
program is shown in LFS (up to 15 characters). `prefix` is one character: what users type
|
|
47
|
+
before a command.
|
|
48
|
+
- `@insim.on(IS_VER)` calls the function for every `IS_VER` packet LFS sends. The same works for
|
|
49
|
+
any packet: `IS_NPL` (a player joins), `IS_LAP`, `IS_BTC` (a button click)...
|
|
50
|
+
- `@insim.on_command("hello")` calls the function when someone types `!hello`.
|
|
51
|
+
- `insim.run()` connects and dispatches packets until Ctrl+C. If LFS is not running yet, or
|
|
52
|
+
closes, it keeps trying again.
|
|
53
|
+
|
|
54
|
+
Run it, then type `!hello` in LFS.
|
|
55
|
+
|
|
56
|
+
## `def` or `async def`
|
|
57
|
+
|
|
58
|
+
Handlers may be plain functions (they run at once, so keep them quick) or `async def` (they run
|
|
59
|
+
as tasks and may `await`). An exception in a handler is logged and never stops the others.
|
|
60
|
+
|
|
61
|
+
If your program already runs an asyncio event loop, use the connection as a context manager
|
|
62
|
+
instead of `run()`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import asyncio
|
|
66
|
+
|
|
67
|
+
from lfs_insim import InSim
|
|
68
|
+
|
|
69
|
+
insim = InSim(name="inside-a-loop")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def main() -> None:
|
|
73
|
+
async with insim: # connects in the background and keeps the connection up
|
|
74
|
+
await asyncio.sleep(60)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Next
|
|
81
|
+
|
|
82
|
+
[InSim](insim.md) covers everything a connection can do.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# InSim
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
from lfs_insim import InSim
|
|
5
|
+
|
|
6
|
+
insim = InSim(name="my program", prefix="!")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The options of `InSim(...)`: `admin_password`, `host` and `port` (default `127.0.0.1:29999`),
|
|
10
|
+
`flags` (the `ISF` options, such as `ISF.MCI | ISF.LOCAL`), `interval_ms` (time between
|
|
11
|
+
`IS_MCI`/`IS_NLP` packets) and `prefix`.
|
|
12
|
+
|
|
13
|
+
## Receiving
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from lfs_insim.packets import IS_NPL
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@insim.on(IS_NPL)
|
|
20
|
+
def joined(packet: IS_NPL) -> None:
|
|
21
|
+
print(packet.PName, "is on track in a", packet.CName)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Text fields are `str`. They keep LFS's `^` codes; see [text](roster-and-text.md#text).
|
|
25
|
+
|
|
26
|
+
### Chat commands
|
|
27
|
+
|
|
28
|
+
With a `prefix`, `@insim.on_command("kick")` is called for `!kick Bob` with a `Command`:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from lfs_insim import Command
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@insim.on_command("kick")
|
|
35
|
+
def kick(command: Command) -> None:
|
|
36
|
+
# command.name == "kick", command.args == ("Bob",)
|
|
37
|
+
# command.ucid: the connection that typed it; command.plid: its player (0 if none)
|
|
38
|
+
command.reply(f"Kicking {', '.join(command.args)}") # only the sender sees this
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Names are matched in any case. Commands are hidden from the other users. A command with no
|
|
42
|
+
function is left alone, since it may be meant for another program.
|
|
43
|
+
|
|
44
|
+
### On connection, and on a rhythm
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
@insim.on_connect
|
|
48
|
+
async def start() -> None:
|
|
49
|
+
... # after every connection and reconnection: ask LFS for the initial state here
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@insim.every(2)
|
|
53
|
+
async def tick() -> None:
|
|
54
|
+
... # every 2 seconds while connected; calls never overlap
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Sending
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from lfs_insim.packets import IS_MST
|
|
61
|
+
|
|
62
|
+
insim.say("Hello everyone") # the chat; everyone sees it
|
|
63
|
+
insim.say("Just you", to=3) # only the connection with UCID 3 (0 = this computer)
|
|
64
|
+
insim.command("/restart") # an LFS command
|
|
65
|
+
insim.send(IS_MST(Msg="/end")) # any packet; several at once: insim.send(a, b)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`say()` picks the right packet for you and raises `ValueError` if the text is too long
|
|
69
|
+
(`PacketError` is a `ValueError`). `send()` raises `ConnectionError` if LFS is not connected.
|
|
70
|
+
|
|
71
|
+
## Asking LFS
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from lfs_insim.packets import IS_NCN, IS_STA
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@insim.every(5)
|
|
78
|
+
async def report() -> None:
|
|
79
|
+
state = await insim.request(IS_STA) # one packet
|
|
80
|
+
everyone = await insim.request_all(IS_NCN) # a list: one per connection
|
|
81
|
+
print(state.NumP, len(everyone))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`request` raises `TimeoutError` if LFS does not answer within 2 seconds (change it with
|
|
85
|
+
`timeout=`). The answers also reach your `@insim.on` handlers. Asking for a packet that LFS does
|
|
86
|
+
not send on request raises `TypeError` and says which function to use instead.
|
|
87
|
+
|
|
88
|
+
## Buttons
|
|
89
|
+
|
|
90
|
+
Buttons are `IS_BTN` packets; clicks come back as `IS_BTC` (and `IS_BTT` for text typed into a
|
|
91
|
+
button). Each button of your program needs its own `ClickID` (0 to 239), and sending a button
|
|
92
|
+
again with the same `ClickID` updates it. See [`examples/buttons.py`](../examples/buttons.py).
|
|
93
|
+
|
|
94
|
+
## Closing
|
|
95
|
+
|
|
96
|
+
`insim.run()` ends with Ctrl+C. From a handler or an event loop, `await insim.close()` sends the
|
|
97
|
+
goodbye to LFS and stops reconnecting.
|
|
98
|
+
|
|
99
|
+
## Without `run()`
|
|
100
|
+
|
|
101
|
+
`await insim.connect()` connects once (no reconnection) and returns; `wait_closed()` waits until
|
|
102
|
+
it ends. Use `async with insim:` or `run()` unless you need that.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# OutSim and OutGauge
|
|
2
|
+
|
|
3
|
+
Both are UDP: LFS sends them to a port of your choice, and you listen. Neither needs InSim.
|
|
4
|
+
|
|
5
|
+
## OutGauge (the dashboard)
|
|
6
|
+
|
|
7
|
+
With LFS closed, set these lines in its `cfg.txt`:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
OutGauge Mode 1
|
|
11
|
+
OutGauge IP 127.0.0.1
|
|
12
|
+
OutGauge Port 30001
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
If you also set `OutGauge ID`, LFS adds that number to every packet (`packet.ID`).
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from lfs_insim import OutGauge
|
|
19
|
+
from lfs_insim.packets import OutGaugePack
|
|
20
|
+
|
|
21
|
+
gauge = OutGauge(port=30001)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@gauge.on(OutGaugePack)
|
|
25
|
+
def dashboard(packet: OutGaugePack) -> None:
|
|
26
|
+
print(f"{packet.Speed * 3.6:.0f} km/h at {packet.RPM:.0f} rpm")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
gauge.run()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
LFS can send up to 100 packets a second. If you only need them now and then, keep the last one
|
|
34
|
+
and act on a rhythm (see [`examples/gauge.py`](../examples/gauge.py)).
|
|
35
|
+
|
|
36
|
+
## OutSim (the car's motion)
|
|
37
|
+
|
|
38
|
+
The `OutSim ...` lines of `cfg.txt` work the same way. With `OutSim Opts 0`, LFS sends
|
|
39
|
+
`OutSimPack`; with any other options, `OutSimPack2`, which carries only the parts those options
|
|
40
|
+
select. Give the same options to `OutSim`:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from lfs_insim import OutSim
|
|
44
|
+
from lfs_insim.packets import OSO, OutSimPack2
|
|
45
|
+
|
|
46
|
+
outsim = OutSim(port=30002, options=OSO.MAIN | OSO.INPUTS)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@outsim.on(OutSimPack2)
|
|
50
|
+
def motion(packet: OutSimPack2) -> None:
|
|
51
|
+
print(packet) # the parts that were not sent are None
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The options must match `OutSim Opts` in `cfg.txt`, written in hexadecimal there. If a datagram
|
|
55
|
+
cannot be read, the library logs one warning pointing at `cfg.txt` and skips it.
|
|
56
|
+
|
|
57
|
+
## Together with InSim
|
|
58
|
+
|
|
59
|
+
`run()` starts several sources in one process and one event loop:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from lfs_insim import InSim, OutGauge, run
|
|
63
|
+
|
|
64
|
+
insim = InSim(name="both")
|
|
65
|
+
gauge = OutGauge(port=30001)
|
|
66
|
+
|
|
67
|
+
run(insim, gauge)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If the port is already taken, starting raises `OSError`. Port 30000 is often in use by other
|
|
71
|
+
programs; pick another.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Who is who, and text
|
|
2
|
+
|
|
3
|
+
## Roster
|
|
4
|
+
|
|
5
|
+
Almost every program needs to know who is connected and who is driving. `Roster` follows the
|
|
6
|
+
packets for you:
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from lfs_insim import InSim
|
|
10
|
+
from lfs_insim.packets import IS_NPL
|
|
11
|
+
from lfs_insim.state import Roster
|
|
12
|
+
|
|
13
|
+
insim = InSim(name="who")
|
|
14
|
+
roster = Roster(insim) # create it before running the connection
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@insim.on(IS_NPL)
|
|
18
|
+
def joined(packet: IS_NPL) -> None:
|
|
19
|
+
player = roster.players[packet.PLID]
|
|
20
|
+
print(player.name, "is driven from", player.connection.username or "this computer")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `roster.connections` maps UCID to a `Connection` (`username`, `name`, `admin`, `remote`,
|
|
24
|
+
`players`).
|
|
25
|
+
- `roster.players` maps PLID to a `Player` (`name`, `plate`, `car`, `is_ai`, `in_pits`,
|
|
26
|
+
`connection`). An AI belongs to the connection that added it.
|
|
27
|
+
- On every connection and reconnection it asks LFS for everyone, so it starts complete.
|
|
28
|
+
- It is already up to date in your handlers: when someone arrives it is updated before them, and
|
|
29
|
+
when someone leaves (`IS_PLL`, `IS_CNL`) after them, so you can still look up who is leaving.
|
|
30
|
+
- It is read only: change things in LFS, not in the roster.
|
|
31
|
+
|
|
32
|
+
See [`examples/who.py`](../examples/who.py).
|
|
33
|
+
|
|
34
|
+
## Text
|
|
35
|
+
|
|
36
|
+
Text fields (`PName`, `Msg`, ...) arrive as `str`, with LFS's `^` codes kept as LFS sends them:
|
|
37
|
+
colours (`^1`, `^7`...) and escaped characters (`^v` is `|`, `^h` is `#`, `^^` is `^`). The
|
|
38
|
+
library has already turned LFS's code-page bytes into the right characters, so accents and other
|
|
39
|
+
alphabets just work.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from lfs_insim.text import plain, strip_colours
|
|
43
|
+
|
|
44
|
+
plain("^4^h773 ^7Bob") # '#773 Bob' to print or save
|
|
45
|
+
strip_colours("^1Red ^7car") # 'Red car' only the colours
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `plain()` to show text outside LFS. Do not send its result back to LFS: the escapes are
|
|
49
|
+
gone, so send the original.
|
|
50
|
+
|
|
51
|
+
`IS_MSO.text` is the message without the player's name in front of it.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Buttons in LFS: one to click, one to type into. Type !buttons in LFS to show them, !hide to
|
|
2
|
+
remove them.
|
|
3
|
+
|
|
4
|
+
LFS must be listening for InSim: type /insim=29999 in LFS first.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from lfs_insim import Command, InSim
|
|
8
|
+
from lfs_insim.packets import BFN, IS_BFN, IS_BTC, IS_BTN, IS_BTT, ISB
|
|
9
|
+
|
|
10
|
+
insim = InSim(name="buttons", prefix="!")
|
|
11
|
+
|
|
12
|
+
CLICK_ME, TYPE_HERE = 1, 2 # ClickID: each button of this program needs its own (0 to 239)
|
|
13
|
+
clicks = 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def show(ucid: int) -> None:
|
|
17
|
+
"""Send both buttons to a connection. Sending one again with its ClickID updates it."""
|
|
18
|
+
insim.send(
|
|
19
|
+
IS_BTN(
|
|
20
|
+
ReqI=1, # LFS needs it non-zero
|
|
21
|
+
UCID=ucid, # 0 = this computer, 255 = everyone
|
|
22
|
+
ClickID=CLICK_ME,
|
|
23
|
+
BStyle=ISB.CLICK | ISB.LIGHT,
|
|
24
|
+
L=70, # position and size: the screen is 200 x 200
|
|
25
|
+
T=40,
|
|
26
|
+
W=60,
|
|
27
|
+
H=8,
|
|
28
|
+
Text=f"Clicked {clicks} times",
|
|
29
|
+
),
|
|
30
|
+
IS_BTN(
|
|
31
|
+
ReqI=1,
|
|
32
|
+
UCID=ucid,
|
|
33
|
+
ClickID=TYPE_HERE,
|
|
34
|
+
BStyle=ISB.CLICK | ISB.DARK,
|
|
35
|
+
TypeIn=20, # clicking opens a text box for up to 20 characters
|
|
36
|
+
L=70,
|
|
37
|
+
T=50,
|
|
38
|
+
W=60,
|
|
39
|
+
H=8,
|
|
40
|
+
Text="Type something",
|
|
41
|
+
Caption="Your text",
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@insim.on_command("buttons")
|
|
47
|
+
def buttons(command: Command) -> None:
|
|
48
|
+
show(command.ucid)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@insim.on_command("hide")
|
|
52
|
+
def hide(command: Command) -> None:
|
|
53
|
+
insim.send(IS_BFN(SubT=BFN.CLEAR, UCID=command.ucid))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@insim.on(IS_BTC)
|
|
57
|
+
def clicked(packet: IS_BTC) -> None:
|
|
58
|
+
global clicks
|
|
59
|
+
if packet.ClickID == CLICK_ME:
|
|
60
|
+
clicks += 1
|
|
61
|
+
show(packet.UCID)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@insim.on(IS_BTT)
|
|
65
|
+
def typed(packet: IS_BTT) -> None:
|
|
66
|
+
if packet.ClickID == TYPE_HERE:
|
|
67
|
+
insim.say(f"You typed: {packet.Text}", to=packet.UCID)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
insim.run()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""A fixed rhythm, async and OutGauge at once: every 2 s, show the speed of your car in LFS.
|
|
2
|
+
|
|
3
|
+
LFS must be listening for InSim (type /insim=29999 in LFS) and sending OutGauge: with LFS
|
|
4
|
+
closed, set these lines in its cfg.txt: "OutGauge Mode 1" and "OutGauge Port 30001".
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from lfs_insim import InSim, OutGauge, run
|
|
8
|
+
from lfs_insim.packets import IS_MSL, IS_STA, OutGaugePack
|
|
9
|
+
|
|
10
|
+
insim = InSim(name="gauge")
|
|
11
|
+
gauge = OutGauge(port=30001) # UDP; port 30000 is often taken by other programs
|
|
12
|
+
|
|
13
|
+
latest: OutGaugePack | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@gauge.on(OutGaugePack)
|
|
17
|
+
def dashboard(packet: OutGaugePack) -> None:
|
|
18
|
+
global latest
|
|
19
|
+
latest = packet # up to 100 a second: keep the last one, and act on a rhythm
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@insim.every(2)
|
|
23
|
+
async def show() -> None:
|
|
24
|
+
state = await insim.request(IS_STA) # waits for LFS without stopping anything else
|
|
25
|
+
if latest is None:
|
|
26
|
+
text = "No OutGauge data yet: is a car on track?"
|
|
27
|
+
else:
|
|
28
|
+
text = f"{latest.Speed * 3.6:.0f} km/h at {latest.RPM:.0f} rpm"
|
|
29
|
+
insim.send(IS_MSL(Msg=f"{text} ({state.NumP} in the race)")) # only on this computer
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
run(insim, gauge) # one program, both sources
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Answer a chat command: type !hello in LFS and it answers you.
|
|
2
|
+
|
|
3
|
+
LFS must be listening for InSim: type /insim=29999 in LFS first.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from lfs_insim import Command, InSim
|
|
7
|
+
from lfs_insim.packets import IS_VER
|
|
8
|
+
|
|
9
|
+
insim = InSim(name="hello", prefix="!") # admin_password="..." if LFS has one
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@insim.on(IS_VER)
|
|
13
|
+
def connected(packet: IS_VER) -> None:
|
|
14
|
+
print(f"Connected to LFS {packet.Version}, InSim {packet.InSimVer}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@insim.on_command("hello")
|
|
18
|
+
def hello(command: Command) -> None:
|
|
19
|
+
command.reply("Hello from Python")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
insim.run() # blocks until Ctrl+C; reconnects on its own if LFS restarts
|