Released with files, no release notes written yet.
plugin
iSRO-R
v2.1
Shining Stone
by @CK3 · 1d ago · updated 1d ago · 15 views
Crafting automatic all Shining Stones from your inventory.
Changelog
▸ ShiningStone.py v2.1 Download
# -*- coding: utf-8 -*-
#
# ShiningStone - plugin for phBot
# -------------------------------
# Crafts "Shining Stone" from Blue Stone + Black Stone, over and over.
#
# The craft packet (opcode 0x7538) is replayed on a loop, with the inventory
# slots of the two materials patched in on every round:
#
# 01 | uint32 | uint16 len | recipe codename | mat count | slot | slot | qty
#
from phBot import *
from threading import Timer
import QtBind
import os
import json
pName = 'ShiningStone'
pVersion = '2.1'
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), pName + '.json')
# recorded craft packet - used when the json has no recipe of its own
DEFAULT_RECIPE = [[0x7538, '011d0000001f004d4b5f52435f54524144455f4d41544552'
'49414c5f4c4947485453544f4e4502454901']]
DEFAULT_BLUE_SLOT = 69
DEFAULT_BLACK_SLOT = 73
started = False
_timer = None
rounds = 0
# per-quality statistics: {exact item name: crafted so far}
stats = {}
fails = 0
_prev_counts = {}
# craft packets + the slots the materials were in when they were recorded
recipe = list(DEFAULT_RECIPE)
learn_blue = DEFAULT_BLUE_SLOT
learn_black = DEFAULT_BLACK_SLOT
_step = 0
# ============================================================================
# GUI
# ============================================================================
gui = QtBind.init(__name__, pName)
QtBind.createLabel(gui, 'S H I N I N G S T O N E v%s' % pVersion, 10, 6)
QtBind.createLabel(gui, 'Material 1 (name contains):', 10, 34)
txtBlue = QtBind.createLineEdit(gui, 'Blue Stone', 175, 32, 150, 20)
QtBind.createLabel(gui, 'Material 2 (name contains):', 10, 58)
txtBlack = QtBind.createLineEdit(gui, 'Black Stone', 175, 56, 150, 20)
QtBind.createLabel(gui, 'Result (name contains):', 10, 82)
txtResult = QtBind.createLineEdit(gui, 'Shining Stone', 175, 80, 150, 20)
QtBind.createLabel(gui, 'Delay between crafts (ms):', 10, 112)
txtDelay = QtBind.createLineEdit(gui, '3000', 175, 110, 55, 20)
QtBind.createLabel(gui, 'Delay between packets (ms):', 10, 136)
txtStepDelay = QtBind.createLineEdit(gui, '500', 175, 134, 55, 20)
QtBind.createLabel(gui, 'Stop after N crafts (0 = all):', 10, 160)
txtCount = QtBind.createLineEdit(gui, '0', 175, 158, 55, 20)
chkStopEmpty = QtBind.createCheckBox(gui, 'on_dummy', 'Stop when a material runs out', 10, 186)
QtBind.createButton(gui, 'on_check', ' Check materials ', 10, 214)
QtBind.createButton(gui, 'on_save', ' Save ', 145, 214)
QtBind.createButton(gui, 'on_stats', ' Stats ', 220, 214)
QtBind.createButton(gui, 'on_reset_stats', ' Reset stats ', 290, 214)
btnStartStop = QtBind.createButton(gui, 'on_startstop', ' Start ', 10, 244)
lblStatus = QtBind.createLabel(gui, 'stopped', 120, 248)
QtBind.createLabel(gui, 'Log:', 420, 30)
lstLog = QtBind.createList(gui, 420, 50, 270, 105)
QtBind.createLabel(gui, 'Stats:', 420, 160)
lstStats = QtBind.createList(gui, 420, 178, 270, 82)
QtBind.createLabel(gui, 'Author : CK3 @ iLog1K.net', 10, 272)
# ============================================================================
# Helpers
# ============================================================================
def on_dummy(*_):
pass
def set_status(txt):
try:
QtBind.setText(gui, lblStatus, txt)
except Exception:
pass
def add_log(text):
log('[%s] %s' % (pName, text))
try:
QtBind.append(gui, lstLog, text)
except Exception:
pass
def add_stat(text):
try:
QtBind.append(gui, lstStats, text)
except Exception:
pass
def clear_stats_box():
"""QtBind has no documented clear(), so try what exists and fall back to a
separator line if the list cannot be emptied."""
for fname in ('clearList', 'clear', 'clearItems'):
fn = getattr(QtBind, fname, None)
if fn:
try:
fn(gui, lstStats)
return
except Exception:
pass
add_stat('-' * 34)
def _int(widget, default):
try:
return max(0, int(QtBind.text(gui, widget).strip()))
except Exception:
return default
def delay_sec():
return _int(txtDelay, 3000) / 1000.0
def step_delay_sec():
return _int(txtStepDelay, 500) / 1000.0
def name_blue():
return QtBind.text(gui, txtBlue).strip()
def name_black():
return QtBind.text(gui, txtBlack).strip()
def name_result():
return QtBind.text(gui, txtResult).strip()
def find_item(namepart):
"""Returns (first_slot, total_quantity) for an item whose name contains
namepart. (None, 0) when not found."""
if not namepart:
return None, 0
inv = get_inventory()
if not inv or 'items' not in inv:
return None, 0
first = None
total = 0
low = namepart.lower()
for slot, item in enumerate(inv['items']):
if not item or slot <= 12:
continue
if low in (item.get('name') or '').lower():
if first is None:
first = slot
total += int(item.get('quantity', 1) or 1)
return first, total
def count_by_name(namepart):
"""{exact item name: total quantity} for every stack matching namepart."""
res = {}
if not namepart:
return res
inv = get_inventory()
if not inv or 'items' not in inv:
return res
low = namepart.lower()
for slot, item in enumerate(inv['items']):
if not item or slot <= 12:
continue
nm = item.get('name') or ''
if low in nm.lower():
res[nm] = res.get(nm, 0) + int(item.get('quantity', 1) or 1)
return res
def on_stats():
clear_stats_box()
# what is in the inventory right now, per quality
cur = count_by_name(name_result())
add_stat('--- in inventory ---')
if cur:
for nm, q in sorted(cur.items(), key=lambda kv: -kv[1]):
add_stat(' %s : %d' % (nm, q))
add_stat(' TOTAL : %d' % sum(cur.values()))
else:
add_stat(' none')
# what this session actually produced
total = sum(stats.values())
if not total:
add_stat('--- crafted this session: nothing yet ---')
return
add_stat('--- crafted this session: %d in %d round(s) ---' % (total, rounds))
for nm, q in sorted(stats.items(), key=lambda kv: -kv[1]):
add_stat(' %s : %d (%.1f%%)' % (nm, q, 100.0 * q / total))
if fails:
add_stat(' rounds with no gain: %d (%.1f%%)'
% (fails, 100.0 * fails / max(1, rounds)))
def on_reset_stats():
global stats, fails, _prev_counts
stats = {}
fails = 0
_prev_counts = count_by_name(name_result())
add_log('Stats reset.')
def on_check():
bs, bq = find_item(name_blue())
ks, kq = find_item(name_black())
_rs, rq = find_item(name_result())
add_log('%s: slot %s x%d | %s: slot %s x%d | %s: x%d' %
(name_blue(), bs, bq, name_black(), ks, kq, name_result(), rq))
# ============================================================================
# Config
# ============================================================================
def on_save(*_):
try:
data = {
'blue': name_blue(),
'black': name_black(),
'result': name_result(),
'delay': _int(txtDelay, 3000),
'step_delay': _int(txtStepDelay, 500),
'count': _int(txtCount, 0),
'stop_empty': bool(QtBind.isChecked(gui, chkStopEmpty)),
'recipe': recipe,
'learn_blue': learn_blue,
'learn_black': learn_black,
}
with open(CONFIG_FILE, 'w') as f:
json.dump(data, f, indent=2)
add_log('Saved.')
except Exception as e:
add_log('Save error: %s' % e)
def load_config():
global recipe, learn_blue, learn_black
try:
if not os.path.isfile(CONFIG_FILE):
QtBind.setChecked(gui, chkStopEmpty, True)
return
with open(CONFIG_FILE, 'r') as f:
data = json.load(f)
QtBind.setText(gui, txtBlue, str(data.get('blue', 'Blue Stone')))
QtBind.setText(gui, txtBlack, str(data.get('black', 'Black Stone')))
QtBind.setText(gui, txtResult, str(data.get('result', 'Shining Stone')))
QtBind.setText(gui, txtDelay, str(data.get('delay', 3000)))
QtBind.setText(gui, txtStepDelay, str(data.get('step_delay', 500)))
QtBind.setText(gui, txtCount, str(data.get('count', 0)))
QtBind.setChecked(gui, chkStopEmpty, bool(data.get('stop_empty', True)))
recipe = data.get('recipe') or list(DEFAULT_RECIPE)
learn_blue = int(data.get('learn_blue', DEFAULT_BLUE_SLOT))
learn_black = int(data.get('learn_black', DEFAULT_BLACK_SLOT))
except Exception as e:
add_log('Config read error: %s' % e)
# ============================================================================
# Replay
# ============================================================================
def scan_start(raw):
"""Offset where slot patching may begin.
The craft packet is flag | uint32 | uint16 len | recipe codename | mats...
and the codename is plain ascii, so letters like 'E' (0x45) / 'I' (0x49)
collide with slot numbers. Never touch anything before the codename ends.
"""
try:
if len(raw) >= 7:
slen = raw[5] | (raw[6] << 8)
end = 7 + slen
if 0 < slen and end <= len(raw) and all(32 <= b < 127 for b in raw[7:end]):
return end
except Exception:
pass
return 0
def patch_payload(raw, blue_slot, black_slot):
"""Replace the slot bytes captured while recording with the current ones."""
out = bytearray(raw)
if blue_slot == learn_blue and black_slot == learn_black:
return bytes(out) # slots did not move, replay as recorded
hits = []
for i in range(scan_start(raw), len(out)):
if learn_blue >= 0 and out[i] == learn_blue and blue_slot is not None:
out[i] = blue_slot
hits.append('%d:blue' % i)
elif learn_black >= 0 and out[i] == learn_black and black_slot is not None:
out[i] = black_slot
hits.append('%d:black' % i)
if hits:
add_log('slots moved, patched offsets %s' % ', '.join(hits))
return bytes(out)
def send_step():
global _timer, _step
if not started:
return
if _step >= len(recipe):
# whole sequence sent -> wait, then check the result and go again
_timer = Timer(delay_sec(), finish_round)
_timer.start()
return
blue_slot, _bq = find_item(name_blue())
black_slot, _kq = find_item(name_black())
op, hx = recipe[_step]
try:
raw = bytes.fromhex(hx)
except Exception:
add_log('Corrupt recipe packet, stopping.')
stop()
return
inject_joymax(op, patch_payload(raw, blue_slot, black_slot), False)
_step += 1
_timer = Timer(step_delay_sec(), send_step)
_timer.start()
def finish_round():
global _timer, rounds, fails, _prev_counts
if not started:
return
rounds += 1
# what changed in the inventory since the previous round tells us which
# quality dropped - no need to parse the crafting chat message
cur = count_by_name(name_result())
gains = []
for nm, q in cur.items():
diff = q - _prev_counts.get(nm, 0)
if diff > 0:
stats[nm] = stats.get(nm, 0) + diff
gains.append('%s +%d' % (nm, diff))
_prev_counts = cur
if not gains:
fails += 1
rq = sum(cur.values())
_bs, bq = find_item(name_blue())
_ks, kq = find_item(name_black())
set_status('round %d | %s x%d | mats %d / %d' % (rounds, name_result(), rq, bq, kq))
add_log('Round %d: %s (total x%d, mats left: %d / %d)'
% (rounds, ', '.join(gains) if gains else 'nothing gained', rq, bq, kq))
if rounds % 25 == 0:
on_stats()
target = _int(txtCount, 0)
if target > 0 and rounds >= target:
add_log('Target reached (%d crafts). Stopping.' % target)
stop()
return
if QtBind.isChecked(gui, chkStopEmpty) and (bq <= 0 or kq <= 0):
add_log('Out of materials. Stopping.')
stop()
return
craft_once()
def craft_once():
global _step
if not started:
return
blue_slot, _bq = find_item(name_blue())
black_slot, _kq = find_item(name_black())
if blue_slot is None or black_slot is None:
add_log('Material missing in inventory. Stopping.')
stop()
return
_step = 0
send_step()
# ============================================================================
# Start / Stop
# ============================================================================
def on_startstop():
global started, rounds
if started:
stop()
return
if not recipe:
add_log('No craft packet available.')
return
blue_slot, bq = find_item(name_blue())
black_slot, kq = find_item(name_black())
if blue_slot is None or black_slot is None:
add_log('Materials not in inventory (%s / %s).' % (name_blue(), name_black()))
return
started = True
rounds = 0
on_reset_stats() # also takes the baseline inventory snapshot
QtBind.setText(gui, btnStartStop, ' Stop ')
set_status('running')
add_log('Started. Materials: %d / %d.' % (bq, kq))
craft_once()
def stop():
global started, _timer
if started:
on_stats()
started = False
QtBind.setText(gui, btnStartStop, ' Start ')
if _timer:
try:
_timer.cancel()
except Exception:
pass
_timer = None
set_status('stopped')
# ============================================================================
# Init
# ============================================================================
load_config()
log('[%s] v%s loaded OK.' % (pName, pVersion))
Sign in to propose something or back an existing proposal.
No proposals yet. Be the first to suggest something.
Sign in to join the discussion.
No comments yet.
Sign in to report a bug.
No bug reports. Nice.