-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathworld_hopper.py
More file actions
491 lines (419 loc) · 20.4 KB
/
world_hopper.py
File metadata and controls
491 lines (419 loc) · 20.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
import os
import json
import time
import datetime
import re
import webbrowser
import vrchatapi
import vrcx_utils
from vrcx_utils import add_favorite_world
from vrchatapi import InstanceType, InstanceRegion, CreateInstanceRequest
from vrchatapi.rest import ApiException
from vrchatapi.api import AuthenticationApi
def clean_for_json(obj):
if hasattr(obj, "to_dict"):
return clean_for_json(obj.to_dict())
elif isinstance(obj, dict):
return {k: clean_for_json(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [clean_for_json(item) for item in obj]
elif isinstance(obj, (datetime.datetime, datetime.date)):
return obj.isoformat()
else:
return obj
def get_unity_version(created):
ups = (created.get('world') or {}).get('unity_packages') or []
return ups[-1].get('unity_version') if ups else 'unknown'
def load_co_explorers(file_path):
"""Load co-explorers from JSON file. Returns list of dicts with 'uid' and 'name'."""
if not os.path.exists(file_path):
return []
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data if isinstance(data, list) else []
except (json.JSONDecodeError, IOError):
return []
def save_co_explorers(file_path, explorers):
"""Save co-explorers to JSON file."""
# Ensure directory exists
dir_path = os.path.dirname(file_path)
if dir_path and not os.path.exists(dir_path):
os.makedirs(dir_path, exist_ok=True)
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(explorers, f, indent=2, ensure_ascii=False)
print(f"Co-explorers list saved. Total: {len(explorers)}")
def get_instance_users(api_client, world_id, instance_id):
"""Fetch users currently in the specified instance."""
try:
api_instance = vrchatapi.InstancesApi(api_client)
instance = api_instance.get_instance(world_id, instance_id)
if not hasattr(instance, 'users') or instance.users is None:
print("No users data available for this instance (you may not own it)")
return []
users_list = []
for user in instance.users:
users_list.append({
'uid': user.id,
'name': user.display_name
})
return users_list
except ApiException as e:
print(f"Error fetching instance users: {e}")
return []
def get_user_by_id(api_client, user_id):
"""Fetch user information by ID."""
try:
api_users = vrchatapi.UsersApi(api_client)
user = api_users.get_user(user_id)
return {
'uid': user.id,
'name': user.display_name
}
except ApiException as e:
print(f"Error fetching user: {e}")
return None
def display_co_explorers(explorers):
"""Display current co-explorers list nicely."""
if not explorers:
print("No co-explorers in list.")
return
print("\n--- Current Co-Explorers List ---")
for i, explorer in enumerate(explorers, 1):
print(f"{i}. {explorer['name']} ({explorer['uid']})")
print("-" * 40)
def get_current_presence_world_id(api_client):
"""Fetches the ID of the world the user is currently physically standing in."""
try:
api_auth = AuthenticationApi(api_client)
user = api_auth.get_current_user()
if user and user.presence and user.presence.world:
return user.presence.world
return None
except Exception as e:
print(f"Could not fetch presence: {e}")
return None
def handle_update_command(api_client, explorers, current_user_id):
"""Update list with users from current instance."""
try:
api_auth = AuthenticationApi(api_client)
current_user = api_auth.get_current_user()
# Get world and instance from presence
presence = getattr(current_user, 'presence', None)
if not presence:
print("No presence data available.")
return explorers
world_id = getattr(presence, 'world', None)
instance_id = getattr(presence, 'instance', None)
if not world_id or not instance_id:
print(f"You are not in a valid world instance. world={world_id}, instance={instance_id}")
return explorers
instance_users = get_instance_users(api_client, world_id, instance_id)
if not instance_users:
print("No users found in your current instance.")
return explorers
# Filter out yourself from the list
instance_users = [user for user in instance_users if user['uid'] != current_user_id]
# Replace the list with instance users
explorers = instance_users
print(f"\nUpdated co-explorers list with {len(explorers)} users from your current instance:")
display_co_explorers(explorers)
except Exception as e:
print(f"Error updating co-explorers: {e}")
import traceback
traceback.print_exc()
return explorers
def handle_add_command(api_client, explorers):
"""Add a user to the co-explorers list."""
user_input = input("Enter user ID (or 'cancel' to abort): ").strip()
if user_input.lower() == 'cancel':
print("Add cancelled.")
return explorers
# Validate user ID format
if not re.match(r"^usr_[0-9a-fA-F-]{36}$", user_input):
print("Invalid user ID format. Expected format: usr_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
return explorers
# Check if already in list
if any(explorer['uid'] == user_input for explorer in explorers):
print("User already in co-explorers list.")
return explorers
# Fetch user info
user_info = get_user_by_id(api_client, user_input)
if not user_info:
print("User not found or error fetching user data.")
return explorers
explorers.append(user_info)
print(f"Added {user_info['name']} ({user_info['uid']}) to co-explorers list.")
display_co_explorers(explorers)
return explorers
def handle_remove_command(explorers):
"""Remove a user from the co-explorers list."""
if not explorers:
print("No co-explorers to remove.")
return explorers
display_co_explorers(explorers)
try:
remove_index = int(input("Enter number to remove (or 0 to cancel): ").strip()) - 1
if remove_index == -1:
print("Remove cancelled.")
return explorers
if not (0 <= remove_index < len(explorers)):
print("Invalid selection.")
return explorers
removed_user = explorers.pop(remove_index)
print(f"Removed {removed_user['name']} from co-explorers list.")
display_co_explorers(explorers)
except ValueError:
print("Invalid input.")
return explorers
def handle_clear_command():
"""Clear the co-explorers list."""
confirm = input("Are you sure you want to clear the co-explorers list? (y/n): ").lower()
if confirm == 'y':
print("Co-explorers list cleared.")
return []
else:
print("Clear cancelled.")
return None
def run(api_client, current_user, settingsdata):
script_dir = os.path.dirname(os.path.abspath(__file__))
# Extract settings specific to this script
world_json_output_path = settingsdata['files']['world_list_path']
worldhoppers_file_path = settingsdata['files']['co_explorers_path']
apiHappyTime = settingsdata['files']['api_delay_seconds']
manual_world_list_path = os.path.join(script_dir, settingsdata['files']['manual_world_list'])
instance_name = settingsdata['instanceid']
current_world = settingsdata['currentworld']
vrcx_db_path = settingsdata['files']['vrcx_db_path']
vrcx_allowed = settingsdata['allowvrcx']
vrcx_fav_group = settingsdata['vrcxfavgroup']
# --- Verify VRC+ Status ---
has_vrcplus = hasattr(current_user, 'tags') and current_user.tags and 'system_supporter' in current_user.tags
if not has_vrcplus and instance_name:
print("\n[!] Notice: VRC+ subscription not detected. Disabling custom instance names to prevent API errors.")
offset = 0
current_instance_type = InstanceType.FRIENDS
action_mode = "invite"
api_instance_instance = vrchatapi.InstancesApi(api_client)
api_invite_instance = vrchatapi.InviteApi(api_client)
api_world_instance = vrchatapi.WorldsApi(api_client)
# --- Mode Selection & World Loading ---
newsession = True
if current_world:
print(f"\nYou already have an ongoing jump session stored with number {current_world}.")
if input("Do you want to continue last session? (y/n): ").lower() == "y":
newsession = False
if newsession:
api_response = []
if input("\nLoad worlds from IDs file? (y/n): ").lower() == "y":
if os.path.exists(manual_world_list_path):
with open(manual_world_list_path, 'r') as f:
ids = [line.strip() for line in f if line.strip()]
print(f"Found {len(ids)} IDs. Fetching world data...")
for i, wid in enumerate(ids):
try:
w_data = api_world_instance.get_world(wid)
api_response.append(w_data)
print(f"[{i+1}/{len(ids)}] Fetched: {w_data.name}")
time.sleep(0.5)
except ApiException:
print(f"Error fetching ID {wid}")
use_api_search = False if api_response else True
else:
use_api_search = True
if use_api_search:
offset_input = input("Enter offset (0-999): ").strip()
offset = int(offset_input) if offset_input.isdigit() else 0
if input("Load old worlds (Ascending)? (y/n): ").lower() == "y":
api_response = api_world_instance.search_worlds(sort=vrchatapi.SortOption().CREATED, n=100, offset=offset, order=vrchatapi.OrderOption().ASCENDING)
else:
api_response = api_world_instance.search_worlds(sort=vrchatapi.SortOption().LABSPUBLICATIONDATE, n=100, offset=offset, order=vrchatapi.OrderOption().DESCENDING)
with open(world_json_output_path, "w", encoding="utf-8") as f:
json.dump(clean_for_json(api_response), f, indent=2, ensure_ascii=False)
# --- Load co-explorers from JSON ---
co_explorers = load_co_explorers(worldhoppers_file_path)
# --- JUMPING LOOP ---
with open(world_json_output_path, 'r', encoding='utf-8') as f:
json_list = json.load(f)
lastInviteTime = 0
activeJumpIndex = int(current_world) if current_world != "" else 0
last_index = activeJumpIndex
while True:
next_preview_idx = activeJumpIndex + 1
if 0 <= next_preview_idx < len(json_list):
print(f"\nNext world will be: {json_list[next_preview_idx]['name']} by {json_list[next_preview_idx]['author_name']}")
else:
print("\nNo preview available for next world.")
user_input = input("goto#? (empty to advance, 'help' for options): ").strip()
# --- Fast-track commands ---
if user_input.lower().startswith('type '):
t_val = user_input.lower().split(' ', 1)[1]
if t_val == 'friends': current_instance_type = InstanceType.FRIENDS
elif t_val in ['friends+', 'hidden']: current_instance_type = InstanceType.HIDDEN
elif t_val in ['invite', 'invite+', 'private']: current_instance_type = InstanceType.PRIVATE
elif t_val == 'public': current_instance_type = InstanceType.PUBLIC
else:
print("Unknown type. Use friends, friends+, invite, or public.")
continue
print(f"Instance type set to {current_instance_type}")
continue
elif user_input.lower().startswith('mode '):
m_val = user_input.lower().split(' ', 1)[1]
if m_val in ['invite', 'launch', 'both']:
action_mode = m_val
print(f"Mode set to {action_mode.upper()}")
else:
print("Unknown mode. Use invite, launch, or both.")
continue
# --- Interactive Menus ---
elif user_input.lower() == 'help':
print("\n--- Available Commands ---")
print(" [empty] : Advance to the next world in the list")
print(" [number]: Jump to a specific world index")
print(" type : Change the instance type (Friends, Friends+, etc.)")
print(" mode : Toggle between API Invites and Launching the client")
print(" update : Add users from your current instance to co-explorers")
print(" add : Manually add a co-explorer by User ID")
print(" remove : Remove a user from your co-explorers list")
print(" clear : Clear your co-explorers list completely")
print(" fav : Add the world you are currently in to VRCX favorites")
continue
elif user_input.lower() == 'type':
print(f"\nCurrent Type: {current_instance_type}")
print("1. Friends (Default)")
print("2. Friends+ (Hidden)")
print("3. Invite Only (Private)")
print("4. Public")
try:
t_choice = int(input("Select type (1-4, 0 to cancel): ").strip())
if t_choice == 1: current_instance_type = InstanceType.FRIENDS
elif t_choice == 2: current_instance_type = InstanceType.HIDDEN
elif t_choice == 3: current_instance_type = InstanceType.PRIVATE
elif t_choice == 4: current_instance_type = InstanceType.PUBLIC
if 1 <= t_choice <= 4:
print(f"Instance type set to {current_instance_type}")
except ValueError:
print("Cancelled or invalid input.")
continue
elif user_input.lower() == 'mode':
print(f"\nCurrent Mode: {action_mode.upper()}")
print("1. Invite (Send API invites to yourself and co-explorers)")
print("2. Launch (Open VRChat client directly via URI)")
print("3. Both (Launch client AND invite co-explorers)")
try:
m_choice = int(input("Select mode (1-3, 0 to cancel): ").strip())
if m_choice == 1: action_mode = "invite"
elif m_choice == 2: action_mode = "launch"
elif m_choice == 3: action_mode = "both"
if 1 <= m_choice <= 3:
print(f"Mode set to {action_mode.upper()}")
except ValueError:
print("Cancelled or invalid input.")
continue
# --- Handle co-explorer management commands ---
elif user_input.lower() == 'update':
print("Fetching users from your current instance...")
co_explorers = handle_update_command(api_client, co_explorers, current_user.id)
save_co_explorers(worldhoppers_file_path, co_explorers)
continue
elif user_input.lower() == 'clear':
result = handle_clear_command()
if result is not None:
co_explorers = result
save_co_explorers(worldhoppers_file_path, co_explorers)
continue
elif user_input.lower() == 'add':
co_explorers = handle_add_command(api_client, co_explorers)
save_co_explorers(worldhoppers_file_path, co_explorers)
continue
elif user_input.lower() == 'remove':
co_explorers = handle_remove_command(co_explorers)
save_co_explorers(worldhoppers_file_path, co_explorers)
continue
elif user_input.lower() == 'fav':
if vrcx_allowed:
current_world_id = get_current_presence_world_id(api_client)
print(f"\n[!] Adding {current_world_id} to {vrcx_fav_group}")
status = add_favorite_world(vrcx_db_path, current_world_id, vrcx_fav_group)
if status == "added":
print(f"\n[+] Success! Added {current_world_id} to >{vrcx_fav_group}<")
elif status == "duplicate":
print(f"\n[!] Skip: This world is already in the '{vrcx_fav_group}' group.")
else:
print(f"\n[X] Error: Could not add to database. (Check if VRCX has a heavy lock)")
else:
print(f"\nVRCX interactions is set to {vrcx_allowed}. \nIf you want to do this, change from false to true in settings.json and restart the script. \nOn linux, make sure it's pointing at your vrcx database. I take no responsibility over what happens to your vrcx db. \nTake a backup. \nAlso remember that worlds doesn't show up in VRCX immediately. But does after a restart of VRCX.")
continue
# --- Normal navigation ---
elif user_input == "":
if activeJumpIndex == 0 and last_index == 0 and newsession:
activeJumpIndex = 0
newsession = False
else:
activeJumpIndex = last_index + 1
else:
try:
activeJumpIndex = int(user_input)
except ValueError:
continue
if not (0 <= activeJumpIndex < len(json_list)) or activeJumpIndex == 500:
break
# Rate limiting
elapsed = int(time.time()) - lastInviteTime
if elapsed < apiHappyTime:
to_sleep = apiHappyTime - elapsed
print(f"Sleeping for {to_sleep} seconds to keep the servers happy")
time.sleep(to_sleep)
last_index = activeJumpIndex
try:
# Build the request arguments dynamically to avoid sending empty parameters
instance_args = {
"world_id": json_list[activeJumpIndex]['id'],
"owner_id": current_user.id,
"type": current_instance_type,
"region": InstanceRegion.EU
}
# Only append the display_name parameter if they have VRC+ and actually set a name
if has_vrcplus and instance_name:
instance_args["display_name"] = instance_name
created_instance = api_instance_instance.create_instance(
create_instance_request=CreateInstanceRequest(**instance_args)
)
created = clean_for_json(created_instance)
unity_version = get_unity_version(created)
world_asset_name = (created.get('world') or {}).get('name', 'Unknown World')
world_author_name = (created.get('world') or {}).get('author_name')
world_location = created.get('location') or created.get('id')
print("\n-------------------------------------------------------------------------------------------------------")
print(f"Sent request for #{last_index}: {world_asset_name} by {world_author_name}")
print(f"World id: {world_location}")
print(f"Unity version: {unity_version}")
print(f"Instance Type: {current_instance_type}")
print("-------------------------------------------------------------------------------------------------------")
# Personal execution based on Mode
if action_mode == "invite":
parts = world_location.split(':')
api_invite_instance.invite_myself_to(parts[0], parts[1] if len(parts) > 1 else "")
print("Invited myself via API.")
elif action_mode in ["launch", "both"]:
launch_uri = f"vrchat://launch/?ref=vrchat.com&id={world_location}&attach=1"
print("Launching VRChat client directly...")
webbrowser.open(launch_uri)
# Invite all co-explorers based on Mode
if action_mode in ["invite", "both"]:
if co_explorers:
print(f"Inviting {len(co_explorers)} co-explorers:")
for explorer in co_explorers:
try:
api_invite_instance.invite_user(explorer['uid'], vrchatapi.InviteRequest(instance_id=world_location))
print(f" → Invited {explorer['name']}")
except Exception as e:
print(f" ✗ Failed to invite {explorer['name']}: {e}")
else:
if action_mode == "invite":
print("No co-explorers in list to invite.")
lastInviteTime = int(time.time())
print("\n")
except Exception as e:
print(f"Critical error during jump: {e}")
print("Session finished.")