Track protocol 777 health and hunger
This commit is contained in:
@@ -48,8 +48,14 @@ g.job = None
|
||||
g.correction_count = 0
|
||||
g.holding = 0
|
||||
g.afk = False
|
||||
g.health = 20
|
||||
g.food = 20
|
||||
g.health = None
|
||||
g.food = None
|
||||
g.hunger = None
|
||||
g.food_saturation = None
|
||||
g.saturation = None
|
||||
g.health_update_received = False
|
||||
g.health_updated_at = None
|
||||
g.dead = False
|
||||
g.sand_origin = None
|
||||
g.reload_lock = threading.RLock()
|
||||
_item_icon_cache = Path(__file__).resolve().parent / 'web_interface' / 'item_cache'
|
||||
|
||||
@@ -107,6 +107,14 @@ class Commands:
|
||||
if command == 'pos':
|
||||
reply = str(utils.pint(self.g.pos))[1:-1] + ', ' + self.g.dimension
|
||||
|
||||
## !health - replies with health, hunger, and saturation
|
||||
if command == 'health':
|
||||
if not self.g.get('health_update_received'):
|
||||
reply = 'health status not received yet'
|
||||
else:
|
||||
reply = 'health {:.1f}, hunger {}/20, saturation {:.2f}'.format(
|
||||
self.g.health, self.g.food, self.g.food_saturation)
|
||||
|
||||
## !afk - goes AFK with /afk
|
||||
if command == 'afk':
|
||||
if not self.g.afk:
|
||||
|
||||
@@ -583,6 +583,44 @@ class Game(LegacyGame):
|
||||
self.break_deadline = None
|
||||
self.breaking_state = None
|
||||
|
||||
from mosfet.protocol.packets import SetHealthPacket
|
||||
# Health is a protocol-777 packet, not the legacy UpdateHealthPacket.
|
||||
# Register its decoder on an already-running reactor as well, so a
|
||||
# hot reload can enable health tracking without reconnecting. Rebuild
|
||||
# the base map first to remove any stale decoder for an earlier ID.
|
||||
reactor = self.g.connection.reactor
|
||||
if hasattr(reactor, 'clientbound_packets'):
|
||||
context = self.g.connection.context
|
||||
reactor.clientbound_packets = {
|
||||
packet.get_id(context): packet
|
||||
for packet in reactor.__class__.get_clientbound_packets(context)
|
||||
}
|
||||
reactor.clientbound_packets[SetHealthPacket.id] = SetHealthPacket
|
||||
self.g.connection.register_packet_listener(
|
||||
self.handle_set_health, SetHealthPacket)
|
||||
|
||||
if not self.g.get('health_update_received', False):
|
||||
self.g.health = None
|
||||
self.g.food = None
|
||||
self.g.hunger = None
|
||||
self.g.food_saturation = None
|
||||
self.g.saturation = None
|
||||
self.g.health_update_received = False
|
||||
self.g.health_updated_at = None
|
||||
self.g.dead = False
|
||||
|
||||
def handle_set_health(self, packet):
|
||||
self.g.health = float(packet.health)
|
||||
self.g.food = int(packet.food)
|
||||
self.g.hunger = self.g.food
|
||||
self.g.food_saturation = float(packet.food_saturation)
|
||||
self.g.saturation = self.g.food_saturation
|
||||
self.g.health_update_received = True
|
||||
self.g.health_updated_at = time.time()
|
||||
self.g.dead = self.g.health <= 0
|
||||
print('[status] health={:.1f} hunger={} saturation={:.2f}'.format(
|
||||
self.g.health, self.g.food, self.g.food_saturation), flush=True)
|
||||
|
||||
def _send_action(self, packet):
|
||||
self.g.connection.write_packet(packet, force=True)
|
||||
self.g.connection.write_packet(ClientTickEnd(), force=True)
|
||||
|
||||
@@ -13,6 +13,17 @@ from .types import Nbt, Slot, Entry, Trade
|
||||
from mosfet.info import blocks
|
||||
|
||||
|
||||
class SetHealthPacket(Packet):
|
||||
"""Minecraft 26.3 (protocol 777) clientbound health/food update."""
|
||||
id = 0x6A
|
||||
packet_name = 'set health'
|
||||
definition = [
|
||||
{'health': Float},
|
||||
{'food': VarInt},
|
||||
{'food_saturation': Float},
|
||||
]
|
||||
|
||||
|
||||
class ChunkDataPacket(Packet):
|
||||
id = 0x20
|
||||
packet_name = 'chunk data'
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import io
|
||||
import struct
|
||||
import unittest
|
||||
|
||||
from munch import Munch
|
||||
|
||||
from mosfet.game import Game
|
||||
from mosfet.protocol.packets import SetHealthPacket
|
||||
|
||||
|
||||
class TestSetHealthPacket(unittest.TestCase):
|
||||
def test_protocol_777_packet_decodes_all_status_values(self):
|
||||
payload = struct.pack('>f', 13.5) + bytes([14]) + struct.pack('>f', 5.25)
|
||||
packet = SetHealthPacket()
|
||||
packet.read(io.BytesIO(payload))
|
||||
|
||||
self.assertEqual(packet.id, 0x6A)
|
||||
self.assertEqual(packet.health, 13.5)
|
||||
self.assertEqual(packet.food, 14)
|
||||
self.assertEqual(packet.food_saturation, 5.25)
|
||||
|
||||
def test_game_state_tracks_health_hunger_and_saturation(self):
|
||||
game = Game.__new__(Game)
|
||||
game.g = Munch()
|
||||
game.handle_set_health(SetHealthPacket(
|
||||
health=7.5, food=9, food_saturation=2.25))
|
||||
|
||||
self.assertEqual(game.g.health, 7.5)
|
||||
self.assertEqual(game.g.food, 9)
|
||||
self.assertEqual(game.g.hunger, 9)
|
||||
self.assertEqual(game.g.food_saturation, 2.25)
|
||||
self.assertEqual(game.g.saturation, 2.25)
|
||||
self.assertTrue(game.g.health_update_received)
|
||||
self.assertFalse(game.g.dead)
|
||||
|
||||
def test_zero_health_marks_bot_dead_without_faking_values(self):
|
||||
game = Game.__new__(Game)
|
||||
game.g = Munch()
|
||||
game.handle_set_health(SetHealthPacket(
|
||||
health=0.0, food=0, food_saturation=0.0))
|
||||
|
||||
self.assertEqual(game.g.health, 0.0)
|
||||
self.assertEqual(game.g.food, 0)
|
||||
self.assertTrue(game.g.dead)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -37,6 +37,7 @@
|
||||
<div class="stat-top"><span class="stat-label">HUNGER</span><span class="stat-icon food">◆</span></div>
|
||||
<div class="stat-value"><span id="food-value">—</span><small>/ 20</small></div>
|
||||
<div class="meter"><span id="food-meter" class="meter-fill food-fill"></span></div>
|
||||
<div id="food-saturation" class="stat-caption">Saturation —</div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">MOVEMENT</span><span class="live-dot"></span></div>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<div class="stat-top"><span class="stat-label">HUNGER</span><span class="stat-icon food">◆</span></div>
|
||||
<div class="stat-value"><span id="food-value">—</span><small>/ 20</small></div>
|
||||
<div class="meter"><span id="food-meter" class="meter-fill food-fill"></span></div>
|
||||
<div id="food-saturation" class="stat-caption">Saturation —</div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">MOVEMENT</span><span class="live-dot"></span></div>
|
||||
|
||||
@@ -147,12 +147,17 @@
|
||||
$('position').textContent = vectorText(state.pos || state.player_position);
|
||||
$('dimension').textContent = dimensionName(state.dimension);
|
||||
|
||||
const health = Number(state.health);
|
||||
const food = Number(state.food);
|
||||
$('health-value').textContent = Number.isFinite(health) ? String(health) : '—';
|
||||
const health = state.health == null ? NaN : Number(state.health);
|
||||
const food = state.food == null ? NaN : Number(state.food);
|
||||
const saturation = state.food_saturation == null
|
||||
? NaN : Number(state.food_saturation);
|
||||
$('health-value').textContent = Number.isFinite(health)
|
||||
? Number(health.toFixed(1)).toString() : '—';
|
||||
$('food-value').textContent = Number.isFinite(food) ? String(food) : '—';
|
||||
$('health-meter').style.width = `${Math.max(0, Math.min(100, health * 5 || 0))}%`;
|
||||
$('food-meter').style.width = `${Math.max(0, Math.min(100, food * 5 || 0))}%`;
|
||||
$('food-saturation').textContent = Number.isFinite(saturation)
|
||||
? `Saturation ${saturation.toFixed(2)}` : 'Saturation —';
|
||||
$('health-meter').style.width = `${Math.max(0, Math.min(100, (health || 0) * 5))}%`;
|
||||
$('food-meter').style.width = `${Math.max(0, Math.min(100, (food || 0) * 5))}%`;
|
||||
|
||||
const movement = state.movement_status || (state.path?.length ? 'following_path' : 'idle');
|
||||
$('movement-value').textContent = String(movement).replaceAll('_', ' ').toUpperCase();
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<div class="stat-top"><span class="stat-label">HUNGER</span><span class="stat-icon food">◆</span></div>
|
||||
<div class="stat-value"><span id="food-value">—</span><small>/ 20</small></div>
|
||||
<div class="meter"><span id="food-meter" class="meter-fill food-fill"></span></div>
|
||||
<div id="food-saturation" class="stat-caption">Saturation —</div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">MOVEMENT</span><span class="live-dot"></span></div>
|
||||
|
||||
Reference in New Issue
Block a user