from phBot import *
import QtBind
import struct
import webbrowser
from threading import Timer
pName = 'FWheelManager'
pVersion = '1.0.1'
DISCORD_URL = 'https://discord.gg/eB9sGSMYBg'
OPCODE_REQUEST = 0x7151
OPCODE_RESPONSE = 0xB151
COLOR_PRIMARY = '#5b57e0'
COLOR_TEXT = '#2b3038'
COLOR_MUTED = '#9aa0ac'
COLOR_SUCCESS = '#1f9d63'
COLOR_WARNING = '#c98a1a'
COLOR_ERROR = '#d93a4d'
OFFSCREEN_X = 3000
MAX_LINES = 32
MAX_TARGET_VALUE = 2147483647
MODES = ('fate', 'fortune', 'pen')
MODE_LABELS = {'fate': 'Fate', 'fortune': 'Fortune', 'pen': 'Pen'}
MODE_DELAYS = {'fate': 0.55, 'fortune': 0.45, 'pen': 1.55}
NEXT_ITEM_DELAY = 0.85
CONSUMABLE_NAMES = {
'fate': ('Wheel of Fate', 'Wheel of Fortune'),
'fortune': ('Wheel of Fortune',),
'pen': ('Feather Pen of Fortune',)
}
WEAPON_STATS = ['STR', 'INT', 'Durability', 'Attack Rate', 'Evade Block', 'Critical']
SHIELD_STATS = ['STR', 'INT', 'Durability', 'Evade Critical', 'Block']
ARMOR_STATS = {
1: ['STR', 'INT', 'Durability', 'Parry Ratio', 'HP', 'MP'],
2: ['STR', 'INT', 'Durability', 'Parry Ratio'],
3: ['STR', 'INT', 'Durability', 'Parry Ratio', 'HP', 'MP', 'HP/MP Recovery'],
4: ['STR', 'INT', 'Durability', 'Parry Ratio', 'HP', 'MP'],
5: ['STR', 'INT', 'Durability', 'Parry Ratio'],
6: ['STR', 'INT', 'Durability', 'Parry Ratio']
}
ACCESSORY_BASE_STATS = [
'STR', 'INT', 'Frostbite Resist', 'Fire Resist', 'Lightning Resist',
'Poison Resist', 'Zombie Resist'
]
ACCESSORY_STATS = {
1: ACCESSORY_BASE_STATS + ['CSMP Resist', 'Sleep Resist'],
2: ACCESSORY_BASE_STATS + ['Stun Resist'],
3: ACCESSORY_BASE_STATS + ['Disease Resist', 'Fear Resist']
}
WEAPON_NAMES = {
2: 'Sword', 3: 'Blade', 4: 'Spear', 5: 'Glaive', 6: 'Bow',
7: 'One-handed Sword', 8: 'Two-handed Sword', 9: 'Axe',
10: 'Warlock Rod', 11: 'Staff', 12: 'Crossbow', 13: 'Dagger',
14: 'Harp', 15: 'Cleric Rod'
}
ARMOR_NAMES = {1: 'Head', 2: 'Shoulder', 3: 'Chest', 4: 'Legs', 5: 'Hands', 6: 'Foot'}
ACCESSORY_NAMES = {1: 'Earring', 2: 'Necklace', 3: 'Ring'}
CLASSIC_CODES = {
0x0A: 'STR', 0x0B: 'INT', 0x0C: 'Durability', 0x0F: 'Evade Critical',
0x10: 'Parry Ratio', 0x11: 'HP', 0x12: 'MP', 0x13: 'Frostbite Resist',
0x14: 'Lightning Resist', 0x15: 'Fire Resist', 0x16: 'Poison Resist',
0x17: 'Zombie Resist', 0x19: 'Block', 0x1A: 'Stun Resist',
0x1C: 'CSMP Resist', 0x1D: 'Disease Resist', 0x1E: 'Sleep Resist',
0x1F: 'Fear Resist', 0x39: 'STR', 0x53: 'STR', 0x54: 'STR',
0x55: 'STR', 0x3A: 'INT', 0x56: 'INT', 0x57: 'INT', 0x58: 'INT',
0x3B: 'Durability', 0x59: 'Durability', 0x5A: 'Durability',
0x47: 'Critical', 0x7D: 'Critical', 0x7E: 'Critical',
0x3C: 'Attack Rate', 0x5C: 'Attack Rate', 0x3D: 'Block Rate',
0x5F: 'Block Rate', 0x40: 'HP', 0x68: 'HP', 0x41: 'MP', 0x6B: 'MP',
0x49: 'Stun', 0x4A: 'HP/MP Recovery', 0x4B: 'Combustion',
0x4C: 'Disease', 0x4D: 'Sleep', 0x4E: 'Fear'
}
# High-degree weapon family verified from this server's Fortune responses.
# These codes overlap with classic meanings, so they must only be applied
# after the selected item has been classified as a weapon.
WEAPON_CONTEXT_CODES = {
0x69: 'STR', 0x6A: 'INT', 0x6B: 'Durability',
0x6C: 'Attack Rate', 0x6D: 'Evade Block', 0x77: 'Critical'
}
DG12_CODES = {
0x0049: 'STR', 0x004F: 'INT', 0x0067: 'Attack Rate',
0x0073: 'Evade Block', 0x008B: 'HP', 0x0097: 'MP', 0x010C: 'Critical'
}
IGNORED_RESPONSE_STATS = {'Athanasia', 'Solid', 'Luck', 'Repair'}
PEN_STAT_CODES = {
0x39: 'STR', 0x53: 'STR', 0x54: 'STR', 0x55: 'STR',
0x3A: 'INT', 0x56: 'INT', 0x57: 'INT', 0x58: 'INT',
0x3B: 'Durability', 0x59: 'Durability', 0x5A: 'Durability',
0x3C: 'Attack Rate', 0x5C: 'Attack Rate', 0x3D: 'Evade Block',
0x5F: 'Evade Block', 0x3E: 'Evade Critical', 0x3F: 'Parry Ratio',
0x65: 'Parry Ratio', 0x40: 'HP', 0x68: 'HP', 0x41: 'MP', 0x6B: 'MP',
0x42: 'Frostbite Resist', 0x43: 'Lightning Resist', 0x44: 'Fire Resist',
0x45: 'Poison Resist', 0x46: 'Zombie Resist', 0x47: 'Critical',
0x7D: 'Critical', 0x7E: 'Critical', 0x48: 'Block', 0x49: 'Stun Resist',
0x4A: 'HP/MP Recovery', 0x4B: 'Combustion Resist', 0x4C: 'Disease Resist',
0x4D: 'Sleep Resist', 0x4E: 'Fear Resist'
}
PEN_STATS = []
for _name in PEN_STAT_CODES.values():
if _name not in PEN_STATS:
PEN_STATS.append(_name)
states = {}
for _mode in MODES:
states[_mode] = {
'inventory': [], 'queue': [], 'targets': [], 'available': [],
'selected': None, 'index': 0, 'last_result': None
}
test_items = {mode: None for mode in MODES}
visible_mode = 'fate'
visible_page = 'setup'
active_mode = None
awaiting_response = False
single_roll = False
timer_token = 0
pending_timer = None
def plugin_log(message):
log('[%s] %s' % (pName, str(message)))
def fixed_width_text(content, width):
return ('
').format(width, content)
def html_safe(value):
return str(value).replace('&', '&').replace('<', '<').replace('>', '>')
def set_status(message, color=COLOR_MUTED):
QtBind.setText(gui, lbl_status, fixed_width_text(
'%s' % (color, html_safe(message)), 680))
def set_result(mode, message, color=COLOR_MUTED):
states[mode]['last_result'] = message
QtBind.setText(gui, result_labels[mode], fixed_width_text(
'%s' % (color, html_safe(message)), 680))
def discord_clicked():
try:
webbrowser.open(DISCORD_URL)
set_status('Opening Discord invite...', COLOR_SUCCESS)
except Exception as error:
plugin_log('Discord link error: %s' % error)
set_status('Could not open Discord invite', COLOR_ERROR)
def item_classification(item):
try:
data = get_item(int(item.get('model', 0)))
except Exception:
data = None
if not data or int(data.get('tid1', 0)) != 1:
return None
tid2 = int(data.get('tid2', 0))
tid3 = int(data.get('tid3', 0))
if tid2 == 6 and tid3 in WEAPON_NAMES:
return ('Weapon', WEAPON_NAMES[tid3], list(WEAPON_STATS))
if tid2 == 4 and tid3 in (1, 2):
return ('Shield', 'Chinese Shield' if tid3 == 1 else 'European Shield', list(SHIELD_STATS))
if tid2 in (1, 2, 3, 9, 10, 11) and tid3 in ARMOR_STATS:
family = {1: 'Garment', 2: 'Protector', 3: 'Armor', 9: 'Robe', 10: 'Light Armor', 11: 'Heavy Armor'}[tid2]
return ('Armor', '%s %s' % (family, ARMOR_NAMES[tid3]), list(ARMOR_STATS[tid3]))
if tid2 in (5, 12) and tid3 in ACCESSORY_STATS:
race = 'Chinese' if tid2 == 5 else 'European'
return ('Accessory', '%s %s' % (race, ACCESSORY_NAMES[tid3]), list(ACCESSORY_STATS[tid3]))
return None
def inventory_snapshot(mode):
inventory = get_inventory()
if not inventory or 'items' not in inventory:
return []
result = []
for slot, item in enumerate(inventory['items']):
if slot < 13 or not item:
continue
classification = item_classification(item)
if not classification:
continue
copied = dict(item)
copied['slot'] = slot
copied['subtype'] = classification[1]
copied['available_stats'] = classification[2]
result.append(copied)
return result
def find_inventory_item(slot):
inventory = get_inventory()
items = inventory.get('items', []) if inventory else []
return items[slot] if 0 <= slot < len(items) else None
def find_consumable_slot(mode):
inventory = get_inventory()
if not inventory or 'items' not in inventory:
return None
names = CONSUMABLE_NAMES[mode]
for slot, item in enumerate(inventory['items']):
item_name = str(item.get('name', '')) if item else ''
if any(name in item_name for name in names):
return slot
return None
def selected_item(mode):
widget = inventory_lists[mode]
index = QtBind.currentIndex(gui, widget)
values = states[mode]['inventory']
return values[index] if 0 <= index < len(values) else None
def format_item(item):
return 'Slot %d | +%s | %s' % (item['slot'], item.get('plus', 0), item.get('name', 'Unknown'))
def refresh_inventory(mode):
state = states[mode]
state['inventory'] = inventory_snapshot(mode)
QtBind.clear(gui, inventory_lists[mode])
for item in state['inventory']:
suffix = ' | %s' % item['subtype'] if mode == 'fortune' else ''
QtBind.append(gui, inventory_lists[mode], format_item(item) + suffix)
set_status('%s: %d eligible inventory item(s) found' % (
MODE_LABELS[mode], len(state['inventory'])),
COLOR_SUCCESS if state['inventory'] else COLOR_WARNING)
def fate_refresh_inventory(): refresh_inventory('fate')
def fortune_refresh_inventory(): refresh_inventory('fortune')
def pen_refresh_inventory(): refresh_inventory('pen')
def inspect_item(mode):
state = states[mode]
item = selected_item(mode)
if not item:
set_status('Select an inventory item first', COLOR_WARNING)
return
state['selected'] = {'slot': item['slot'], 'model': item['model']}
QtBind.setText(gui, selected_labels[mode], fixed_width_text(
'%s' % (COLOR_TEXT, html_safe(format_item(item))), 350))
if mode == 'fortune':
state['available'] = list(item['available_stats'])
state['targets'] = []
QtBind.clear(gui, stat_lists[mode])
QtBind.clear(gui, target_lists[mode])
for name in state['available']:
QtBind.append(gui, stat_lists[mode], name)
set_status('%s item selected; configure its target' % MODE_LABELS[mode], COLOR_MUTED)
def fate_inspect(): inspect_item('fate')
def fortune_inspect(): inspect_item('fortune')
def pen_inspect(): inspect_item('pen')
def refresh_targets(mode):
QtBind.clear(gui, target_lists[mode])
for target in states[mode]['targets']:
if mode == 'fortune':
text = '%s x%d' % (target['name'], target['count'])
else:
text = '%s >= %d total' % (target['name'], target['value'])
QtBind.append(gui, target_lists[mode], text)
def add_stat_target(mode):
name = QtBind.text(gui, stat_lists[mode])
if not name:
set_status('Select a stat first', COLOR_WARNING)
return
try:
value = int(QtBind.text(gui, target_inputs[mode]).strip())
except Exception:
value = 0
maximum = MAX_LINES if mode == 'fortune' else MAX_TARGET_VALUE
if value < 1 or value > maximum:
set_status('Target value is outside the allowed range', COLOR_ERROR)
return
key = 'count' if mode == 'fortune' else 'value'
for target in states[mode]['targets']:
if target['name'] == name:
target[key] = value
refresh_targets(mode)
set_status('Target updated: %s' % name, COLOR_SUCCESS)
return
states[mode]['targets'].append({'name': name, key: value})
refresh_targets(mode)
set_status('Target added: %s' % name, COLOR_SUCCESS)
def fortune_add_target(): add_stat_target('fortune')
def pen_add_target(): add_stat_target('pen')
def remove_stat_target(mode):
index = QtBind.currentIndex(gui, target_lists[mode])
if index < 0 or index >= len(states[mode]['targets']):
set_status('Select a target to remove', COLOR_WARNING)
return
removed = states[mode]['targets'].pop(index)
refresh_targets(mode)
set_status('Removed %s target' % removed['name'], COLOR_MUTED)
def fortune_remove_target(): remove_stat_target('fortune')
def pen_remove_target(): remove_stat_target('pen')
def queue_item(mode):
state = states[mode]
item = selected_item(mode)
if not item:
set_status('Select an inventory item first', COLOR_WARNING)
return
queued = {'slot': item['slot'], 'model': item['model'], 'name': item.get('name', 'Unknown')}
if mode == 'fate':
try:
target = int(QtBind.text(gui, target_inputs[mode]).strip())
except Exception:
target = 0
if target < 1 or target > MAX_LINES:
set_status('Blue-line target must be between 1 and 32', COLOR_ERROR)
return
queued.update({'target': target, 'last_count': None})
else:
if not state['targets']:
set_status('Add at least one stat target', COLOR_WARNING)
return
queued['targets'] = [dict(value) for value in state['targets']]
if mode == 'pen':
queued['totals'] = {}
for index, existing in enumerate(state['queue']):
if existing['slot'] == queued['slot']:
state['queue'][index] = queued
refresh_queue(mode)
set_status('%s queue target updated' % MODE_LABELS[mode], COLOR_SUCCESS)
return
state['queue'].append(queued)
refresh_queue(mode)
set_status('Item added to the %s queue' % MODE_LABELS[mode], COLOR_SUCCESS)
def fate_queue_item(): queue_item('fate')
def fortune_queue_item(): queue_item('fortune')
def pen_queue_item(): queue_item('pen')
def target_summary(mode, item):
if mode == 'fate':
current = '?' if item['last_count'] is None else item['last_count']
return 'blue %s/%d' % (current, item['target'])
if mode == 'fortune':
return ', '.join('%s x%d' % (x['name'], x['count']) for x in item['targets'])
return ', '.join('%s %s/%d' % (
x['name'], item['totals'].get(x['name'], '?'), x['value']) for x in item['targets'])
def refresh_queue(mode):
QtBind.clear(gui, queue_lists[mode])
state = states[mode]
for index, item in enumerate(state['queue']):
marker = '>' if active_mode == mode and index == state['index'] else ' '
QtBind.append(gui, queue_lists[mode], '%s Slot %d | %s | %s' % (
marker, item['slot'], target_summary(mode, item), item['name']))
def remove_queue_item(mode):
if active_mode is not None:
set_status('Stop the active operation before editing a queue', COLOR_WARNING)
return
index = QtBind.currentIndex(gui, queue_lists[mode])
if index < 0 or index >= len(states[mode]['queue']):
set_status('Select a queued item first', COLOR_WARNING)
return
del states[mode]['queue'][index]
refresh_queue(mode)
set_status('Queued item removed', COLOR_MUTED)
def clear_queue(mode):
if active_mode is not None:
set_status('Stop the active operation before clearing a queue', COLOR_WARNING)
return
states[mode]['queue'][:] = []
refresh_queue(mode)
set_status('%s queue cleared' % MODE_LABELS[mode], COLOR_MUTED)
def fate_remove_queue(): remove_queue_item('fate')
def fortune_remove_queue(): remove_queue_item('fortune')
def pen_remove_queue(): remove_queue_item('pen')
def fate_clear_queue(): clear_queue('fate')
def fortune_clear_queue(): clear_queue('fortune')
def pen_clear_queue(): clear_queue('pen')
def invalidate_timer():
global pending_timer, timer_token, awaiting_response
timer_token += 1
awaiting_response = False
if pending_timer:
try:
pending_timer.cancel()
except Exception:
pass
pending_timer = None
def schedule_request(delay, mode):
global pending_timer
token = timer_token
def callback():
if active_mode == mode and token == timer_token:
send_request(mode)
pending_timer = Timer(delay, callback)
pending_timer.daemon = True
pending_timer.start()
def active_item(mode):
if single_roll and test_items.get(mode):
return test_items[mode]
state = states[mode]
return state['queue'][state['index']] if 0 <= state['index'] < len(state['queue']) else None
def send_request(mode):
global awaiting_response
if active_mode != mode or awaiting_response:
return
item = active_item(mode)
if not item:
stop_operation('All %s targets reached' % MODE_LABELS[mode], COLOR_SUCCESS)
return
live = find_inventory_item(item['slot'])
if not live or live.get('model') != item['model']:
stop_operation('Item moved or changed at slot %d' % item['slot'], COLOR_ERROR)
return
consumable_slot = find_consumable_slot(mode)
if consumable_slot is None:
stop_operation('%s consumable not found' % MODE_LABELS[mode], COLOR_ERROR)
return
if item['slot'] > 255 or consumable_slot > 255:
stop_operation('Inventory slot is outside packet range', COLOR_ERROR)
return
awaiting_response = True
inject_joymax(OPCODE_REQUEST, b'\x02\x19\x02' + bytes([item['slot'], consumable_slot]), False)
set_status('%s is rolling slot %d...' % (MODE_LABELS[mode], item['slot']), COLOR_WARNING)
def start_mode(mode):
global active_mode, single_roll
if active_mode is not None:
set_status('%s operation is already active' % MODE_LABELS[active_mode], COLOR_WARNING)
return
state = states[mode]
if not state['queue']:
set_status('Add at least one item to the %s queue' % MODE_LABELS[mode], COLOR_WARNING)
return
for item in state['queue']:
live = find_inventory_item(item['slot'])
if not live or live.get('model') != item['model']:
set_status('Queued item changed at slot %d' % item['slot'], COLOR_ERROR)
return
if find_consumable_slot(mode) is None:
set_status('%s consumable not found' % MODE_LABELS[mode], COLOR_ERROR)
return
state['index'] = 0
active_mode = mode
single_roll = False
invalidate_timer()
refresh_all_queues()
set_status('%s automation started' % MODE_LABELS[mode], COLOR_SUCCESS)
send_request(mode)
def test_mode(mode):
global active_mode, single_roll
if active_mode is not None:
set_status('%s operation is already active' % MODE_LABELS[active_mode], COLOR_WARNING)
return
item = selected_item(mode)
if not item:
set_status('Select an inventory item first', COLOR_WARNING)
return
test_items[mode] = {
'slot': item['slot'], 'model': item['model'], 'name': item.get('name', 'Unknown'),
'target': 1, 'last_count': None, 'targets': [], 'totals': {}
}
active_mode = mode
single_roll = True
invalidate_timer()
send_request(mode)
set_status('One-roll %s test sent; waiting for 0xB151...' % MODE_LABELS[mode], COLOR_WARNING)
def fate_start(): start_mode('fate')
def fortune_start(): start_mode('fortune')
def pen_start(): start_mode('pen')
def fate_test(): test_mode('fate')
def fortune_test(): test_mode('fortune')
def pen_test(): test_mode('pen')
def stop_operation(message='Operation stopped', color=COLOR_WARNING):
global active_mode, single_roll
old_mode = active_mode
if old_mode:
test_items[old_mode] = None
active_mode = None
single_roll = False
invalidate_timer()
if old_mode:
refresh_queue(old_mode)
set_status(message, color)
def stop_clicked():
if active_mode is None:
set_status('No operation is running', COLOR_MUTED)
else:
stop_operation()
def parse_fate(data):
if len(data) < 43 or data[0] != 1 or data[2] != 1:
return None
count = data[26]
if count < 1 or count > MAX_LINES + 1:
return None
blue_count = count - 1
return (data[4], blue_count) if len(data) >= 43 + blue_count * 8 else None
def parse_fortune(data, item=None):
classification = None
if item is not None:
live_item = find_inventory_item(item['slot'])
classification = item_classification(live_item) if live_item else None
candidates = []
for start in range(max(0, len(data) - 7)):
cursor = start
options = []
while cursor + 8 <= len(data):
group = data[cursor:cursor + 8]
if (group[0] == 0 or group[1] != 2 or group[2] != 0 or group[3] != 0 or
group[4] == 0 or group[5] != 0 or group[6] != 0 or group[7] != 0):
break
name = CLASSIC_CODES.get(group[0])
if classification and classification[0] == 'Weapon':
name = WEAPON_CONTEXT_CODES.get(group[0], name)
options.append({'name': name or 'Unknown code 0x%02X' % group[0],
'value': group[4]})
cursor += 8
if options:
candidates.append((start, options))
if candidates:
candidates.sort(key=lambda entry: (-len(entry[1]), entry[0]))
return candidates[0][1]
if len(data) >= 35:
options = []
for index in range(len(data[35:]) // 8):
group = data[35 + index * 8:43 + index * 8]
code = struct.unpack_from(' len(data):
return None
count_position = end + 22
internal_position = end + 30
records_start = end + 31
count = data[count_position]
visible = count - 1
if count < 2 or visible > MAX_LINES or data[internal_position] != 0x40:
return None
if records_start + visible * 8 > len(data):
return None
totals = {}
for index in range(visible):
group = data[records_start + index * 8:records_start + (index + 1) * 8]
if (group[1] != 2 or group[2] != 0 or group[3] != 0 or group[4] == 0 or
group[5] != 0 or group[6] != 0 or group[7] != 0):
return None
name = PEN_STAT_CODES.get(group[0])
if not name:
return None
totals[name] = totals.get(name, 0) + group[4]
return data[end], totals
def process_response(data):
global awaiting_response, single_roll
mode = active_mode
if not mode or not awaiting_response:
return
awaiting_response = False
item = active_item(mode)
if not item:
stop_operation('No active item; operation stopped', COLOR_ERROR)
return
reached = False
if mode == 'fate':
parsed = parse_fate(data)
if not parsed or parsed[0] != item['slot']:
stop_operation('Fate response could not be verified', COLOR_ERROR)
return
item['last_count'] = parsed[1]
reached = parsed[1] >= item.get('target', 1)
set_result(mode, 'Slot %d: %d blue line(s)' % (item['slot'], parsed[1]), COLOR_SUCCESS)
elif mode == 'fortune':
options = parse_fortune(data, item)
if not options:
stop_operation('Fortune response had no verified stats', COLOR_ERROR)
return
valid, reason = normalize_fortune_options(item, options)
if not valid:
plugin_log('Fortune unsafe layout 0xB151 raw (%d bytes): %s%s' % (
len(data), ' '.join('%02X' % value for value in data[:512]),
' ...' if len(data) > 512 else ''))
stop_operation(reason, COLOR_ERROR)
return
counts = {}
for option in options:
counts[option['name']] = counts.get(option['name'], 0) + 1
reached = all(counts.get(x['name'], 0) >= x['count'] for x in item.get('targets', []))
set_result(mode, ', '.join('%s x%d' % pair for pair in sorted(counts.items())), COLOR_SUCCESS)
else:
parsed = parse_pen(data)
if not parsed or parsed[0] != item['slot']:
stop_operation('Pen response could not be verified', COLOR_ERROR)
return
item['totals'] = parsed[1]
reached = all(parsed[1].get(x['name'], 0) >= x['value'] for x in item.get('targets', []))
set_result(mode, ', '.join('%s=%d' % pair for pair in sorted(parsed[1].items())), COLOR_SUCCESS)
plugin_log('%s slot %d response processed' % (MODE_LABELS[mode], item['slot']))
if single_roll:
single_roll = False
stop_operation('One-roll %s test completed' % MODE_LABELS[mode], COLOR_SUCCESS)
return
if reached:
states[mode]['index'] += 1
refresh_queue(mode)
if states[mode]['index'] >= len(states[mode]['queue']):
stop_operation('All %s targets reached' % MODE_LABELS[mode], COLOR_SUCCESS)
else:
schedule_request(NEXT_ITEM_DELAY, mode)
else:
refresh_queue(mode)
schedule_request(MODE_DELAYS[mode], mode)
def handle_joymax(opcode, data):
try:
if opcode == OPCODE_RESPONSE and active_mode is not None:
process_response(data)
except Exception as error:
plugin_log('Packet processing error for 0x%04X: %s' % (opcode, error))
if opcode == OPCODE_RESPONSE and active_mode is not None:
stop_operation('Packet processing failed; operation stopped', COLOR_ERROR)
return True
def disconnected():
if active_mode is not None or awaiting_response:
stop_operation('Disconnected; operation stopped', COLOR_ERROR)
else:
invalidate_timer()
set_status('Disconnected', COLOR_MUTED)
def move_widgets(widgets, visible):
for widget, x, y in widgets:
QtBind.move(gui, widget, x if visible else OFFSCREEN_X, y)
def show_view(mode, page='setup'):
global visible_mode, visible_page
visible_mode = mode
visible_page = page
for current_mode in MODES:
move_widgets(setup_widgets[current_mode], current_mode == mode and page == 'setup')
move_widgets(queue_widgets[current_mode], current_mode == mode and page == 'queue')
QtBind.setText(gui, lbl_view, fixed_width_text(
'%s / %s' % (
COLOR_PRIMARY, MODE_LABELS[mode].upper(), page.upper()), 160))
refresh_queue(mode)
def show_fate(): show_view('fate', 'setup')
def show_fortune(): show_view('fortune', 'setup')
def show_pen(): show_view('pen', 'setup')
def fate_setup(): show_view('fate', 'setup')
def fortune_setup(): show_view('fortune', 'setup')
def pen_setup(): show_view('pen', 'setup')
def fate_queue(): show_view('fate', 'queue')
def fortune_queue(): show_view('fortune', 'queue')
def pen_queue(): show_view('pen', 'queue')
def refresh_all_queues():
for mode in MODES:
refresh_queue(mode)
gui = QtBind.init(__name__, pName)
QtBind.createLabel(gui, '✦ %s' % (COLOR_PRIMARY, pName), 12, 6)
QtBind.createLabel(gui, 'v%s' % (COLOR_MUTED, pVersion), 175, 12)
QtBind.createButton(gui, 'show_fate', 'Fate', 245, 6)
QtBind.createButton(gui, 'show_fortune', 'Fortune', 300, 6)
QtBind.createButton(gui, 'show_pen', 'Pen', 370, 6)
QtBind.createButton(gui, 'discord_clicked', u'\U0001f4ac Discord', 462, 6)
QtBind.createLabel(gui, u'⚜ Made By FascinaTe' % COLOR_PRIMARY, 565, 11)
QtBind.createLineEdit(gui, '', 12, 30, 716, 1)
lbl_view = QtBind.createLabel(gui, fixed_width_text('FATE / SETUP' % COLOR_PRIMARY, 160), 12, 42)
lbl_status = QtBind.createLabel(gui, fixed_width_text('Ready' % COLOR_MUTED, 680), 12, 294)
setup_widgets = {mode: [] for mode in MODES}
queue_widgets = {mode: [] for mode in MODES}
inventory_lists = {}
selected_labels = {}
stat_lists = {}
target_lists = {}
target_inputs = {}
queue_lists = {}
result_labels = {}
for _mode_index, _mode in enumerate(MODES):
_hidden = _mode != 'fate'
_x = OFFSCREEN_X if _hidden else 12
_title = QtBind.createLabel(gui, '%s ITEM SETUP' % (COLOR_PRIMARY, MODE_LABELS[_mode].upper()), _x, 66)
_refresh = QtBind.createButton(gui, _mode + '_refresh_inventory', '↻ Refresh', _x, 86)
_inspect = QtBind.createButton(gui, _mode + '_inspect', 'Select Item', OFFSCREEN_X if _hidden else 105, 86)
_test = QtBind.createButton(gui, _mode + '_test', 'Test One Roll', OFFSCREEN_X if _hidden else 205, 86)
_queue_page = QtBind.createButton(gui, _mode + '_queue', 'Queue & Start →', OFFSCREEN_X if _hidden else 610, 42)
_list = QtBind.createList(gui, _x, 114, 360, 130)
_selected = QtBind.createLabel(gui, fixed_width_text('No item selected' % COLOR_MUTED, 350), _x, 252)
inventory_lists[_mode] = _list
selected_labels[_mode] = _selected
setup_widgets[_mode].extend([
(_title, 12, 66), (_refresh, 12, 86), (_inspect, 105, 86), (_test, 205, 86),
(_queue_page, 610, 42), (_list, 12, 114), (_selected, 12, 252)
])
if _mode == 'fate':
_target_label = QtBind.createLabel(gui, 'TARGET BLUE LINES' % COLOR_PRIMARY, 400 if not _hidden else OFFSCREEN_X, 70)
_input = QtBind.createLineEdit(gui, '6', 400 if not _hidden else OFFSCREEN_X, 94, 48, 20)
_add = QtBind.createButton(gui, 'fate_queue_item', 'Add Item to Queue', 460 if not _hidden else OFFSCREEN_X, 90)
target_inputs[_mode] = _input
setup_widgets[_mode].extend([(_target_label, 400, 70), (_input, 400, 94), (_add, 460, 90)])
else:
_stats_label = QtBind.createLabel(gui, 'AVAILABLE STATS' % COLOR_PRIMARY, OFFSCREEN_X, 70)
_targets_label = QtBind.createLabel(gui, 'ITEM TARGETS' % COLOR_PRIMARY, OFFSCREEN_X, 70)
_stats = QtBind.createList(gui, OFFSCREEN_X, 92, 145, 120)
_targets = QtBind.createList(gui, OFFSCREEN_X, 92, 155, 120)
_input = QtBind.createLineEdit(gui, '1' if _mode == 'fortune' else '10', OFFSCREEN_X, 222, 50, 20)
_add_target = QtBind.createButton(gui, _mode + '_add_target', 'Add / Update', OFFSCREEN_X, 218)
_remove_target = QtBind.createButton(gui, _mode + '_remove_target', 'Remove Target', OFFSCREEN_X, 248)
_add_queue = QtBind.createButton(gui, _mode + '_queue_item', 'Add Item to Queue', OFFSCREEN_X, 248)
stat_lists[_mode] = _stats
target_lists[_mode] = _targets
target_inputs[_mode] = _input
setup_widgets[_mode].extend([
(_stats_label, 400, 70), (_targets_label, 560, 70), (_stats, 400, 92),
(_targets, 560, 92), (_input, 400, 222), (_add_target, 460, 218),
(_remove_target, 560, 218), (_add_queue, 400, 248)
])
for _name in PEN_STATS:
QtBind.append(gui, stat_lists['pen'], _name)
for _mode in MODES:
_back = QtBind.createButton(gui, _mode + '_setup', '← Item Setup', OFFSCREEN_X, 42)
_header = QtBind.createLabel(gui, '%s AUTOMATION QUEUE' % (COLOR_PRIMARY, MODE_LABELS[_mode].upper()), OFFSCREEN_X, 70)
_queue = QtBind.createList(gui, OFFSCREEN_X, 94, 535, 118)
_remove = QtBind.createButton(gui, _mode + '_remove_queue', 'Remove Item', OFFSCREEN_X, 94)
_clear = QtBind.createButton(gui, _mode + '_clear_queue', 'Clear Queue', OFFSCREEN_X, 126)
_start = QtBind.createButton(gui, _mode + '_start', '▶ START', OFFSCREEN_X, 224)
_stop = QtBind.createButton(gui, 'stop_clicked', '■ STOP', OFFSCREEN_X, 224)
_result = QtBind.createLabel(gui, fixed_width_text('No verified result yet' % COLOR_MUTED, 680), OFFSCREEN_X, 260)
queue_lists[_mode] = _queue
result_labels[_mode] = _result
queue_widgets[_mode].extend([
(_back, 610, 42), (_header, 12, 70), (_queue, 12, 94), (_remove, 560, 94),
(_clear, 560, 126), (_start, 12, 224), (_stop, 105, 224), (_result, 12, 260)
])
log('[%s] Loaded - ⚜ Made By FascinaTe' % pName)