add predictive movement to the unity code

using System.Text;
using UnityEngine.Networking;
using UnityEngine;
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using System.Text;
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;

// Removed controlStates dictionary as we now work with actions
private string lastAction = "stop";
private bool lastJump = false;
private bool lastSprint = false;

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;

    // 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;
    }
}

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);
                serverPosition = new Vector3(coords.x, coords.y, coords.z);
                transform.position = Vector3.Lerp(transform.position, serverPosition, 0.5f);
            }
            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();
});