diff --git a/plan.txt b/plan.txt
index eebd8b1..0c202c7 100644
--- a/plan.txt
+++ b/plan.txt
@@ -1,10 +1,12 @@
 Open:
-- Refactor input messages for more than one player
+- Refactor input generics to distinct local/full types
+- Multiplayer loopback server
 - Insecured websocket server implementation
 - Cloneable RNG that goes in state (use MurmurHash3 finalizer in counter mode?)
 - remove all random() calls
 
 Done:
+- Refactor input messages for more than one player
 - Rework State implementation for easier cloning/deserialization
 - Test Lockstep/rollback
 - Smarter typings for Join/Lookup functions
diff --git a/src/Ecs/Lockstep.ts b/src/Ecs/Lockstep.ts
index cb959d6..711b76b 100644
--- a/src/Ecs/Lockstep.ts
+++ b/src/Ecs/Lockstep.ts
@@ -2,15 +2,16 @@
 export const INPUT_FREQUENCY = 33; // roughly 30fps
 
 export interface LockstepProcessor<Input, State> {
-    compareInput(a: Input, b: Input): boolean;
+    compareInput(a: Input[], b: Input[]): boolean;
+    predictInput(prev: Input[] | null, localPlayer: number, localInput: Input): Input[];
     cloneState(source: State): State;
-    advanceState(state: State, input: Input): void;
+    advanceState(state: State, input: Input[]): void;
 }
 
 export class LockstepState<Input, State> {
 
     private inputIndex = -1;
-    private inputLog: Input[] = [];
+    private inputLog: Input[][] = [];
     private canonIndex = -1;
     private canonState: State;
     private renderIndex = -1;
@@ -21,7 +22,7 @@ export class LockstepState<Input, State> {
         this.renderState = engine.cloneState(initialState);
     }
 
-    public addCanonInput(input: Input): void {
+    public addCanonInput(input: Input[]): void {
         this.canonIndex++;
 
         // advance canonical game state
@@ -37,13 +38,14 @@ export class LockstepState<Input, State> {
         this.inputLog[this.canonIndex] = input;
     }
 
-    public addLocalInput(input: Input): void {
+    /** Warning: this only supports one player input per frame. There is no support for two players using the same lockstep simulation instance. */
+    public addLocalInput(player: number, input: Input): void {
         this.inputIndex++;
 
         // ensure that we don't overwrite the canon input with local input somehow
         // (probably only possible in situations where game is unplayable anyways? but still for sanity.)
         if(this.inputIndex > this.canonIndex) {
-            this.inputLog[this.inputIndex] = input;
+            this.inputLog[this.inputIndex] = this.engine.predictInput(this.inputLog[this.inputIndex - 1] ?? null, player, input);
         }
     }
 
@@ -69,7 +71,7 @@ export class Playback<Input, State> {
 
     public constructor(
         private state: State,
-        private inputLog: Input[],
+        private inputLog: Input[][],
         private engine: LockstepProcessor<Input, State>,
     ) {}
 
diff --git a/src/Game/GameComponents.ts b/src/Game/GameComponents.ts
index b6460ad..a289019 100644
--- a/src/Game/GameComponents.ts
+++ b/src/Game/GameComponents.ts
@@ -23,7 +23,7 @@ export class World {
     phase = GamePhase.TITLE;
     score = 0;
 
-    constructor() {}
+    constructor() { }
 
     /*
      * Drawing Layers
@@ -50,9 +50,9 @@ export class World {
 
             const score = `Score: ${this.score}`;
             cx.fillStyle = "#000";
-            cx.fillText(score, this.width/3 + 1, this.height - 18 + 1, this.width/4);
+            cx.fillText(score, this.width / 3 + 1, this.height - 18 + 1, this.width / 4);
             cx.fillStyle = "#0ff";
-            cx.fillText(score, this.width/3, this.height - 18, this.width/4);
+            cx.fillText(score, this.width / 3, this.height - 18, this.width / 4);
         }));
     }
 }
@@ -71,6 +71,7 @@ export class Data extends EcsData implements StateForSchema<GameSchema> {
     hp: Store<Hp>;
     lifetime: Store<Lifetime>;
     message: Store<Message>;
+    playerControl: Store<PlayerControl>;
 
     // globals
     debugLayer = new Layer(2);
@@ -82,6 +83,7 @@ export class Data extends EcsData implements StateForSchema<GameSchema> {
         this.hp = copySparse(from.hp);
         this.lifetime = copySparse(from.lifetime);
         this.message = copySparse(from.message);
+        this.playerControl = copySparse(from.playerControl);
     }
 
     clone() {
@@ -124,7 +126,7 @@ export class Hp extends Component<Hp> {
 
 export class Lifetime extends Component<Lifetime> {
     time: number;
-    constructor(from: Partial<Lifetime> & {time: number}) {
+    constructor(from: Partial<Lifetime> & { time: number }) {
         super(from);
         this.time = from.time;
     }
@@ -163,35 +165,55 @@ export class Message extends Component<Message> {
     }
 }
 
+export class PlayerControl extends Component<PlayerControl> {
+    playerNumber: number;
+    constructor(from: Partial<PlayerControl> & {playerNumber: number}) {
+        super(from);
+        this.playerNumber = from.playerNumber;
+    }
+    clone(): PlayerControl {
+        return new PlayerControl(this);
+    }
+}
+
 export class Engine implements LockstepProcessor<KeyName[], Data> {
     cloneState(old: Data) {
         return new Data(old);
     }
 
-    compareInput(a: KeyName[], b: KeyName[]): boolean {
-        if (a.length != b.length) return false;
-
-        let matches = true;
-        a.forEach((keyA, i) => {
-            if (keyA != b[i]) {
-                matches = false;
-            }
-        });
-
-        return matches;
+    predictInput(prev: KeyName[][] | null, localPlayer: number, localInput: KeyName[]): KeyName[][] {
+        return prev?.map((prevInput, player) => (player == localPlayer) ? localInput : prevInput) ?? [];
     }
 
-    advanceState(state: Data, input: KeyName[]) {
+    compareInput(a: KeyName[][], b: KeyName[][]): boolean {
+        if (a.length != b.length) return false;
+
+        for (let i = 0; i < a.length; i++) {
+            if (a[i].length != b[i].length) return false;
+            for (let j = 0; j < a[i].length; j++) {
+                if (a[i][j] != b[i][j]) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    advanceState(state: Data, input: KeyName[][]) {
         DumbMotion(state, INPUT_FREQUENCY);
-        Join(state, "location").forEach(([location]) => {
-            let dir = 0;
-            if(input.indexOf("left") != -1) {
-                dir -= 1;
+        Join(state, "playerControl", "location").forEach(([player, location]) => {
+            const playerInput = input[player.playerNumber];
+            if(playerInput) {
+                let dir = 0;
+                if (playerInput.indexOf("left") != -1) {
+                    dir -= 1;
+                }
+                if (playerInput.indexOf("right") != -1) {
+                    dir += 1;
+                }
+                location.VAngle = dir * 0.01;
             }
-            if(input.indexOf("right") != -1) {
-                dir += 1;
-            }
-            location.VAngle = dir * 0.01;
         });
     }
 }
diff --git a/src/Game/Main.ts b/src/Game/Main.ts
index 7e477af..28fb918 100644
--- a/src/Game/Main.ts
+++ b/src/Game/Main.ts
@@ -8,7 +8,7 @@ import { Create } from "../Ecs/Data";
 import { RunRenderBounds } from "../Ecs/Renderers";
 import { LockstepClient } from "../Net/LockstepClient";
 import { Loopback } from "../Net/LoopbackServer";
-import { Data, Engine } from "./GameComponents";
+import { Data, Engine, PlayerControl } from "./GameComponents";
 import { Buttons } from "./Input";
 
 export class Main extends LockstepClient<KeyName[], Data> {
@@ -43,8 +43,11 @@ export class Main extends LockstepClient<KeyName[], Data> {
         const newState = new Data(patch);
 
         Create(newState, {
+            playerControl: new PlayerControl({
+                playerNumber: 0
+            }),
             location: new Location({
-                X: 200,
+                X: 100,
                 Y: 200,
             }),
             bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40]}),
@@ -54,6 +57,21 @@ export class Main extends LockstepClient<KeyName[], Data> {
             }),
         });
 
+        Create(newState, {
+            playerControl: new PlayerControl({
+                playerNumber: 1
+            }),
+            location: new Location({
+                X: 400,
+                Y: 200,
+            }),
+            bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40]}),
+            renderBounds: new RenderBounds({
+                color: "#f0a",
+                layer: 0
+            }),
+        });
+
         return newState;
     }
 
diff --git a/src/Net/LockstepClient.ts b/src/Net/LockstepClient.ts
index ad22e97..43ea9df 100644
--- a/src/Net/LockstepClient.ts
+++ b/src/Net/LockstepClient.ts
@@ -43,8 +43,8 @@ export type ClientMessage<Input, State> =
 ;
 
 export type ServerMessage<Input, State> =
-    | Packet<MessageTypes.SET_STATE, { s: Partial<State> }>
-    | Packet<MessageTypes.INPUT, { i: Input }>
+    | Packet<MessageTypes.SET_STATE, { u: number, s: Partial<State> }>
+    | Packet<MessageTypes.INPUT, { i: Input[] }>
     | Packet<MessageTypes.GET_STATE, { c: number }>
     | Packet<MessageTypes.PING, { z: number }>
 ;
@@ -53,6 +53,7 @@ export type Server<Input, State> = Callbag<ClientMessage<Input, State>, ServerMe
 
 export abstract class LockstepClient<Input, State> {
 
+    private playerNumber = -1;
     private state: LockstepState<Input, State>;
 
     public constructor(
@@ -75,8 +76,10 @@ export abstract class LockstepClient<Input, State> {
         const sampleInput = () => {
             if (serverTalkback) {
                 const input = this.gatherInput();
-                this.state.addLocalInput(input);
-                serverTalkback(1, { t: MessageTypes.INPUT, i: input });
+                if(this.playerNumber >= 0) {
+                    this.state.addLocalInput(this.playerNumber, input);
+                    serverTalkback(1, { t: MessageTypes.INPUT, i: input });
+                }
                 setTimeout(sampleInput, INPUT_FREQUENCY);
             }
         };
@@ -95,6 +98,7 @@ export abstract class LockstepClient<Input, State> {
                     case MessageTypes.SET_STATE:
                         const resetState = this.initState(message.s);
                         this.state = new LockstepState(resetState, this.engine);
+                        this.playerNumber = message.u;
                         break;
                     case MessageTypes.INPUT:
                         this.state.addCanonInput(message.i);
diff --git a/src/Net/LoopbackServer.ts b/src/Net/LoopbackServer.ts
index 9e1deb1..323b858 100644
--- a/src/Net/LoopbackServer.ts
+++ b/src/Net/LoopbackServer.ts
@@ -15,10 +15,13 @@ export function Loopback<Input, State>(start: number, data?: Client<Input, State
             const message = data as ClientMessage<Input, State>;
             switch(message.t) {
                 case MessageTypes.INPUT:
-                    sink(1, message);
+                    sink(1, {
+                        t: MessageTypes.INPUT,
+                        i: [message.i],
+                    });
                     break;
             }
         }
     });
-    sink(1, {t: MessageTypes.SET_STATE, s: {}});
+    sink(1, {t: MessageTypes.SET_STATE, u: 0, s: {}});
 };