-
Notifications
You must be signed in to change notification settings - Fork 891
Add action logger #773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Wavy77
wants to merge
3
commits into
mindcraft-bots:develop
Choose a base branch
from
Wavy77:pr-action-logger
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add action logger #773
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Structured Action Logging | ||
|
|
||
| Mindcraft can optionally write structured `ACTION` lines for successful bot actions that change or use blocks. The log is intended to make Andy's physical actions inspectable without changing how the actions are executed. | ||
|
|
||
| ## Enabling | ||
|
|
||
| Set `action_logging` to `true` in `settings.js`: | ||
|
|
||
| ```js | ||
| "action_logging": true, | ||
| "action_log_file_prefix": "andy_log", | ||
| ``` | ||
|
|
||
| Logging is disabled by default so normal Mindcraft behavior stays unchanged unless the feature is explicitly enabled. | ||
|
|
||
| ## Log Location | ||
|
|
||
| When the first action is logged, Mindcraft creates a new file for the current bot: | ||
|
|
||
| ```text | ||
| ./bots/<bot-name>/logs/andy_log_<timestamp>.txt | ||
| ``` | ||
|
|
||
| The filename prefix can be changed with `action_log_file_prefix`. | ||
|
|
||
| ## Log Format | ||
|
|
||
| Each logged action is one line: | ||
|
|
||
| ```text | ||
| ACTION <type> item=<item> previous_block=<block> result_block=<block> result_coord=[x,y,z] clicked_block=[x,y,z] clicked_face=<face> player_pos=[x.xxxxxx,y.yyyyyy,z.zzzzzz] yaw=<degrees> pitch=<degrees> sneaking=<true|false> standing_on=[x,y,z] standing_on_block=<block> hand=<main_hand|off_hand> tick=<tick> timestamp=<iso-date> | ||
| ``` | ||
|
|
||
| Example: | ||
|
|
||
| ```text | ||
| ACTION place_block item=dirt previous_block=air result_block=dirt result_coord=[4,-61,1] clicked_block=[4,-62,1] clicked_face=up player_pos=[3.472681,-60.000000,2.516543] yaw=-493.917 pitch=58.176 sneaking=false standing_on=[3,-61,2] standing_on_block=grass_block hand=main_hand tick=1011330 timestamp=2026-05-02T16:56:42.367Z | ||
| ``` | ||
|
|
||
| Unknown or unavailable fields are written as `unknown`. | ||
|
|
||
| ## Logged Actions | ||
|
|
||
| The logger currently records successful actions from the standard skill execution paths: | ||
|
|
||
| - `break_block` from `breakBlockAt` | ||
| - `place_block` from normal `placeBlock` | ||
| - `place_fluid` from bucket-based fluid placement through `placeBlock` or `useToolOnBlock` | ||
| - `use_bucket` from empty bucket use on water or lava through `useToolOnBlock` | ||
| - `use_item_on_block` from generic tool/item use through `useToolOnBlock` | ||
|
|
||
| ## Known Limitations | ||
|
|
||
| - The logger records actions that go through the shared skill functions. Direct Mineflayer calls made elsewhere are not automatically logged. | ||
| - Some interactions do not expose an exact clicked face through the existing skill API; those lines use `clicked_face=unknown`. | ||
| - Cheat-mode `/setblock` placement and breaking are logged with the target coordinate, but they do not have a real clicked block face. | ||
| - Entity interactions are not logged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,8 @@ const settings = { | |
|
|
||
| "spawn_timeout": 30, // num seconds allowed for the bot to spawn before throwing error. Increase when spawning takes a while. | ||
| "block_place_delay": 0, // delay between placing blocks (ms) if using newAction. helps avoid bot being kicked by anti-cheat mechanisms on servers. | ||
| "action_logging": false, // when true, write structured ACTION logs for successful bot block/item actions. | ||
| "action_log_file_prefix": "andy_log", // prefix for action log files in ./bots/<bot>/logs/ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be hard coded. Or just use
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i made the log name log__.... instead of andy_log in the new commit. |
||
|
|
||
| "log_all_prompts": false, // log ALL prompts to file | ||
| }; | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| import { appendFileSync, mkdirSync, writeFileSync } from 'fs'; | ||
| import path from 'path'; | ||
| import settings from '../../settings.js'; | ||
|
|
||
| const ACTION_LOG_STATE = Symbol.for('mindcraft.actionLogState'); | ||
|
|
||
| export function isActionLoggingEnabled() { | ||
| return Boolean(settings.action_logging ?? settings.action_logging_enabled ?? false); | ||
| } | ||
|
|
||
| function timestampForFilename(date = new Date()) { | ||
| return date.toISOString().replace(/[:.]/g, '-'); | ||
| } | ||
|
|
||
| function safeName(name, fallback = 'andy') { | ||
| return String(name || fallback).replace(/[^a-zA-Z0-9_-]/g, '_'); | ||
| } | ||
|
|
||
| function getBotLogDirectory(bot) { | ||
| const botName = safeName(bot?.username || bot?.name); | ||
| return path.join('.', 'bots', botName, 'logs'); | ||
| } | ||
|
|
||
| function getActionLogPath(bot) { | ||
| if (!bot[ACTION_LOG_STATE]) { | ||
| const logDir = getBotLogDirectory(bot); | ||
| mkdirSync(logDir, { recursive: true }); | ||
| const prefix = safeName(settings.action_log_file_prefix || 'andy_log', 'andy_log'); | ||
| const logPath = path.resolve(logDir, `${prefix}_${timestampForFilename()}.txt`); | ||
| writeFileSync(logPath, '', 'utf8'); | ||
| bot[ACTION_LOG_STATE] = { logPath }; | ||
| } | ||
| return bot[ACTION_LOG_STATE].logPath; | ||
| } | ||
|
|
||
| function blockCoord(coord) { | ||
| if (!coord) return null; | ||
| return { | ||
| x: Math.floor(coord.x), | ||
| y: Math.floor(coord.y), | ||
| z: Math.floor(coord.z), | ||
| }; | ||
| } | ||
|
|
||
| function entityPosition(bot) { | ||
| const pos = bot?.entity?.position; | ||
| if (!pos) return null; | ||
| return { x: pos.x, y: pos.y, z: pos.z }; | ||
| } | ||
|
|
||
| function standingState(bot) { | ||
| const pos = bot?.entity?.position; | ||
| if (!pos) { | ||
| return { position: null, blockName: 'unknown' }; | ||
| } | ||
| const standing = { | ||
| x: Math.floor(pos.x), | ||
| y: Math.floor(pos.y - 0.01), | ||
| z: Math.floor(pos.z), | ||
| }; | ||
| const blockName = blockNameAt(bot, standing); | ||
| return { position: standing, blockName }; | ||
| } | ||
|
|
||
| function blockNameAt(bot, coord) { | ||
| if (!bot || !coord) return 'unknown'; | ||
| try { | ||
| const pos = { | ||
| x: Math.floor(coord.x), | ||
| y: Math.floor(coord.y), | ||
| z: Math.floor(coord.z), | ||
| }; | ||
| pos.floor = () => pos; | ||
| pos.floored = () => pos; | ||
| const block = bot.blockAt(pos); | ||
| return block?.name || 'unknown'; | ||
| } catch (_) { | ||
| return 'unknown'; | ||
| } | ||
| } | ||
|
|
||
| function heldItemName(bot) { | ||
| return bot?.heldItem?.name || 'none'; | ||
| } | ||
|
|
||
| function getSneakState(bot) { | ||
| if (typeof bot?.getControlState === 'function') { | ||
| return Boolean(bot.getControlState('sneak')); | ||
| } | ||
| return Boolean(bot?.controlState?.sneak); | ||
| } | ||
|
|
||
| function getBotTick(bot) { | ||
| const candidates = [bot?.time?.age, bot?.time?.time, bot?.time?.timeOfDay]; | ||
| return candidates.find(value => Number.isFinite(value)) ?? 'unknown'; | ||
| } | ||
|
|
||
| function radToDeg(radians) { | ||
| return radians * 180 / Math.PI; | ||
| } | ||
|
|
||
| function actualYawDegrees(bot) { | ||
| return Number.isFinite(bot?.entity?.yaw) ? radToDeg(bot.entity.yaw) : null; | ||
| } | ||
|
|
||
| function actualPitchDegrees(bot) { | ||
| return Number.isFinite(bot?.entity?.pitch) ? radToDeg(bot.entity.pitch) : null; | ||
| } | ||
|
|
||
| function actionValue(value) { | ||
| if (value === null || value === undefined || value === '') return 'unknown'; | ||
| return String(value); | ||
| } | ||
|
|
||
| function intCoordText(coord) { | ||
| if (!coord) return 'unknown'; | ||
| return `[${Math.floor(coord.x)},${Math.floor(coord.y)},${Math.floor(coord.z)}]`; | ||
| } | ||
|
|
||
| function floatText(value, precision = 6) { | ||
| return Number.isFinite(value) ? Number(value).toFixed(precision) : 'unknown'; | ||
| } | ||
|
|
||
| function floatCoordText(coord, precision = 6) { | ||
| if (!coord) return 'unknown'; | ||
| return `[${floatText(coord.x, precision)},${floatText(coord.y, precision)},${floatText(coord.z, precision)}]`; | ||
| } | ||
|
|
||
| function actionNumber(value, precision = 3) { | ||
| return Number.isFinite(value) ? Number(value).toFixed(precision) : 'unknown'; | ||
| } | ||
|
|
||
| export function faceNameFromVector(vec) { | ||
| if (!vec) return 'unknown'; | ||
| if (vec.x === 1 && vec.y === 0 && vec.z === 0) return 'east'; | ||
| if (vec.x === -1 && vec.y === 0 && vec.z === 0) return 'west'; | ||
| if (vec.x === 0 && vec.y === 1 && vec.z === 0) return 'up'; | ||
| if (vec.x === 0 && vec.y === -1 && vec.z === 0) return 'down'; | ||
| if (vec.x === 0 && vec.y === 0 && vec.z === 1) return 'south'; | ||
| if (vec.x === 0 && vec.y === 0 && vec.z === -1) return 'north'; | ||
| return 'unknown'; | ||
| } | ||
|
|
||
| export function formatActionLogLine(record) { | ||
| return [ | ||
| `ACTION ${record.type}`, | ||
| `item=${actionValue(record.item)}`, | ||
| `previous_block=${actionValue(record.previousBlock)}`, | ||
| `result_block=${actionValue(record.resultBlock)}`, | ||
| `result_coord=${intCoordText(record.resultCoord)}`, | ||
| `clicked_block=${intCoordText(record.clickedBlock)}`, | ||
| `clicked_face=${actionValue(record.clickedFace)}`, | ||
| `player_pos=${floatCoordText(record.playerPos, 6)}`, | ||
| `yaw=${actionNumber(record.yaw, 3)}`, | ||
| `pitch=${actionNumber(record.pitch, 3)}`, | ||
| `sneaking=${record.sneaking === null || record.sneaking === undefined ? 'unknown' : String(Boolean(record.sneaking))}`, | ||
| `standing_on=${intCoordText(record.standingOn)}`, | ||
| `standing_on_block=${actionValue(record.standingOnBlock)}`, | ||
| `hand=${actionValue(record.hand)}`, | ||
| `tick=${actionValue(record.tick)}`, | ||
| `timestamp=${actionValue(record.timestamp)}`, | ||
| ].join(' '); | ||
| } | ||
|
|
||
| export function appendActionLog(bot, action) { | ||
| if (!bot || !isActionLoggingEnabled()) { | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| const standing = standingState(bot); | ||
| const resultCoord = blockCoord(action.resultCoord); | ||
| const record = { | ||
| type: action.type, | ||
| item: action.item || heldItemName(bot), | ||
| previousBlock: action.previousBlock, | ||
| resultBlock: action.resultBlock ?? blockNameAt(bot, resultCoord), | ||
| resultCoord, | ||
| clickedBlock: blockCoord(action.clickedBlock), | ||
| clickedFace: action.clickedFace, | ||
| playerPos: entityPosition(bot), | ||
| yaw: actualYawDegrees(bot), | ||
| pitch: actualPitchDegrees(bot), | ||
| sneaking: getSneakState(bot), | ||
| standingOn: standing.position, | ||
| standingOnBlock: standing.blockName, | ||
| hand: action.hand || 'main_hand', | ||
| tick: getBotTick(bot), | ||
| timestamp: new Date().toISOString(), | ||
| }; | ||
| appendFileSync(getActionLogPath(bot), `${formatActionLogLine(record)}\n`, 'utf8'); | ||
| return true; | ||
| } catch (error) { | ||
| console.warn(`Failed to append action log: ${error.message}`); | ||
| return false; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not important enough to have its own file. Consider moving to README.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i removed the actionlogging md in the new commit and merged it more succintly in the readme