Modify the movement to use websockets

using System.Text;
using UnityEngine.Networking;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class FirstPersonController : MonoBehaviour
{
    [Header("Movement Settings")]
    [SerializeField] private float moveSpeed = 5f;
    [SerializeField] private float sprintMultiplier = 1.5f;
    [SerializeField] private float jumpForce = 5f;

    [Header("Mouse Look Settings")]
    [SerializeField] private float mouseSensitivity = 2f;
    [SerializeField] private float maxLookAngle = 80f;

    [Header("Network Settings")]
    [SerializeField] private string serverUrl = "http://localhost:8000";
    [SerializeField] private float networkUpdateRate = 0.1f; // Seconds between position updates
    [SerializeField] private float movementUpdateRate = 0.1f; // Seconds between movement updates

    private Camera playerCamera;
    private float verticalRotation = 0f;
    private Vector3 serverPosition;
    private float lastNetworkUpdate;
    private float lastMovementUpdate;

    private string lastAction = "stop";
    private bool lastJump = false;
    private bool lastSprint = false;

    private Vector3 prevServerPosition;
    private float prevServerTime;
    private float serverTime;
    private float currentLerpTime;

    private void Start()
    {
        playerCamera = GetComponentInChildren<Camera>();
        if (playerCamera == null)
        {
            Debug.LogError("No camera found in children of FirstPersonController!");
            enabled = false;
            return;
        }

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

        // Initialize position variables
        prevServerPosition = transform.position;
        serverPosition = transform.position;
        prevServerTime = Time.time;
        serverTime = Time.time;
        currentLerpTime = 0f;

        // Start position sync coroutine
        StartCoroutine(SyncPositionWithServer());
    }

    private void Update()
    {
        HandleMouseLook();
        HandleMovementInput();

        // Send position updates to server periodically
        if (Time.time - lastNetworkUpdate >= networkUpdateRate)
        {
            SendPositionToServer();
            lastNetworkUpdate = Time.time;
        }

        // Interpolate position
        currentLerpTime += Time.deltaTime;

        float timeSinceLastUpdate = Time.time - prevServerTime;
        float lerpDuration = serverTime - prevServerTime;

        // Avoid division by zero
        if (lerpDuration > 0f)
        {
            float t = currentLerpTime / lerpDuration;
            t = Mathf.Clamp01(t);
            transform.position = Vector3.Lerp(prevServerPosition, serverPosition, t);
        }
        else
        {
            transform.position = serverPosition;
        }
    }

    private void HandleMouseLook()
    {
        // Horizontal rotation
        float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
        transform.Rotate(Vector3.up * mouseX);

        // Vertical rotation (camera only)
        float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;
        verticalRotation -= mouseY;
        verticalRotation = Mathf.Clamp(verticalRotation, -maxLookAngle, maxLookAngle);
        playerCamera.transform.localRotation = Quaternion.Euler(verticalRotation, 0f, 0f);

        // Send look direction to server if significant movement
        if (Mathf.Abs(mouseX) > 0.01f || Mathf.Abs(mouseY) > 0.01f)
        {
            SendLookDirectionToServer();
        }
    }

    private void HandleMovementInput()
    {
        // Determine action based on input
        string action = "stop";
        bool jump = Input.GetKey(KeyCode.Space);
        bool sprint = Input.GetKey(KeyCode.LeftShift);

        bool forward = Input.GetKey(KeyCode.W);
        bool backward = Input.GetKey(KeyCode.S);
        bool left = Input.GetKey(KeyCode.A);
        bool right = Input.GetKey(KeyCode.D);

        // Decide action based on key presses
        if (forward && !backward)
        {
            action = "forward";
        }
        else if (backward && !forward)
        {
            action = "backward";
        }
        else if (left && !right)
        {
            action = "left";
        }
        else if (right && !left)
        {
            action = "right";
        }
        else if (jump)
        {
            action = "jump";
        }
        else
        {
            action = "stop";
        }

        // Check if movement state has changed
        bool shouldUpdate = action != lastAction || jump != lastJump || sprint != lastSprint;

        // Only send movement update if something changed or update timer elapsed
        if (shouldUpdate || Time.time - lastMovementUpdate >= movementUpdateRate)
        {
            SendMovementToServer(action, jump, sprint);
            lastMovementUpdate = Time.time;

            // Update last known states
            lastAction = action;
            lastJump = jump;
            lastSprint = sprint;
        }
    }

    private void SendMovementToServer(string action, bool jump, bool sprint)
    {
        StartCoroutine(PostMovementRequest(action, jump, sprint));
    }

    private void SendPositionToServer()
    {
        StartCoroutine(PostPositionRequest());
    }

    private IEnumerator PostPositionRequest()
    {
        var posData = new CoordinateData
        {
            x = transform.position.x,
            y = transform.position.y,
            z = transform.position.z
        };

        string json = JsonUtility.ToJson(posData);

        using (UnityWebRequest request = new UnityWebRequest($"{serverUrl}/bot/coordinates", "POST"))
        {
            byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
            request.uploadHandler = new UploadHandlerRaw(jsonBytes);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");

            yield return request.SendWebRequest();

            if (request.result != UnityWebRequest.Result.Success)
            {
                Debug.LogWarning($"Position request failed: {request.error}");
            }
        }
    }

    private void SendLookDirectionToServer()
    {
        Vector3 lookTarget = playerCamera.transform.position + playerCamera.transform.forward * 100f;
        StartCoroutine(PostLookRequest(lookTarget));
    }

    private IEnumerator SyncPositionWithServer()
    {
        while (enabled)
        {
            using (UnityWebRequest request = UnityWebRequest.Get($"{serverUrl}/bot/coordinates"))
            {
                yield return request.SendWebRequest();

                if (request.result == UnityWebRequest.Result.Success)
                {
                    string json = request.downloadHandler.text;
                    CoordinateData coords = JsonUtility.FromJson<CoordinateData>(json);

                    // Store previous server position and time
                    prevServerPosition = serverPosition;
                    prevServerTime = serverTime;

                    // Update current server position and time
                    serverPosition = new Vector3(coords.x, coords.y, coords.z);
                    serverTime = Time.time;
                    currentLerpTime = 0f;
                }
                else
                {
                    Debug.LogWarning($"Position sync failed: {request.error}");
                }
            }

            yield return new WaitForSeconds(networkUpdateRate);
        }
    }

    private IEnumerator PostMovementRequest(string action, bool jump, bool sprint)
    {
        var moveData = new MovementData
        {
            action = action,
            jump = jump,
            sprint = sprint
        };
        string json = JsonUtility.ToJson(moveData);

        using (UnityWebRequest request = new UnityWebRequest($"{serverUrl}/bot/move", "POST"))
        {
            byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
            request.uploadHandler = new UploadHandlerRaw(jsonBytes);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");

            yield return request.SendWebRequest();

            if (request.result != UnityWebRequest.Result.Success)
            {
                Debug.LogWarning($"Movement request failed: {request.error}");
            }
        }
    }

    private IEnumerator PostLookRequest(Vector3 lookTarget)
    {
        var lookData = new CoordinateData
        {
            x = lookTarget.x,
            y = lookTarget.y,
            z = lookTarget.z
        };

        string json = JsonUtility.ToJson(lookData);

        using (UnityWebRequest request = new UnityWebRequest($"{serverUrl}/bot/look", "POST"))
        {
            byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
            request.uploadHandler = new UploadHandlerRaw(jsonBytes);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");

            yield return request.SendWebRequest();

            if (request.result != UnityWebRequest.Result.Success)
            {
                Debug.LogWarning($"Look request failed: {request.error}");
            }
        }
    }

    [System.Serializable]
    private class MovementData
    {
        public string action;
        public bool jump;
        public bool sprint;
    }

    [System.Serializable]
    private class CoordinateData
    {
        public float x;
        public float y;
        public float z;
    }
}

const mineflayer = require('mineflayer');
const express = require('express');
const Vec3 = require('vec3');

const app = express();
const port = 8000;

// Bot configuration
const botConfig = {
  host: 'localhost',
  port: 25565,
  username: 'ChunkScanner',
  version: '1.18.2'
};

const transparentBlocks = new Set([
  'air', 'water', 'lava', 'chain', 'grass_block', 'grass', 'tall_grass'
]);

const transparentBlockPattern = new RegExp(
  '(glass|leaves|fence|door|trapdoor|bars|rod|lantern|vine|sapling|coral|kelp|' +
  'bamboo|mushroom|rail|torch|sign|banner|button|pressure|carpet|ladder)'
);

const CHUNK_SIZE = 16;
const CHUNK_RANGE = { X: 3, Z: 3 };
const ENABLE_CULLING = true;

const NEIGHBOR_OFFSETS = [
  new Vec3(1, 0, 0), new Vec3(-1, 0, 0),
  new Vec3(0, 1, 0), new Vec3(0, -1, 0),
  new Vec3(0, 0, 1), new Vec3(0, 0, -1)
];

let bot;
let chunksLoaded = false;
let chunkCache = new Map();
let cachedBlockData = [];

app.use(express.json());

function isTransparentBlock(blockName) {
  return transparentBlocks.has(blockName) || transparentBlockPattern.test(blockName);
}

function isBlockExposed(bot, blockPos) {
  for (const offset of NEIGHBOR_OFFSETS) {
    const neighborPos = blockPos.plus(offset);
    const neighborBlock = bot.blockAt(neighborPos);
    if (!neighborBlock || isTransparentBlock(neighborBlock.name)) {
      return true;
    }
  }
  return false;
}

function scanLoadedChunks(bot) {
  const blocks = [];
  const world = bot.world;
  const playerPos = bot.entity.position;

  const playerChunk = {
    x: Math.floor(playerPos.x / CHUNK_SIZE),
    z: Math.floor(playerPos.z / CHUNK_SIZE)
  };

  const minY = world.minY || 0;
  const maxY = world.maxY || 256;
  const blockPos = new Vec3(0, 0, 0);

  const batchSize = 4;
  for (let cx = -CHUNK_RANGE.X; cx <= CHUNK_RANGE.X; cx += batchSize) {
    for (let cz = -CHUNK_RANGE.Z; cz <= CHUNK_RANGE.Z; cz += batchSize) {
      processChunkBatch(cx, cz, Math.min(batchSize, CHUNK_RANGE.X - cx + 1),
        Math.min(batchSize, CHUNK_RANGE.Z - cz + 1));
    }
  }

  function processChunkBatch(startCX, startCZ, batchWidth, batchHeight) {
    for (let cx = startCX; cx < startCX + batchWidth; cx++) {
      for (let cz = startCZ; cz < startCZ + batchHeight; cz++) {
        const chunkX = playerChunk.x + cx;
        const chunkZ = playerChunk.z + cz;

        for (let x = 0; x < CHUNK_SIZE; x++) {
          for (let z = 0; z < CHUNK_SIZE; z++) {
            const absoluteX = chunkX * CHUNK_SIZE + x;
            const absoluteZ = chunkZ * CHUNK_SIZE + z;

            if (Math.abs(absoluteX - playerPos.x) > CHUNK_SIZE * CHUNK_RANGE.X ||
                Math.abs(absoluteZ - playerPos.z) > CHUNK_SIZE * CHUNK_RANGE.Z) {
              continue;
            }

            for (let y = minY; y < maxY; y++) {
              try {
                blockPos.set(absoluteX, y, absoluteZ);
                const block = bot.blockAt(blockPos);

                if (!block || block.type === 0) continue;

                if (isTransparentBlock(block.name)) {
                  blocks.push({
                    block_name: block.name,
                    x: absoluteX,
                    y,
                    z: absoluteZ,
                    direction: block.metadata
                  });
                  continue;
                }

                if (ENABLE_CULLING && y < playerPos.y && !isBlockExposed(bot, blockPos)) {
                  continue;
                }

                blocks.push({
                  block_name: block.name,
                  x: absoluteX,
                  y,
                  z: absoluteZ,
                  direction: block.metadata
                });
              } catch (err) {
                continue;
              }
            }
          }
        }
      }
    }
  }

  return blocks;
}

function initializeBot() {
  bot = mineflayer.createBot(botConfig);

  bot.once('spawn', () => {
    console.log('Bot spawned, waiting for chunks to load...');
  });

  bot.on('chunkColumnLoad', (point) => {
    if (!chunksLoaded) {
      chunksLoaded = true;
      console.log('First chunk loaded at', point);
      setTimeout(() => {
        cachedBlockData = scanLoadedChunks(bot);
        console.log(`Scanned ${cachedBlockData.length} blocks`);
        chunkCache.clear();
      }, 2000);
    }
  });

  bot.on('error', console.error);
  bot.on('kicked', console.log);
}

app.get('/blocks', (req, res) => {
  res.json(cachedBlockData);
});

app.get('/bot/coordinates', (req, res) => {
  if (bot && bot.entity) {
    const position = bot.entity.position;
    res.json({
      x: position.x,
      y: position.y,
      z: position.z
    });
  } else {
    res.status(404).json({
      success: false,
      error: 'Bot is not available'
    });
  }
});

app.post('/bot/coordinates', (req, res) => {
  const { x, y, z } = req.body;
  if (bot) {
    // Perhaps store or log the coordinates, but do not change control states
    res.json({
      success: true,
      message: `Received coordinates ${x}, ${y}, ${z}`
    });
  } else {
    // Handle bot not available
  }
});

app.post('/bot/move', (req, res) => {
  const { action, jump = false, sprint = false } = req.body;

  if (bot) {
    try {
      bot.clearControlStates();

      switch (action) {
        case 'forward':
          bot.setControlState('forward', true);
          break;
        case 'backward':
          bot.setControlState('back', true);
          break;
        case 'left':
          bot.setControlState('left', true);
          break;
        case 'right':
          bot.setControlState('right', true);
          break;
        case 'jump':
          bot.setControlState('jump', true);
          break;
        case 'stop':
          bot.clearControlStates();
          break;
        default:
          return res.status(400).json({
            success: false,
            error: 'Invalid action'
          });
      }

      if (jump && action !== 'jump') {
        bot.setControlState('jump', true);
      }

      if (sprint) {
        bot.setControlState('sprint', true);
      }

      res.json({
        success: true,
        message: `Bot moving ${action}, Jump: ${jump}, Sprint: ${sprint}`
      });
    } catch (err) {
      res.status(500).json({
        success: false,
        error: err.message
      });
    }
  } else {
    res.status(404).json({
      success: false,
      error: 'Bot is not available'
    });
  }
});

app.post('/bot/look', (req, res) => {
  const { x, y, z } = req.body;
  if (bot) {
    try {
      const targetPosition = new Vec3(x, y, z);
      bot.lookAt(targetPosition, true, () => {
        res.json({
          success: true,
          message: `Bot looking at ${x}, ${y}, ${z}`
        });
      });
    } catch (err) {
      res.status(500).json({
        success: false,
        error: err.message
      });
    }
  } else {
    res.status(404).json({
      success: false,
      error: 'Bot is not available'
    });
  }
});

// New stop endpoint to clear any movement states
app.post('/bot/stop', (req, res) => {
  if (bot) {
    bot.clearControlStates();
    res.json({
      success: true,
      message: 'Bot stopped'
    });
  } else {
    res.status(404).json({
      success: false,
      error: 'Bot is not available'
    });
  }
});

app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
  initializeBot();
});