redfetch 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- redfetch/__about__.py +24 -0
- redfetch/__init__.py +0 -0
- redfetch/api.py +134 -0
- redfetch/auth.py +573 -0
- redfetch/config.py +483 -0
- redfetch/config_firstrun.py +455 -0
- redfetch/desktop_shortcut.py +81 -0
- redfetch/detecteq.py +116 -0
- redfetch/download.py +312 -0
- redfetch/listener.py +216 -0
- redfetch/main.py +744 -0
- redfetch/meta.py +561 -0
- redfetch/navmesh.py +371 -0
- redfetch/net.py +109 -0
- redfetch/processes.py +118 -0
- redfetch/push.py +246 -0
- redfetch/redfetch.ico +0 -0
- redfetch/runtime_errors.py +96 -0
- redfetch/settings.toml +303 -0
- redfetch/special.py +81 -0
- redfetch/store.py +505 -0
- redfetch/sync.py +261 -0
- redfetch/sync_discovery.py +352 -0
- redfetch/sync_executor.py +164 -0
- redfetch/sync_planner.py +196 -0
- redfetch/sync_remote.py +182 -0
- redfetch/sync_types.py +348 -0
- redfetch/terminal_ui.py +2318 -0
- redfetch/terminal_ui.tcss +569 -0
- redfetch/unloadmq.py +148 -0
- redfetch/update_status.py +62 -0
- redfetch/utils.py +256 -0
- redfetch-1.3.0.dist-info/METADATA +257 -0
- redfetch-1.3.0.dist-info/RECORD +37 -0
- redfetch-1.3.0.dist-info/WHEEL +4 -0
- redfetch-1.3.0.dist-info/entry_points.txt +2 -0
- redfetch-1.3.0.dist-info/licenses/LICENSE +674 -0
redfetch/push.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Publish resource updates to RedGuides."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import asyncio
|
|
5
|
+
import httpx
|
|
6
|
+
import keepachangelog
|
|
7
|
+
import typer
|
|
8
|
+
from md2bbcode.main import process_readme
|
|
9
|
+
|
|
10
|
+
from redfetch import api
|
|
11
|
+
from redfetch.net import BASE_URL
|
|
12
|
+
from redfetch import auth
|
|
13
|
+
|
|
14
|
+
XF_API_URL = f'{BASE_URL}/api'
|
|
15
|
+
URI_MESSAGE = f'{XF_API_URL}/resource-updates'
|
|
16
|
+
URI_ATTACHMENT = f'{XF_API_URL}/attachments/new-key'
|
|
17
|
+
URI_RESOURCE_VERSIONS = f'{XF_API_URL}/resource-versions'
|
|
18
|
+
MAX_MESSAGE_CHARS = 10_000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_api_headers_blocking() -> dict:
|
|
22
|
+
"""Synchronous helper to obtain API headers from the async auth client."""
|
|
23
|
+
return asyncio.run(auth.get_api_headers())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def update_resource_description(resource_id, new_description):
|
|
27
|
+
"""Update resource description."""
|
|
28
|
+
url = f"{XF_API_URL}/resources/{resource_id}"
|
|
29
|
+
payload = {'description': new_description}
|
|
30
|
+
headers = _get_api_headers_blocking()
|
|
31
|
+
response = httpx.post(url, headers=headers, data=payload, timeout=30.0)
|
|
32
|
+
response.raise_for_status()
|
|
33
|
+
print("Successfully updated the resource description.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def add_xf_message(resource_id, msg_title, message):
|
|
37
|
+
"""Post a new resource update message."""
|
|
38
|
+
headers = _get_api_headers_blocking()
|
|
39
|
+
form_message = {
|
|
40
|
+
'resource_id': resource_id,
|
|
41
|
+
'title': msg_title,
|
|
42
|
+
'message': message
|
|
43
|
+
}
|
|
44
|
+
response = httpx.post(URI_MESSAGE, headers=headers, data=form_message, timeout=30.0)
|
|
45
|
+
response.raise_for_status()
|
|
46
|
+
print(f"Response: {response.status_code}, {response.text}")
|
|
47
|
+
return response.json()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def add_xf_attachment(resource_id, upfilename, version=None):
|
|
51
|
+
"""Upload a file and attach it as a new resource version."""
|
|
52
|
+
headers = _get_api_headers_blocking()
|
|
53
|
+
|
|
54
|
+
# Prepare the data for getting an attachment key and uploading the file
|
|
55
|
+
data = {
|
|
56
|
+
"type": "resource_version",
|
|
57
|
+
"context[resource_id]": resource_id
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
# Get an attachment key and also upload the file
|
|
62
|
+
with open(upfilename, "rb") as file:
|
|
63
|
+
files = {"attachment": (os.path.basename(upfilename), file, "application/octet-stream")}
|
|
64
|
+
response = httpx.post(URI_ATTACHMENT, headers=headers, data=data, files=files, timeout=60.0)
|
|
65
|
+
response.raise_for_status()
|
|
66
|
+
content = response.json()
|
|
67
|
+
attach_key = content.get("key")
|
|
68
|
+
if attach_key:
|
|
69
|
+
# Now associate the attachment(s) with the resource version
|
|
70
|
+
data_update = {
|
|
71
|
+
"type": "resource_version",
|
|
72
|
+
"resource_id": resource_id,
|
|
73
|
+
"version_attachment_key": attach_key,
|
|
74
|
+
}
|
|
75
|
+
if version:
|
|
76
|
+
data_update["version_string"] = version
|
|
77
|
+
response_update = httpx.post(URI_RESOURCE_VERSIONS, headers=headers, data=data_update, timeout=60.0)
|
|
78
|
+
response_update.raise_for_status()
|
|
79
|
+
print(f"Successfully added attachment for resource {resource_id}")
|
|
80
|
+
else:
|
|
81
|
+
print("[ERROR] No attachment key received from the server.")
|
|
82
|
+
raise RuntimeError("No attachment key received from the server.")
|
|
83
|
+
except httpx.HTTPStatusError as e:
|
|
84
|
+
print(f"HTTP Error: {e.response.status_code} - {e.response.text}")
|
|
85
|
+
raise
|
|
86
|
+
except FileNotFoundError:
|
|
87
|
+
print(f"Error: File '{upfilename}' not found.")
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def update_resource(resource_id, version_info, upfilename=None):
|
|
92
|
+
"""Create a version update (message + optional attachment)."""
|
|
93
|
+
message = version_info.get('message', '')
|
|
94
|
+
if message.strip():
|
|
95
|
+
add_xf_message(resource_id, version_info['version_string'], message)
|
|
96
|
+
else:
|
|
97
|
+
print("Warning: No message content provided, skipping update post.")
|
|
98
|
+
|
|
99
|
+
if upfilename:
|
|
100
|
+
add_xf_attachment(resource_id, upfilename, version_info['version_string'])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def convert_markdown_to_bbcode(markdown_text, domain=None):
|
|
104
|
+
"""Convert markdown text to BBCode."""
|
|
105
|
+
bbcode_output = process_readme(markdown_text, domain=domain)
|
|
106
|
+
return bbcode_output
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _truncate_text(text: str, max_chars: int, suffix: str = "\n\n(truncated)") -> str:
|
|
110
|
+
"""Truncate text to max_chars (including suffix), if needed."""
|
|
111
|
+
if not isinstance(text, str):
|
|
112
|
+
text = str(text)
|
|
113
|
+
if max_chars <= 0 or len(text) <= max_chars:
|
|
114
|
+
return text
|
|
115
|
+
|
|
116
|
+
suffix = suffix or ""
|
|
117
|
+
if len(suffix) >= max_chars:
|
|
118
|
+
return suffix[:max_chars]
|
|
119
|
+
|
|
120
|
+
allowed = max_chars - len(suffix)
|
|
121
|
+
return text[:allowed].rstrip() + suffix
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _try_parse_keepachangelog_dict(changelog_path: str):
|
|
125
|
+
"""
|
|
126
|
+
Attempt to parse a keep-a-changelog file.
|
|
127
|
+
|
|
128
|
+
Returns a non-empty dict if it looks like a changelog; otherwise returns None.
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
changes = keepachangelog.to_dict(changelog_path)
|
|
132
|
+
except Exception:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
if not isinstance(changes, dict) or not changes:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
return changes
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def parse_changelog(changelog_path, version, domain=None, changes=None):
|
|
142
|
+
"""
|
|
143
|
+
Parses the changelog file and returns the changelog entry for the given version as BBCode.
|
|
144
|
+
"""
|
|
145
|
+
# Use keepachangelog to parse the changelog file
|
|
146
|
+
if changes is None:
|
|
147
|
+
changes = keepachangelog.to_dict(changelog_path)
|
|
148
|
+
# Remove 'v' prefix if present
|
|
149
|
+
version_key = version.lstrip('v')
|
|
150
|
+
if version_key in changes:
|
|
151
|
+
# Flatten the change notes into a markdown string
|
|
152
|
+
version_data = changes[version_key]
|
|
153
|
+
markdown_lines = []
|
|
154
|
+
for section, notes in version_data.items():
|
|
155
|
+
if section != 'metadata':
|
|
156
|
+
markdown_lines.append(f"### {section.capitalize()}")
|
|
157
|
+
for note in notes:
|
|
158
|
+
markdown_lines.append(f"- {note}")
|
|
159
|
+
markdown_lines.append("") # Add a newline
|
|
160
|
+
markdown_message = "\n".join(markdown_lines)
|
|
161
|
+
# Convert markdown to BBCode
|
|
162
|
+
bbcode_message = convert_markdown_to_bbcode(markdown_message, domain=domain)
|
|
163
|
+
return bbcode_message
|
|
164
|
+
else:
|
|
165
|
+
raise ValueError(f"Version {version} not found in {changelog_path}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def generate_version_message(args):
|
|
169
|
+
"""
|
|
170
|
+
Build a version message from `--message` (file path or literal string).
|
|
171
|
+
|
|
172
|
+
If `--message` is a keep-a-changelog file, extract the entry for `--version`; otherwise post the
|
|
173
|
+
file contents (markdown is converted to BBCode). Large messages are truncated.
|
|
174
|
+
"""
|
|
175
|
+
max_chars = MAX_MESSAGE_CHARS
|
|
176
|
+
|
|
177
|
+
# message can be a literal string, or a path-like string to a real file.
|
|
178
|
+
if args.message and os.path.isfile(args.message):
|
|
179
|
+
message_path = args.message
|
|
180
|
+
ext = os.path.splitext(message_path)[1].lower()
|
|
181
|
+
|
|
182
|
+
if ext == ".md":
|
|
183
|
+
# If it looks like keep-a-changelog, post the entry for --version; otherwise post the file.
|
|
184
|
+
changes = _try_parse_keepachangelog_dict(message_path)
|
|
185
|
+
if changes is not None:
|
|
186
|
+
try:
|
|
187
|
+
message = parse_changelog(message_path, args.version, domain=args.domain, changes=changes)
|
|
188
|
+
except ValueError as e:
|
|
189
|
+
with open(message_path, "r", encoding="utf-8", errors="replace") as f:
|
|
190
|
+
markdown_text = f.read()
|
|
191
|
+
message = convert_markdown_to_bbcode(markdown_text, domain=args.domain)
|
|
192
|
+
print(f"Warning: {e}. Posting full file contents instead.")
|
|
193
|
+
else:
|
|
194
|
+
with open(message_path, "r", encoding="utf-8", errors="replace") as f:
|
|
195
|
+
markdown_text = f.read()
|
|
196
|
+
message = convert_markdown_to_bbcode(markdown_text, domain=args.domain)
|
|
197
|
+
else:
|
|
198
|
+
# Plain text (or other) file: post as-is.
|
|
199
|
+
with open(message_path, "r", encoding="utf-8", errors="replace") as f:
|
|
200
|
+
message = f.read()
|
|
201
|
+
|
|
202
|
+
return _truncate_text(message, max_chars)
|
|
203
|
+
|
|
204
|
+
# If --message is a regular string, use it directly (still guard against extreme length).
|
|
205
|
+
return _truncate_text(args.message, max_chars)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def update_description(resource_id, description_path, domain=None):
|
|
209
|
+
"""Read description, convert markdown to BBCode if needed, then update."""
|
|
210
|
+
with open(description_path, 'r') as f:
|
|
211
|
+
new_description = f.read()
|
|
212
|
+
if description_path.lower().endswith('.md'):
|
|
213
|
+
new_description = convert_markdown_to_bbcode(new_description, domain=domain)
|
|
214
|
+
update_resource_description(resource_id, new_description)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def handle_cli(args):
|
|
218
|
+
"""Handle the push subcommand using existing push helpers."""
|
|
219
|
+
|
|
220
|
+
if not any([args.description, args.version, args.message, args.file]):
|
|
221
|
+
print("At least one option (--description, --version, --message, or --file) must be specified.")
|
|
222
|
+
raise typer.Exit(code=1)
|
|
223
|
+
|
|
224
|
+
if args.message and not args.version:
|
|
225
|
+
print("The --message option requires --version to be specified.")
|
|
226
|
+
raise typer.Exit(code=1)
|
|
227
|
+
|
|
228
|
+
# Ensure the user is authorized
|
|
229
|
+
auth.initialize_keyring()
|
|
230
|
+
auth.authorize()
|
|
231
|
+
|
|
232
|
+
# Blocking call is fine here; push is a short-lived CLI operation.
|
|
233
|
+
headers = _get_api_headers_blocking()
|
|
234
|
+
resource = asyncio.run(api.get_resource_details(args.resource_id, headers))
|
|
235
|
+
resource_id = resource['resource_id']
|
|
236
|
+
|
|
237
|
+
if args.description:
|
|
238
|
+
update_description(resource_id, args.description, domain=args.domain)
|
|
239
|
+
|
|
240
|
+
if args.version and args.message:
|
|
241
|
+
message = generate_version_message(args)
|
|
242
|
+
version_info = {'version_string': args.version, 'message': message}
|
|
243
|
+
update_resource(resource_id, version_info, args.file)
|
|
244
|
+
elif args.file:
|
|
245
|
+
# Allow publishing a version with a file but no changelog message.
|
|
246
|
+
add_xf_attachment(resource_id, args.file, args.version)
|
redfetch/redfetch.ico
ADDED
|
Binary file
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Shared fatal error helpers for startup/CLI boundaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import traceback
|
|
8
|
+
from typing import NoReturn
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_windows_pyapp() -> bool:
|
|
12
|
+
"""are we using the redfetch.exe executable on windows?"""
|
|
13
|
+
return sys.platform == "win32" and bool(os.getenv("PYAPP"))
|
|
14
|
+
|
|
15
|
+
def normalize_error_message(
|
|
16
|
+
message: str | BaseException | None,
|
|
17
|
+
*,
|
|
18
|
+
fallback: str = "An unexpected error occurred.",
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Make a safe non-empty string."""
|
|
21
|
+
if isinstance(message, BaseException):
|
|
22
|
+
normalized = str(message).strip()
|
|
23
|
+
if normalized:
|
|
24
|
+
return normalized
|
|
25
|
+
return f"{message.__class__.__name__} ({fallback})"
|
|
26
|
+
|
|
27
|
+
if message is None:
|
|
28
|
+
return fallback
|
|
29
|
+
|
|
30
|
+
normalized = str(message).strip()
|
|
31
|
+
if normalized:
|
|
32
|
+
return normalized
|
|
33
|
+
return fallback
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _show_windows_error_dialog(message: str) -> None:
|
|
37
|
+
"""Shows a Windows MessageBox."""
|
|
38
|
+
try:
|
|
39
|
+
import ctypes
|
|
40
|
+
|
|
41
|
+
ctypes.windll.user32.MessageBoxW(0, message, "redfetch", 0x10)
|
|
42
|
+
except Exception:
|
|
43
|
+
# Best effort only: CLI stderr output remains the source of truth.
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _format_error_details(error: BaseException) -> str:
|
|
48
|
+
"""Summarize the exception with traceback and details."""
|
|
49
|
+
summary = f"{error.__class__.__name__}: {normalize_error_message(error)}"
|
|
50
|
+
traceback_text = "".join(
|
|
51
|
+
traceback.format_exception(type(error), error, error.__traceback__)
|
|
52
|
+
).strip()
|
|
53
|
+
if not traceback_text:
|
|
54
|
+
return summary
|
|
55
|
+
return f"{summary}\n\n{traceback_text}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def display_fatal_error(
|
|
59
|
+
message: str | BaseException | None,
|
|
60
|
+
) -> str:
|
|
61
|
+
"""Print a fatal error and optionally show a Windows MessageBox."""
|
|
62
|
+
normalized_message = normalize_error_message(message)
|
|
63
|
+
error_details = (
|
|
64
|
+
_format_error_details(message)
|
|
65
|
+
if isinstance(message, BaseException)
|
|
66
|
+
else None
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if is_windows_pyapp():
|
|
70
|
+
if error_details:
|
|
71
|
+
dialog_message = (
|
|
72
|
+
"Tip: Press Ctrl+C to copy this error report.\n\n"
|
|
73
|
+
f"{error_details}"
|
|
74
|
+
)
|
|
75
|
+
print(error_details, file=sys.stderr)
|
|
76
|
+
else:
|
|
77
|
+
dialog_message = normalized_message
|
|
78
|
+
print(normalized_message, file=sys.stderr)
|
|
79
|
+
_show_windows_error_dialog(dialog_message)
|
|
80
|
+
else:
|
|
81
|
+
# CLI behavior: print the raw normalized message without extra wrappers.
|
|
82
|
+
if error_details:
|
|
83
|
+
print(error_details, file=sys.stderr)
|
|
84
|
+
else:
|
|
85
|
+
print(normalized_message, file=sys.stderr)
|
|
86
|
+
|
|
87
|
+
return normalized_message
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def exit_with_fatal_error(
|
|
91
|
+
message: str | BaseException | None,
|
|
92
|
+
exit_code: int = 1,
|
|
93
|
+
) -> NoReturn:
|
|
94
|
+
"""Display a fatal error and terminate with a non-zero exit code."""
|
|
95
|
+
display_fatal_error(message)
|
|
96
|
+
raise SystemExit(exit_code)
|
redfetch/settings.toml
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Hail adventurer! This file is not meant to be edited directly.
|
|
2
|
+
# Instead, you should make a settings.local.toml file, and edit that. It will override anything here.
|
|
3
|
+
|
|
4
|
+
# ========================================
|
|
5
|
+
# DEFAULT SETTINGS USED IN ALL ENVIRONMENTS
|
|
6
|
+
# ========================================
|
|
7
|
+
# These settings are the base configuration for all environments. You can override them with [LIVE], [TEST], or [EMU] sections.
|
|
8
|
+
[DEFAULT]
|
|
9
|
+
DOWNLOAD_FOLDER = "@format {env[REDFETCH_DATA_DIR]}/Downloads"
|
|
10
|
+
EQPATH = ""
|
|
11
|
+
|
|
12
|
+
# ========================================
|
|
13
|
+
# XenForo OAuth2
|
|
14
|
+
# ========================================
|
|
15
|
+
OAUTH_CLIENT_ID = "4558786089326342"
|
|
16
|
+
OAUTH_CLIENT_SECRET = ""
|
|
17
|
+
OAUTH_SCOPE = "user:read resource:read resource:write attachment:write"
|
|
18
|
+
OAUTH_REDIRECT_URI = "http://127.0.0.1:62897/"
|
|
19
|
+
|
|
20
|
+
# ========================================
|
|
21
|
+
# LIVE ENVIRONMENT SETTINGS
|
|
22
|
+
# ========================================
|
|
23
|
+
[LIVE]
|
|
24
|
+
THEME = "textual-dark"
|
|
25
|
+
|
|
26
|
+
[LIVE.PROTECTED_FILES_BY_RESOURCE]
|
|
27
|
+
1974 = ["CharSelect.cfg", "Zoned.cfg", "MQ2Map.ini", "MQ2MoveUtils.ini"]
|
|
28
|
+
|
|
29
|
+
[LIVE.SPECIAL_RESOURCES.1974]
|
|
30
|
+
default_path = "VanillaMQ_LIVE"
|
|
31
|
+
custom_path = ""
|
|
32
|
+
opt_in = true
|
|
33
|
+
|
|
34
|
+
[LIVE.SPECIAL_RESOURCES.151]
|
|
35
|
+
default_path = "MySEQ_LIVE"
|
|
36
|
+
custom_path = ""
|
|
37
|
+
opt_in = false
|
|
38
|
+
|
|
39
|
+
[LIVE.SPECIAL_RESOURCES.151.dependencies.153]
|
|
40
|
+
subfolder = "maps"
|
|
41
|
+
flatten = true
|
|
42
|
+
opt_in = true
|
|
43
|
+
|
|
44
|
+
[LIVE.SPECIAL_RESOURCES.151.dependencies.1865]
|
|
45
|
+
subfolder = ""
|
|
46
|
+
flatten = false
|
|
47
|
+
opt_in = true
|
|
48
|
+
|
|
49
|
+
[LIVE.SPECIAL_RESOURCES.153]
|
|
50
|
+
default_path = "@format {this.eqpath}/maps"
|
|
51
|
+
custom_path = ""
|
|
52
|
+
opt_in = false
|
|
53
|
+
|
|
54
|
+
[LIVE.SPECIAL_RESOURCES.303]
|
|
55
|
+
default_path = "@format {this.eqpath}/maps"
|
|
56
|
+
custom_path = ""
|
|
57
|
+
opt_in = false
|
|
58
|
+
|
|
59
|
+
# LIVE staff picks
|
|
60
|
+
# kissassist
|
|
61
|
+
[LIVE.SPECIAL_RESOURCES.4]
|
|
62
|
+
opt_in = true
|
|
63
|
+
staff_pick = true
|
|
64
|
+
|
|
65
|
+
# lua event manager
|
|
66
|
+
[LIVE.SPECIAL_RESOURCES.2539]
|
|
67
|
+
opt_in = true
|
|
68
|
+
staff_pick = true
|
|
69
|
+
|
|
70
|
+
# guildclicky
|
|
71
|
+
[LIVE.SPECIAL_RESOURCES.2318]
|
|
72
|
+
opt_in = true
|
|
73
|
+
staff_pick = true
|
|
74
|
+
|
|
75
|
+
# buttonmaster
|
|
76
|
+
[LIVE.SPECIAL_RESOURCES.2174]
|
|
77
|
+
opt_in = true
|
|
78
|
+
staff_pick = true
|
|
79
|
+
|
|
80
|
+
# alertmaster
|
|
81
|
+
[LIVE.SPECIAL_RESOURCES.2062]
|
|
82
|
+
opt_in = true
|
|
83
|
+
staff_pick = true
|
|
84
|
+
|
|
85
|
+
# rgmercs
|
|
86
|
+
[LIVE.SPECIAL_RESOURCES.3040]
|
|
87
|
+
opt_in = true
|
|
88
|
+
staff_pick = true
|
|
89
|
+
|
|
90
|
+
# lootly
|
|
91
|
+
[LIVE.SPECIAL_RESOURCES.2196]
|
|
92
|
+
opt_in = true
|
|
93
|
+
staff_pick = true
|
|
94
|
+
|
|
95
|
+
# boxhud
|
|
96
|
+
[LIVE.SPECIAL_RESOURCES.2088]
|
|
97
|
+
opt_in = true
|
|
98
|
+
staff_pick = true
|
|
99
|
+
|
|
100
|
+
# scriber
|
|
101
|
+
[LIVE.SPECIAL_RESOURCES.2391]
|
|
102
|
+
opt_in = true
|
|
103
|
+
staff_pick = true
|
|
104
|
+
|
|
105
|
+
# bazaar / auction helper
|
|
106
|
+
[LIVE.SPECIAL_RESOURCES.3001]
|
|
107
|
+
opt_in = true
|
|
108
|
+
staff_pick = true
|
|
109
|
+
|
|
110
|
+
# skill skillup: spells and others
|
|
111
|
+
[LIVE.SPECIAL_RESOURCES.2937]
|
|
112
|
+
opt_in = true
|
|
113
|
+
staff_pick = true
|
|
114
|
+
|
|
115
|
+
# Ninjadvloot.inc
|
|
116
|
+
[LIVE.SPECIAL_RESOURCES.973]
|
|
117
|
+
opt_in = true
|
|
118
|
+
staff_pick = true
|
|
119
|
+
|
|
120
|
+
# ========================================
|
|
121
|
+
# TEST ENVIRONMENT SETTINGS
|
|
122
|
+
# ========================================
|
|
123
|
+
[TEST]
|
|
124
|
+
THEME = "gruvbox"
|
|
125
|
+
|
|
126
|
+
[TEST.PROTECTED_FILES_BY_RESOURCE]
|
|
127
|
+
2218 = ["CharSelect.cfg", "Zoned.cfg", "MQ2Map.ini", "MQ2MoveUtils.ini"]
|
|
128
|
+
|
|
129
|
+
[TEST.SPECIAL_RESOURCES.2218]
|
|
130
|
+
default_path = "VanillaMQ_TEST"
|
|
131
|
+
custom_path = ""
|
|
132
|
+
opt_in = true
|
|
133
|
+
|
|
134
|
+
[TEST.SPECIAL_RESOURCES.164]
|
|
135
|
+
default_path = "MySEQ_TEST"
|
|
136
|
+
custom_path = ""
|
|
137
|
+
opt_in = false
|
|
138
|
+
|
|
139
|
+
[TEST.SPECIAL_RESOURCES.164.dependencies.153]
|
|
140
|
+
subfolder = "maps"
|
|
141
|
+
flatten = true
|
|
142
|
+
opt_in = true
|
|
143
|
+
|
|
144
|
+
[TEST.SPECIAL_RESOURCES.164.dependencies.1865]
|
|
145
|
+
subfolder = ""
|
|
146
|
+
flatten = false
|
|
147
|
+
opt_in = true
|
|
148
|
+
|
|
149
|
+
[TEST.SPECIAL_RESOURCES.153]
|
|
150
|
+
default_path = "@format {this.eqpath}/maps"
|
|
151
|
+
custom_path = ""
|
|
152
|
+
opt_in = false
|
|
153
|
+
|
|
154
|
+
[TEST.SPECIAL_RESOURCES.303]
|
|
155
|
+
default_path = "@format {this.eqpath}/maps"
|
|
156
|
+
custom_path = ""
|
|
157
|
+
opt_in = false
|
|
158
|
+
|
|
159
|
+
# TEST staff picks
|
|
160
|
+
# kissassist
|
|
161
|
+
[TEST.SPECIAL_RESOURCES.4]
|
|
162
|
+
opt_in = true
|
|
163
|
+
staff_pick = true
|
|
164
|
+
|
|
165
|
+
# lua event manager
|
|
166
|
+
[TEST.SPECIAL_RESOURCES.2539]
|
|
167
|
+
opt_in = true
|
|
168
|
+
staff_pick = true
|
|
169
|
+
|
|
170
|
+
# guildclicky
|
|
171
|
+
[TEST.SPECIAL_RESOURCES.2318]
|
|
172
|
+
opt_in = true
|
|
173
|
+
staff_pick = true
|
|
174
|
+
|
|
175
|
+
# buttonmaster
|
|
176
|
+
[TEST.SPECIAL_RESOURCES.2174]
|
|
177
|
+
opt_in = true
|
|
178
|
+
staff_pick = true
|
|
179
|
+
|
|
180
|
+
# alertmaster
|
|
181
|
+
[TEST.SPECIAL_RESOURCES.2062]
|
|
182
|
+
opt_in = true
|
|
183
|
+
staff_pick = true
|
|
184
|
+
|
|
185
|
+
# rgmercs
|
|
186
|
+
[TEST.SPECIAL_RESOURCES.3040]
|
|
187
|
+
opt_in = true
|
|
188
|
+
staff_pick = true
|
|
189
|
+
|
|
190
|
+
# lootly
|
|
191
|
+
[TEST.SPECIAL_RESOURCES.2196]
|
|
192
|
+
opt_in = true
|
|
193
|
+
staff_pick = true
|
|
194
|
+
|
|
195
|
+
# boxhud
|
|
196
|
+
[TEST.SPECIAL_RESOURCES.2088]
|
|
197
|
+
opt_in = true
|
|
198
|
+
staff_pick = true
|
|
199
|
+
|
|
200
|
+
# scriber
|
|
201
|
+
[TEST.SPECIAL_RESOURCES.2391]
|
|
202
|
+
opt_in = true
|
|
203
|
+
staff_pick = true
|
|
204
|
+
|
|
205
|
+
# bazaar / auction helper
|
|
206
|
+
[TEST.SPECIAL_RESOURCES.3001]
|
|
207
|
+
opt_in = true
|
|
208
|
+
staff_pick = true
|
|
209
|
+
|
|
210
|
+
# skill skillup: spells and others
|
|
211
|
+
[TEST.SPECIAL_RESOURCES.2937]
|
|
212
|
+
opt_in = true
|
|
213
|
+
staff_pick = true
|
|
214
|
+
|
|
215
|
+
# Ninjadvloot.inc
|
|
216
|
+
[TEST.SPECIAL_RESOURCES.973]
|
|
217
|
+
opt_in = true
|
|
218
|
+
staff_pick = true
|
|
219
|
+
|
|
220
|
+
# ========================================
|
|
221
|
+
# EMULATOR ENVIRONMENT SETTINGS
|
|
222
|
+
# ========================================
|
|
223
|
+
[EMU]
|
|
224
|
+
THEME = "dracula"
|
|
225
|
+
|
|
226
|
+
[EMU.PROTECTED_FILES_BY_RESOURCE]
|
|
227
|
+
60 = ["CharSelect.cfg", "Zoned.cfg", "MQ2Map.ini", "MQ2MoveUtils.ini"]
|
|
228
|
+
|
|
229
|
+
[EMU.SPECIAL_RESOURCES.60]
|
|
230
|
+
default_path = "VanillaMQ_EMU"
|
|
231
|
+
custom_path = ""
|
|
232
|
+
opt_in = true
|
|
233
|
+
|
|
234
|
+
[EMU.SPECIAL_RESOURCES.153]
|
|
235
|
+
default_path = "@format {this.eqpath}/maps"
|
|
236
|
+
custom_path = ""
|
|
237
|
+
opt_in = false
|
|
238
|
+
|
|
239
|
+
[EMU.SPECIAL_RESOURCES.303]
|
|
240
|
+
default_path = "@format {this.eqpath}/maps"
|
|
241
|
+
custom_path = ""
|
|
242
|
+
opt_in = false
|
|
243
|
+
|
|
244
|
+
# EMU staff picks
|
|
245
|
+
# kissassist
|
|
246
|
+
[EMU.SPECIAL_RESOURCES.4]
|
|
247
|
+
opt_in = true
|
|
248
|
+
staff_pick = true
|
|
249
|
+
|
|
250
|
+
# lua event manager
|
|
251
|
+
[EMU.SPECIAL_RESOURCES.2539]
|
|
252
|
+
opt_in = true
|
|
253
|
+
staff_pick = true
|
|
254
|
+
|
|
255
|
+
# guildclicky
|
|
256
|
+
[EMU.SPECIAL_RESOURCES.2318]
|
|
257
|
+
opt_in = true
|
|
258
|
+
staff_pick = true
|
|
259
|
+
|
|
260
|
+
# buttonmaster
|
|
261
|
+
[EMU.SPECIAL_RESOURCES.2174]
|
|
262
|
+
opt_in = true
|
|
263
|
+
staff_pick = true
|
|
264
|
+
|
|
265
|
+
# alertmaster
|
|
266
|
+
[EMU.SPECIAL_RESOURCES.2062]
|
|
267
|
+
opt_in = true
|
|
268
|
+
staff_pick = true
|
|
269
|
+
|
|
270
|
+
# rgmercs
|
|
271
|
+
[EMU.SPECIAL_RESOURCES.3040]
|
|
272
|
+
opt_in = true
|
|
273
|
+
staff_pick = true
|
|
274
|
+
|
|
275
|
+
# boxhud
|
|
276
|
+
[EMU.SPECIAL_RESOURCES.2088]
|
|
277
|
+
opt_in = true
|
|
278
|
+
staff_pick = true
|
|
279
|
+
|
|
280
|
+
# scriber
|
|
281
|
+
[EMU.SPECIAL_RESOURCES.2391]
|
|
282
|
+
opt_in = true
|
|
283
|
+
staff_pick = true
|
|
284
|
+
|
|
285
|
+
# bazaar / auction helper
|
|
286
|
+
[EMU.SPECIAL_RESOURCES.3001]
|
|
287
|
+
opt_in = true
|
|
288
|
+
staff_pick = true
|
|
289
|
+
|
|
290
|
+
# skill skillup: spells and others
|
|
291
|
+
[EMU.SPECIAL_RESOURCES.2937]
|
|
292
|
+
opt_in = true
|
|
293
|
+
staff_pick = true
|
|
294
|
+
|
|
295
|
+
# lootnscoot (EMU only)
|
|
296
|
+
[EMU.SPECIAL_RESOURCES.2675]
|
|
297
|
+
opt_in = true
|
|
298
|
+
staff_pick = true
|
|
299
|
+
|
|
300
|
+
# Ninjadvloot.inc
|
|
301
|
+
[EMU.SPECIAL_RESOURCES.973]
|
|
302
|
+
opt_in = true
|
|
303
|
+
staff_pick = true
|