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.
Files changed (67) hide show
  1. lfs_insim-0.1.0/.gitignore +22 -0
  2. lfs_insim-0.1.0/CHANGELOG.md +14 -0
  3. lfs_insim-0.1.0/LICENSE +21 -0
  4. lfs_insim-0.1.0/PKG-INFO +94 -0
  5. lfs_insim-0.1.0/README.md +70 -0
  6. lfs_insim-0.1.0/docs/README.md +10 -0
  7. lfs_insim-0.1.0/docs/getting-started.md +82 -0
  8. lfs_insim-0.1.0/docs/insim.md +102 -0
  9. lfs_insim-0.1.0/docs/outsim-outgauge.md +71 -0
  10. lfs_insim-0.1.0/docs/roster-and-text.md +51 -0
  11. lfs_insim-0.1.0/examples/buttons.py +71 -0
  12. lfs_insim-0.1.0/examples/gauge.py +33 -0
  13. lfs_insim-0.1.0/examples/hello.py +23 -0
  14. lfs_insim-0.1.0/examples/who.py +38 -0
  15. lfs_insim-0.1.0/pyproject.toml +67 -0
  16. lfs_insim-0.1.0/scripts/close_check.py +359 -0
  17. lfs_insim-0.1.0/scripts/ctx_log_last.py +63 -0
  18. lfs_insim-0.1.0/scripts/ctx_plan_active.py +80 -0
  19. lfs_insim-0.1.0/scripts/fetch_spec.py +194 -0
  20. lfs_insim-0.1.0/scripts/generate_enums.py +140 -0
  21. lfs_insim-0.1.0/scripts/generate_packets.py +434 -0
  22. lfs_insim-0.1.0/scripts/session_start_check.py +268 -0
  23. lfs_insim-0.1.0/scripts/spec_overrides.py +136 -0
  24. lfs_insim-0.1.0/scripts/spec_parser.py +291 -0
  25. lfs_insim-0.1.0/scripts/verify.py +113 -0
  26. lfs_insim-0.1.0/spec/checked.json +12 -0
  27. lfs_insim-0.1.0/src/lfs_insim/__init__.py +12 -0
  28. lfs_insim-0.1.0/src/lfs_insim/_commands.py +70 -0
  29. lfs_insim-0.1.0/src/lfs_insim/_connection.py +406 -0
  30. lfs_insim-0.1.0/src/lfs_insim/_handlers.py +81 -0
  31. lfs_insim-0.1.0/src/lfs_insim/_requests.py +131 -0
  32. lfs_insim-0.1.0/src/lfs_insim/_run.py +47 -0
  33. lfs_insim-0.1.0/src/lfs_insim/_stream.py +56 -0
  34. lfs_insim-0.1.0/src/lfs_insim/_udp.py +183 -0
  35. lfs_insim-0.1.0/src/lfs_insim/packets/__init__.py +295 -0
  36. lfs_insim-0.1.0/src/lfs_insim/packets/_codec.py +525 -0
  37. lfs_insim-0.1.0/src/lfs_insim/packets/_enums.py +1712 -0
  38. lfs_insim-0.1.0/src/lfs_insim/packets/_extras.py +24 -0
  39. lfs_insim-0.1.0/src/lfs_insim/packets/_insim.py +2807 -0
  40. lfs_insim-0.1.0/src/lfs_insim/packets/_outgauge.py +99 -0
  41. lfs_insim-0.1.0/src/lfs_insim/packets/_outsim.py +261 -0
  42. lfs_insim-0.1.0/src/lfs_insim/packets/_text.py +142 -0
  43. lfs_insim-0.1.0/src/lfs_insim/packets/_types.py +29 -0
  44. lfs_insim-0.1.0/src/lfs_insim/py.typed +0 -0
  45. lfs_insim-0.1.0/src/lfs_insim/state.py +167 -0
  46. lfs_insim-0.1.0/src/lfs_insim/text.py +51 -0
  47. lfs_insim-0.1.0/tests/fake_lfs.py +210 -0
  48. lfs_insim-0.1.0/tests/lfs_screen.ps1 +59 -0
  49. lfs_insim-0.1.0/tests/live_lfs.py +385 -0
  50. lfs_insim-0.1.0/tests/test_codec.py +225 -0
  51. lfs_insim-0.1.0/tests/test_commands.py +152 -0
  52. lfs_insim-0.1.0/tests/test_docs.py +42 -0
  53. lfs_insim-0.1.0/tests/test_every.py +242 -0
  54. lfs_insim-0.1.0/tests/test_examples.py +148 -0
  55. lfs_insim-0.1.0/tests/test_fake_lfs.py +147 -0
  56. lfs_insim-0.1.0/tests/test_fetch_spec.py +71 -0
  57. lfs_insim-0.1.0/tests/test_insim.py +489 -0
  58. lfs_insim-0.1.0/tests/test_package.py +12 -0
  59. lfs_insim-0.1.0/tests/test_packets.py +379 -0
  60. lfs_insim-0.1.0/tests/test_readme.py +15 -0
  61. lfs_insim-0.1.0/tests/test_request.py +143 -0
  62. lfs_insim-0.1.0/tests/test_roster.py +258 -0
  63. lfs_insim-0.1.0/tests/test_say.py +109 -0
  64. lfs_insim-0.1.0/tests/test_spec_parser.py +168 -0
  65. lfs_insim-0.1.0/tests/test_stream.py +73 -0
  66. lfs_insim-0.1.0/tests/test_text.py +175 -0
  67. 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.
@@ -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.
@@ -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