A React renderer for InSim buttons, based on Node InSim.
Warning
This project is still under development. Any API may change as needed.
React Node InSim is a React renderer for Live for Speed InSim buttons. It also provides layout components for easier button positioning, hooks for handling incoming InSim packets and tracking server connections & players.
It is based on Node InSim, a Node.js library, written in TypeScript, for InSim communication.
It allows you to create things like this:
Show source code
import { InSim } from 'node-insim';
import { InSimFlags, IS_BTC, IS_MST, PacketType } from 'node-insim/packets';
import { StrictMode } from 'react';
import {
Button,
ConnectionsPlayersProvider,
createRoot,
useConnections,
useOnConnect,
useOnPacket,
usePlayers,
VStack,
} from 'react-node-insim';
function App() {
// Get the list of current players and connections
const players = usePlayers();
const connections = useConnections();
// Do something after the InSim app has been connected to LFS
useOnConnect((packet, inSim) => {
console.log(`Connected to LFS ${packet.Product} ${packet.Version}`);
inSim.send(new IS_MST({ Msg: `React Node InSim connected` }));
});
// Handle incoming packets
useOnPacket(PacketType.ISP_NCN, (packet) => {
console.log(`New connection: ${packet.UName}`);
});
// Clickable buttons
const handlePlayerClick = (plid: number) => (_: IS_BTC, inSim: InSim) => {
inSim.send(new IS_MST({ Msg: `/echo PLID ${plid}` }));
};
const handleConnectionClick = (ucid: number) => (_: IS_BTC, inSim: InSim) => {
inSim.send(new IS_MST({ Msg: `/echo UCID ${ucid}` }));
};
return (
<>
<Button top={10} left={40} width={30} height={5} UCID={255} color="title">
Players
</Button>
<VStack
background="dark"
top={15}
left={40}
width={30}
height={5}
UCID={255}
>
{players.map((player) => (
<Button key={player.PLID} onClick={handlePlayerClick(player.PLID)}>
{player.PName}
</Button>
))}
</VStack>
<Button top={10} left={70} width={30} height={5} UCID={255} color="title">
Connections
</Button>
<VStack
background="dark"
top={15}
left={70}
width={30}
height={5}
UCID={255}
>
{connections.map((connection) => (
<Button
key={connection.UCID}
onClick={handleConnectionClick(connection.UCID)}
>
{connection.UName}
</Button>
))}
</VStack>
</>
);
}
const inSim = new InSim();
inSim.connect({
IName: 'React InSim',
ReqI: 1,
Host: '127.0.0.1',
Port: 29999,
Flags: InSimFlags.ISF_LOCAL,
});
const root = createRoot(inSim);
root.render(
<StrictMode>
<ConnectionsPlayersProvider>
<App />
</ConnectionsPlayersProvider>
</StrictMode>,
);- Requirements
- Installation
- Basic usage
- Button
- Horizontal stack
- Vertical stack
- Flex
- Grid
- Toggle button
- Toggle button group
- Text box
- Hooks
- Scopes
- Using React Devtools
- Development
- Node.js 14 or higher
- Node InSim
- React 18
npm install react@18 node-insim react-node-insimyarn add react@18 node-insim react-node-insimpnpm add react@18 node-insim react-node-insimDisplaying an InSim button on a local computer
import { InSim } from 'node-insim';
import { InSimFlags } from 'node-insim/packets';
import { Button, createRoot } from 'react-node-insim';
const inSim = new InSim();
inSim.connect({
IName: 'React InSim',
ReqI: 1,
Host: '127.0.0.1',
Port: 29999,
Flags: InSimFlags.ISF_LOCAL,
});
const root = createRoot(inSim);
root.render(
<Button top={100} left={80} width={30} height={10}>
Hello InSim!
</Button>,
);You can use React hooks as usual to display stateful data via InSim.
import { InSimFlags } from 'node-insim/packets';
import { useEffect, useState } from 'react';
import { Button, createRoot } from 'react-node-insim';
function App() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date());
}, 1000);
return () => {
clearInterval(interval);
};
});
return (
<Button top={100} left={80} width={40} height={10}>
Current time: {time.toLocaleTimeString()}
</Button>
);
}
const inSim = new InSim();
inSim.connect({
IName: 'React InSim',
ReqI: 1,
Host: '127.0.0.1',
Port: 29999,
Flags: InSimFlags.ISF_LOCAL,
});
const root = createRoot(inSim);
root.render(<App />);The Button component is used to display a button in LFS.
- Buttons are drawn on a 200 by 200 canvas using absolute positioning
- The maximum number of rendered buttons on a screen is 240
import { Button } from 'react-node-insim';<Button top={100} left={80} width={30} height={10}>
Button
</Button>Buttons use XY coordinates to position themselves on the screen. The top and left props control the button's X and Y position on the screen. The allowed range of values is 0 to 200.
<>
<Button width={12} height={6} top={100} left={40}>
Button
</Button>
<Button width={12} height={6} top={100} left={53}>
Button
</Button>
<Button width={12} height={6} top={106} left={40}>
Button
</Button>
<Button width={12} height={6} top={106} left={53}>
Button
</Button>
</>Use the width and height props to change the dimensions of the button. The allowed range of values is 0 to 200.
<>
<Button variant="light" top={100} left={40} width={6} height={4}>
Button
</Button>
<Button variant="light" top={99} left={47} width={10} height={6}>
Button
</Button>
<Button variant="light" top={97} left={58} width={14} height={10}>
Button
</Button>
</>Use the variant prop to change the button's visual style. You can use light or dark. If you don't specify a variant, the button will have transparent background and a light gray text color.
<>
<Button top={100} left={40} width={12} height={6} variant="light">
Button
</Button>
<Button top={100} left={53} width={12} height={6} variant="dark">
Button
</Button>
</>Use the color prop to customize the button's text color. If you don't specify a color, the button text will be default.
<>
<Button top={73} left={40} width={12} height={6} color="default">
default
</Button>
<Button top={73} left={53} width={12} height={6} color="title">
title
</Button>
<Button top={73} left={66} width={12} height={6} color="unselected">
unselected
</Button>
<Button top={73} left={79} width={12} height={6} color="selected">
selected
</Button>
<Button top={80} left={40} width={12} height={6} color="ok">
ok
</Button>
<Button top={80} left={53} width={12} height={6} color="cancel">
cancel
</Button>
<Button top={80} left={66} width={12} height={6} color="textstring">
textstring
</Button>
<Button top={80} left={79} width={12} height={6} color="unavailable">
unavailable
</Button>
</>You can choose from a set of semantic colors or use one of the colors from the LFS color palette.
Semantic colors
- default
- title
- unselected
- selected
- ok
- cancel
- textstring
- unavailable
Note: The semantic color values can be customized in LFS Options -> Display -> Interface.
LFS color palette
- black
- red
- green
- yellow
- blue
- magenta
- cyan
- white
Use the background prop to customize the button's background color. If you don't specify a color, the background will be transparent.
<>
<Button top={67} left={40} width={12} height={6} background="light">
light
</Button>
<Button top={67} left={53} width={12} height={6} background="dark">
dark
</Button>
<Button top={67} left={66} width={12} height={6} background="transparent">
transparent
</Button>
</>HStack displays buttons in a column without having to specify each button's position manually. You can also override button colors and sizes.
import { HStack } from 'react-node-insim';<HStack top={10} left={20} width={7} height={4} variant="dark">
<Button>Stacked button</Button>
<Button color="title">Custom color</Button>
<Button height={6}>Custom height</Button>
</HStack>VStack displays buttons in a row without having to specify each button's position manually. You can also override button colors and sizes.
import { VStack } from 'react-node-insim';<VStack top={10} left={20} width={7} height={4} variant="dark">
<Button>Stacked button</Button>
<Button color="title">Custom color</Button>
<Button height={6}>Custom height</Button>
</VStack>Flex layout displays buttons in a row or column with flexbox options.
import { Flex } from 'react-node-insim';<Flex
top={10}
left={20}
width={36}
height={16}
alignItems="center"
justifyContent="space-evenly"
background="dark"
backgroundColor="light"
>
<Button width={8} height={4}>
Left
</Button>
<Button width={10} height={6}>
Center
</Button>
<Button width={8} height={4}>
Right
</Button>
</Flex>Grid layout displays buttons in a grid.
import { Grid, GridButton } from 'react-node-insim';<Grid
top={30}
left={40}
width={30}
height={30}
background="dark"
backgroundColor="light"
gridTemplateColumns="1fr 2fr 1fr"
gridTemplateRows="1fr 3fr 2fr"
gridColumnGap={1}
gridRowGap={1}
>
<GridButton>1</GridButton>
<GridButton
gridColumnStart={2}
gridRowStart={1}
gridRowEnd={3}
color="title"
background="light"
>
2
</GridButton>
<GridButton
gridColumnStart={3}
gridColumnEnd={3}
gridRowStart={1}
gridRowEnd={3}
>
3
</GridButton>
<GridButton alignSelf="end" height={10}>
4
</GridButton>
<GridButton gridColumnStart={1} gridColumnEnd={4}>
5
</GridButton>
</Grid>A button that can be toggled on and off by clicking it.
import { ToggleButton } from 'react-node-insim';function App() {
const [isOn, setIsOn] = useState(false);
return (
<ToggleButton
top={100}
left={80}
width={12}
height={6}
isOn={isOn}
onToggle={setIsOn}
>
Toggle
</ToggleButton>
);
}Use the variant prop to change the button's background style. You can use light or dark. If you don't specify a variant, light will be used.
<>
<ToggleButton variant="light" top={100} left={40} width={12} height={6}>
Toggle
</ToggleButton>
<ToggleButton variant="dark" top={100} left={53} width={12} height={6}>
Toggle
</ToggleButton>
</>Use the isDisabled prop to prevent toggling the button on/off.
<>
<ToggleButton
isDisabled={false}
variant="light"
top={100}
left={40}
width={12}
height={6}
>
Enabled
</ToggleButton>
<ToggleButton
isDisabled
variant="light"
top={100}
left={53}
width={12}
height={6}
>
Disabled
</ToggleButton>
<ToggleButton
isDisabled={false}
variant="dark"
top={106}
left={40}
width={12}
height={6}
>
Enabled
</ToggleButton>
<ToggleButton
isDisabled
variant="dark"
top={106}
left={53}
width={12}
height={6}
>
Disabled
</ToggleButton>
</>A group of buttons that can be toggled on and off by clicking them.
import { ToggleButtonGroup } from 'react-node-insim';const options = [
{ label: 'low', value: 1 },
{ label: 'medium', value: 2 },
{ label: 'high', value: 3 },
];
function App() {
const [selectedOption, setSelectedOption] = useState(options[0]);
return (
<ToggleButtonGroup
top={30}
left={50}
width={36}
height={6}
options={options}
selectedOption={selectedOption}
onChange={setSelectedOption}
/>
);
}A text box whose content can span multiple rows. If the content is too long, the text box will show a scrollbar.
import { TextBox } from 'react-node-insim';<TextBox
top={40}
left={50}
cols={20}
rows={4}
width={20}
rowHeight={4}
variant="light"
>
Hello world this is a text box lorem ipsum dolor sit amet consectetur
adipisicing elitrea lorem ipsum dolor sit amet consectetur adipisicing elit
</TextBox>Execute code after the InSim app has been connected.
The first parameter is an IS_VER packet callback executed when IS_VER is received upon successful InSim connection to LFS.
import { useOnConnect } from 'react-node-insim';
function App() {
useOnConnect((packet, inSim) => {
console.log(`Connected to LFS ${packet.Product} ${packet.Version}`);
inSim.send(new IS_MST({ Msg: `React Node InSim connected` }));
});
return null;
}Execute code after the InSim app has been disconnected.
The first parameter is the "disconnect" event callback from Node InSim.
import { useOnDisconnect } from 'react-node-insim';
function App() {
useOnDisconnect(() => {
console.log('Disconnected from LFS');
});
return null;
}Execute code when an InSim packet is received
import { useOnPacket } from 'react-node-insim';
function App() {
useOnPacket(PacketType.ISP_NCN, (packet) => {
console.log(`New connection: ${packet.UName}`);
});
return null;
}Get a live list of all connected guests.
import { useConnections } from 'react-node-insim';
function App() {
const connections = useConnections();
return (
<VStack background="dark" top={10} left={10} width={20} height={4}>
{connections.map((connection) => (
<Button key={connection.UCID}>{connection.UName}</Button>
))}
</VStack>
);
}Get a live list of all players on track.
import { usePlayers } from 'react-node-insim';
function App() {
const players = usePlayers();
return (
<VStack background="dark" top={10} left={10} width={20} height={4}>
{players.map((player) => (
<Button key={player.PLID}>{player.PName}</Button>
))}
</VStack>
);
}Send a race control message (RCM) to a connection or a player.
import { useRaceControlMessage } from 'react-node-insim';
function App() {
const { sendRaceControlMessageToConnection, sendRaceControlMessageToPlayer } =
useRaceControlMessage();
return (
<>
<Button
top={5}
left={10}
width={15}
height={5}
onClick={(packet) => {
sendRaceControlMessageToConnection(
packet.UCID,
'Hello from React Node InSim',
2000,
);
}}
>
Send message to a connection
</Button>
<Button
top={10}
left={10}
width={15}
height={5}
onClick={(packet) => {
sendRaceControlMessageToPlayer(
12, // PLID
'Hello from React Node InSim',
2000,
);
}}
>
Send message to a player
</Button>
</>
);
}Access to Node InSim API of the current InSim client instance.
import { useInSim } from 'react-node-insim';
function App() {
const inSim = useInSim();
useEffect(() => {
inSim.send(new IS_MST({ Msg: 'App mounted' }));
}, []);
return null;
}If you needed to show personalised buttons for each connection or each human player on track, you would need to map over the list of connections/players and pass the correct UCIDs to each button manually. Scopes help in such use cases.
You can show different buttons to each connection by wrapping a sub-tree in a ConnectionScopeProvider, then using the useConnectionScope hook anywhere within that sub-tree to access the connection object.
You don't need to specify the button's UCID in the scope - the correct UCID will be injected automatically.
import {
Button,
ConnectionScopeProvider,
useConnectionScope,
} from 'react-node-insim';
function App() {
return (
<ConnectionScopeProvider>
<UserNameButton />
</ConnectionScopeProvider>
);
}
function UserNameButton() {
const { UName } = useConnectionScope();
return (
<Button top={0} left={80} height={5} width={25}>
{UName}
</Button>
);
}You can show different buttons to each human player on track by wrapping a sub-tree in a HumanPlayerScopeProvider, then using the useHumanPlayerScope hook anywhere within that sub-tree to access the player object.
You don't need to specify the button's UCID in the scope - the correct UCID will be injected automatically.
import {
Button,
HumanPlayerScopeProvider,
useHumanPlayerScope,
} from 'react-node-insim';
function App() {
return (
<HumanPlayerScopeProvider>
<PlayerNameButton />
</HumanPlayerScopeProvider>
);
}
function UserNameButton() {
const { PName } = useHumanPlayerScope();
return (
<Button top={0} left={80} height={5} width={25}>
{PName}
</Button>
);
}You can show the same set of buttons to all connections wrapping a sub-tree in a GlobalScopeProvider.
You don't need to specify the button's UCID in the scope - the correct UCID value of 255 will be injected automatically.
import { Button, GlobalScopeProvider } from 'react-node-insim';
function App() {
return (
<GlobalScopeProvider>
<Button top={0} left={80} height={5} width={40}>
React Node InSim
</Button>
</GlobalScopeProvider>
);
}React Node InSim supports React Devtools out of the box. To enable integration with React Devtools in your application, first ensure you have installed the optional react-devtools-core dependency, and then run your app with the DEV=true environment variable:
DEV=true npm startThen, start React Devtools itself:
npx react-devtoolsAfter it starts, you should see the component tree of your InSim app. You can even inspect and change the props of components, and see the results immediately in LFS, without restarting it.
yarnyarn startyarn lintyarn format
















