2023-05-05 16:38:01 +00:00
|
|
|
|
import re
|
|
|
|
|
from enum import IntEnum, auto
|
|
|
|
|
from typing import Optional, Union
|
|
|
|
|
|
|
|
|
|
from robocop_ng.helpers.disabled_ids import is_build_id_valid
|
2023-11-20 18:48:35 +00:00
|
|
|
|
from robocop_ng.helpers.size import Size
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
|
2023-11-20 18:48:35 +00:00
|
|
|
|
class CommonError(IntEnum):
|
2023-05-05 16:38:01 +00:00
|
|
|
|
SHADER_CACHE_COLLISION = auto()
|
|
|
|
|
DUMP_HASH = auto()
|
|
|
|
|
SHADER_CACHE_CORRUPTION = auto()
|
|
|
|
|
UPDATE_KEYS = auto()
|
|
|
|
|
FILE_PERMISSIONS = auto()
|
|
|
|
|
FILE_NOT_FOUND = auto()
|
|
|
|
|
MISSING_SERVICES = auto()
|
|
|
|
|
VULKAN_OUT_OF_MEMORY = auto()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RyujinxVersion(IntEnum):
|
|
|
|
|
MASTER = auto()
|
|
|
|
|
OLD_MASTER = auto()
|
|
|
|
|
LDN = auto()
|
|
|
|
|
MAC = auto()
|
|
|
|
|
PR = auto()
|
|
|
|
|
CUSTOM = auto()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LogAnalyser:
|
|
|
|
|
_log_text: str
|
|
|
|
|
_log_errors: list[list[str]]
|
|
|
|
|
_hardware_info: dict[str, Optional[str]]
|
|
|
|
|
_emu_info: dict[str, Optional[str]]
|
|
|
|
|
_game_info: dict[str, Optional[str]]
|
|
|
|
|
_settings: dict[str, Optional[str]]
|
2024-01-06 14:33:01 +00:00
|
|
|
|
_notes: Union[set[str], list[str]]
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
2023-05-28 08:36:54 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
def is_homebrew(log_file: str) -> bool:
|
2023-10-09 20:52:29 +00:00
|
|
|
|
return (
|
|
|
|
|
re.search("Load.*Application: Loading as [Hh]omebrew", log_file) is not None
|
|
|
|
|
)
|
2023-05-28 08:36:54 +00:00
|
|
|
|
|
2024-09-01 13:53:44 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
def get_filepaths(log_file: str) -> list[str]:
|
|
|
|
|
return [
|
|
|
|
|
x.rstrip("\u0000")
|
|
|
|
|
for x in re.findall(r"(?:[A-Za-z]:)?(?:[\\/]+[^\\/:\"\r\n]+)+", log_file)
|
|
|
|
|
]
|
|
|
|
|
|
2023-05-05 16:38:01 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
def get_main_ro_section(log_file: str) -> Optional[dict[str, str]]:
|
2023-07-05 06:35:00 +00:00
|
|
|
|
ro_section_matches = re.findall(
|
|
|
|
|
r"PrintRoSectionInfo: main:[\r\n]((?:\s+.*[\r\n])*)", log_file
|
2023-05-05 16:38:01 +00:00
|
|
|
|
)
|
2023-07-05 06:35:00 +00:00
|
|
|
|
if ro_section_matches and len(ro_section_matches) > 0:
|
|
|
|
|
ro_section_match: str = ro_section_matches[-1]
|
2023-05-05 16:38:01 +00:00
|
|
|
|
ro_section = {"module": "", "sdk_libraries": []}
|
2023-07-05 06:35:00 +00:00
|
|
|
|
if ro_section_match is None or len(ro_section_match) == 0:
|
2023-05-05 16:38:01 +00:00
|
|
|
|
return None
|
2023-07-05 06:35:00 +00:00
|
|
|
|
for line in ro_section_match.splitlines():
|
2023-05-05 16:38:01 +00:00
|
|
|
|
line = line.strip()
|
|
|
|
|
if line.startswith("Module:"):
|
|
|
|
|
ro_section["module"] = line[8:]
|
|
|
|
|
elif line.startswith("SDK Libraries:"):
|
|
|
|
|
ro_section["sdk_libraries"].append(line[19:])
|
|
|
|
|
elif line.startswith("SDK "):
|
|
|
|
|
ro_section["sdk_libraries"].append(line[4:])
|
|
|
|
|
else:
|
|
|
|
|
break
|
|
|
|
|
return ro_section
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_app_info(
|
|
|
|
|
log_file: str,
|
|
|
|
|
) -> Optional[tuple[str, str, str, list[str], dict[str, str]]]:
|
2023-06-26 06:00:53 +00:00
|
|
|
|
game_name_match = re.findall(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
r"Loader [A-Za-z]*: Application Loaded:\s([^;\n\r]*)",
|
|
|
|
|
log_file,
|
|
|
|
|
re.MULTILINE,
|
|
|
|
|
)
|
2023-06-26 06:00:53 +00:00
|
|
|
|
if game_name_match:
|
|
|
|
|
game_name = game_name_match[-1].rstrip()
|
|
|
|
|
app_id_match = re.match(r".* \[([a-zA-Z0-9]*)\]", game_name)
|
|
|
|
|
if app_id_match:
|
|
|
|
|
app_id = app_id_match.group(1).strip().upper()
|
2023-07-05 06:35:00 +00:00
|
|
|
|
else:
|
|
|
|
|
app_id = ""
|
|
|
|
|
bids_match_all = re.findall(
|
|
|
|
|
r"Build ids found for title ([a-zA-Z0-9]*):[\n\r]*((?:\s+.*[\n\r]+)+)",
|
2023-05-05 16:38:01 +00:00
|
|
|
|
log_file,
|
|
|
|
|
)
|
2023-07-05 06:35:00 +00:00
|
|
|
|
if bids_match_all and len(bids_match_all) > 0:
|
|
|
|
|
bids_match: tuple[str] = bids_match_all[-1]
|
2023-05-05 16:38:01 +00:00
|
|
|
|
app_id_from_bids = None
|
|
|
|
|
build_ids = None
|
2023-07-05 06:35:00 +00:00
|
|
|
|
if bids_match[0] is not None:
|
|
|
|
|
app_id_from_bids = bids_match[0].strip().upper()
|
|
|
|
|
if bids_match[1] is not None:
|
2023-05-05 16:38:01 +00:00
|
|
|
|
build_ids = [
|
|
|
|
|
bid.strip().upper()
|
2023-07-05 06:35:00 +00:00
|
|
|
|
for bid in bids_match[1].splitlines()
|
2023-05-05 16:38:01 +00:00
|
|
|
|
if is_build_id_valid(bid.strip())
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
game_name,
|
|
|
|
|
app_id,
|
|
|
|
|
app_id_from_bids,
|
|
|
|
|
build_ids,
|
|
|
|
|
LogAnalyser.get_main_ro_section(log_file),
|
|
|
|
|
)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def contains_errors(search_terms, errors):
|
|
|
|
|
for term in search_terms:
|
|
|
|
|
for error_lines in errors:
|
|
|
|
|
line = "\n".join(error_lines)
|
|
|
|
|
if term in line:
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def __init__(self, log_text: Union[str, list[str]]):
|
|
|
|
|
self.__init_members()
|
|
|
|
|
|
|
|
|
|
if isinstance(log_text, str):
|
|
|
|
|
self._log_text = log_text.replace("\r\n", "\n")
|
|
|
|
|
elif isinstance(log_text, list):
|
|
|
|
|
self._log_text = "\n".join(log_text)
|
|
|
|
|
else:
|
|
|
|
|
raise TypeError(log_text)
|
|
|
|
|
|
|
|
|
|
# Large files show a header value when not downloaded completely
|
|
|
|
|
# this regex makes sure that the log text to read starts from the first timestamp, ignoring headers
|
|
|
|
|
log_file_header_regex = re.compile(r"\d{2}:\d{2}:\d{2}\.\d{3}.*", re.DOTALL)
|
|
|
|
|
log_file_match = re.search(log_file_header_regex, self._log_text)
|
|
|
|
|
if log_file_match and log_file_match.group(0) is not None:
|
|
|
|
|
self._log_text = log_file_match.group(0)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError("No log entries found.")
|
|
|
|
|
|
|
|
|
|
self.__get_errors()
|
|
|
|
|
self.__get_hardware_info()
|
2024-05-15 14:58:20 +00:00
|
|
|
|
self.__get_settings_info()
|
2023-05-05 16:38:01 +00:00
|
|
|
|
self.__get_ryujinx_info()
|
|
|
|
|
self.__get_app_name()
|
|
|
|
|
self.__get_mods()
|
|
|
|
|
self.__get_cheats()
|
|
|
|
|
self.__get_notes()
|
|
|
|
|
|
|
|
|
|
def __init_members(self):
|
|
|
|
|
self._hardware_info = {
|
|
|
|
|
"cpu": "Unknown",
|
|
|
|
|
"gpu": "Unknown",
|
|
|
|
|
"ram": "Unknown",
|
|
|
|
|
"os": "Unknown",
|
|
|
|
|
}
|
|
|
|
|
self._emu_info = {
|
|
|
|
|
"ryu_version": "Unknown",
|
|
|
|
|
"ryu_firmware": "Unknown",
|
|
|
|
|
"logs_enabled": None,
|
|
|
|
|
}
|
|
|
|
|
self._game_info = {
|
|
|
|
|
"game_name": "Unknown",
|
|
|
|
|
"errors": "No errors found in log",
|
|
|
|
|
"mods": "No mods found",
|
|
|
|
|
"cheats": "No cheats found",
|
|
|
|
|
}
|
|
|
|
|
self._settings = {
|
|
|
|
|
"audio_backend": "Unknown",
|
|
|
|
|
"backend_threading": "Unknown",
|
|
|
|
|
"docked": "Unknown",
|
|
|
|
|
"expand_ram": "Unknown",
|
|
|
|
|
"fs_integrity": "Unknown",
|
|
|
|
|
"graphics_backend": "Unknown",
|
|
|
|
|
"ignore_missing_services": "Unknown",
|
|
|
|
|
"memory_manager": "Unknown",
|
|
|
|
|
"pptc": "Unknown",
|
|
|
|
|
"shader_cache": "Unknown",
|
|
|
|
|
"vsync": "Unknown",
|
2024-05-15 14:58:20 +00:00
|
|
|
|
"hypervisor": "Unknown",
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"resolution_scale": "Unknown",
|
|
|
|
|
"anisotropic_filtering": "Unknown",
|
|
|
|
|
"aspect_ratio": "Unknown",
|
|
|
|
|
"texture_recompression": "Unknown",
|
|
|
|
|
}
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes = set()
|
2023-05-05 16:38:01 +00:00
|
|
|
|
self._log_errors = []
|
|
|
|
|
|
|
|
|
|
def __get_errors(self):
|
|
|
|
|
errors = []
|
|
|
|
|
curr_error_lines = []
|
|
|
|
|
error_line = False
|
|
|
|
|
for line in self._log_text.splitlines():
|
|
|
|
|
if len(line.strip()) == 0:
|
|
|
|
|
continue
|
|
|
|
|
if "|E|" in line:
|
|
|
|
|
curr_error_lines = [line]
|
|
|
|
|
errors.append(curr_error_lines)
|
|
|
|
|
error_line = True
|
|
|
|
|
elif error_line and line[0] == " ":
|
|
|
|
|
curr_error_lines.append(line)
|
|
|
|
|
if len(curr_error_lines) > 0:
|
|
|
|
|
errors.append(curr_error_lines)
|
|
|
|
|
|
|
|
|
|
self._log_errors = errors
|
|
|
|
|
|
|
|
|
|
def __get_hardware_info(self):
|
|
|
|
|
for setting in self._hardware_info.keys():
|
|
|
|
|
match setting:
|
|
|
|
|
case "cpu":
|
|
|
|
|
cpu_match = re.search(
|
|
|
|
|
r"CPU:\s([^;\n\r]*)", self._log_text, re.MULTILINE
|
|
|
|
|
)
|
|
|
|
|
if cpu_match is not None and cpu_match.group(1) is not None:
|
|
|
|
|
self._hardware_info[setting] = cpu_match.group(1).rstrip()
|
|
|
|
|
|
|
|
|
|
case "ram":
|
2023-11-20 18:48:35 +00:00
|
|
|
|
sizes = "|".join(Size.names())
|
2023-05-05 16:38:01 +00:00
|
|
|
|
ram_match = re.search(
|
2023-11-20 18:48:35 +00:00
|
|
|
|
rf"RAM: Total ([\d.]+) ({sizes}) ; Available ([\d.]+) ({sizes})",
|
2023-05-05 16:38:01 +00:00
|
|
|
|
self._log_text,
|
|
|
|
|
re.MULTILINE,
|
|
|
|
|
)
|
2023-06-09 14:27:23 +00:00
|
|
|
|
if ram_match is not None:
|
|
|
|
|
try:
|
2023-11-20 18:48:35 +00:00
|
|
|
|
dest_unit = Size.MiB
|
|
|
|
|
|
2023-06-09 14:27:23 +00:00
|
|
|
|
ram_available = float(ram_match.group(3))
|
2023-11-20 18:48:35 +00:00
|
|
|
|
ram_available = Size.from_name(ram_match.group(4)).convert(
|
|
|
|
|
ram_available, dest_unit
|
|
|
|
|
)
|
2023-06-09 14:27:23 +00:00
|
|
|
|
|
2023-10-09 20:53:20 +00:00
|
|
|
|
ram_total = float(ram_match.group(1))
|
2023-11-20 18:48:35 +00:00
|
|
|
|
ram_total = Size.from_name(ram_match.group(2)).convert(
|
|
|
|
|
ram_total, dest_unit
|
|
|
|
|
)
|
2023-10-09 20:53:20 +00:00
|
|
|
|
|
2024-03-02 08:56:41 +00:00
|
|
|
|
self._hardware_info[setting] = (
|
|
|
|
|
f"{ram_available:.0f}/{ram_total:.0f} {dest_unit.name}"
|
|
|
|
|
)
|
2023-06-09 14:27:23 +00:00
|
|
|
|
except ValueError:
|
2023-10-09 20:53:20 +00:00
|
|
|
|
# ram_match.group(1) or ram_match.group(3) couldn't be parsed as a float.
|
2023-06-09 14:27:23 +00:00
|
|
|
|
self._hardware_info[setting] = "Error"
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
case "os":
|
|
|
|
|
os_match = re.search(
|
|
|
|
|
r"Operating System:\s([^;\n\r]*)",
|
|
|
|
|
self._log_text,
|
|
|
|
|
re.MULTILINE,
|
|
|
|
|
)
|
|
|
|
|
if os_match is not None and os_match.group(1) is not None:
|
|
|
|
|
self._hardware_info[setting] = os_match.group(1).rstrip()
|
|
|
|
|
|
|
|
|
|
case "gpu":
|
|
|
|
|
gpu_match = re.search(
|
|
|
|
|
r"PrintGpuInformation:\s([^;\n\r]*)",
|
|
|
|
|
self._log_text,
|
|
|
|
|
re.MULTILINE,
|
|
|
|
|
)
|
|
|
|
|
if gpu_match is not None and gpu_match.group(1) is not None:
|
|
|
|
|
self._hardware_info[setting] = gpu_match.group(1).rstrip()
|
|
|
|
|
|
|
|
|
|
case _:
|
|
|
|
|
raise NotImplementedError(setting)
|
|
|
|
|
|
|
|
|
|
def __get_ryujinx_info(self):
|
|
|
|
|
for setting in self._emu_info.keys():
|
|
|
|
|
match setting:
|
|
|
|
|
case "ryu_version":
|
|
|
|
|
for line in self._log_text.splitlines():
|
|
|
|
|
if "Ryujinx Version:" in line:
|
|
|
|
|
self._emu_info[setting] = line.split()[-1].strip()
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
case "logs_enabled":
|
|
|
|
|
logs_match = re.search(
|
|
|
|
|
r"Logs Enabled:\s([^;\n\r]*)", self._log_text, re.MULTILINE
|
|
|
|
|
)
|
|
|
|
|
if logs_match is not None and logs_match.group(1) is not None:
|
|
|
|
|
self._emu_info[setting] = logs_match.group(1).rstrip()
|
|
|
|
|
|
|
|
|
|
case "ryu_firmware":
|
|
|
|
|
for line in self._log_text.splitlines():
|
|
|
|
|
if "Firmware Version:" in line:
|
|
|
|
|
self._emu_info[setting] = line.split()[-1].strip()
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
case _:
|
|
|
|
|
raise NotImplementedError(setting)
|
|
|
|
|
|
|
|
|
|
def __get_setting_value(self, name, key):
|
|
|
|
|
values = [
|
|
|
|
|
line.split()[-1]
|
|
|
|
|
for line in self._log_text.splitlines()
|
|
|
|
|
if re.search(rf"LogValueChange: ({key})\s", line)
|
|
|
|
|
]
|
|
|
|
|
if len(values) > 0:
|
|
|
|
|
value = values[-1]
|
|
|
|
|
else:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
match name:
|
|
|
|
|
case "docked":
|
|
|
|
|
return "Docked" if value == "True" else "Handheld"
|
|
|
|
|
|
|
|
|
|
case "resolution_scale":
|
|
|
|
|
resolution_map = {
|
|
|
|
|
"-1": "Custom",
|
|
|
|
|
"1": "Native (720p/1080p)",
|
|
|
|
|
"2": "2x (1440p/2160p)",
|
|
|
|
|
"3": "3x (2160p/3240p)",
|
|
|
|
|
"4": "4x (2880p/4320p)",
|
|
|
|
|
}
|
|
|
|
|
if value in resolution_map.keys():
|
|
|
|
|
return resolution_map[value]
|
|
|
|
|
else:
|
|
|
|
|
return "Custom"
|
|
|
|
|
|
|
|
|
|
case "anisotropic_filtering":
|
|
|
|
|
anisotropic_map = {
|
|
|
|
|
"-1": "Auto",
|
|
|
|
|
"2": "2x",
|
|
|
|
|
"4": "4x",
|
|
|
|
|
"8": "8x",
|
|
|
|
|
"16": "16x",
|
|
|
|
|
}
|
|
|
|
|
if value in anisotropic_map.keys():
|
|
|
|
|
return anisotropic_map[value]
|
|
|
|
|
else:
|
|
|
|
|
return "Auto"
|
|
|
|
|
|
|
|
|
|
case "aspect_ratio":
|
|
|
|
|
aspect_map = {
|
|
|
|
|
"Fixed4x3": "4:3",
|
|
|
|
|
"Fixed16x9": "16:9",
|
|
|
|
|
"Fixed16x10": "16:10",
|
|
|
|
|
"Fixed21x9": "21:9",
|
|
|
|
|
"Fixed32x9": "32:9",
|
|
|
|
|
"Stretched": "Stretch to Fit Window",
|
|
|
|
|
}
|
|
|
|
|
if value in aspect_map.keys():
|
|
|
|
|
return aspect_map[value]
|
|
|
|
|
else:
|
|
|
|
|
return "Unknown"
|
|
|
|
|
|
|
|
|
|
case "pptc" | "shader_cache" | "texture_recompression" | "vsync":
|
|
|
|
|
return "Enabled" if value == "True" else "Disabled"
|
|
|
|
|
|
2024-05-15 14:58:20 +00:00
|
|
|
|
case "hypervisor":
|
|
|
|
|
if "mac" in self._hardware_info["os"]:
|
|
|
|
|
return "Enabled" if value == "True" else "Disabled"
|
|
|
|
|
else:
|
|
|
|
|
return "N/A"
|
2023-05-05 16:38:01 +00:00
|
|
|
|
case _:
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
def __get_settings_info(self):
|
|
|
|
|
settings_map = {
|
|
|
|
|
"anisotropic_filtering": "MaxAnisotropy",
|
|
|
|
|
"aspect_ratio": "AspectRatio",
|
|
|
|
|
"audio_backend": "AudioBackend",
|
|
|
|
|
"backend_threading": "BackendThreading",
|
|
|
|
|
"docked": "EnableDockedMode",
|
|
|
|
|
"expand_ram": "ExpandRam",
|
|
|
|
|
"fs_integrity": "EnableFsIntegrityChecks",
|
|
|
|
|
"graphics_backend": "GraphicsBackend",
|
|
|
|
|
"ignore_missing_services": "IgnoreMissingServices",
|
|
|
|
|
"memory_manager": "MemoryManagerMode",
|
|
|
|
|
"pptc": "EnablePtc",
|
|
|
|
|
"resolution_scale": "ResScale",
|
|
|
|
|
"shader_cache": "EnableShaderCache",
|
|
|
|
|
"texture_recompression": "EnableTextureRecompression",
|
|
|
|
|
"vsync": "EnableVsync",
|
2024-05-15 14:58:20 +00:00
|
|
|
|
"hypervisor": "UseHypervisor",
|
2023-05-05 16:38:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for key in self._settings.keys():
|
|
|
|
|
if key in settings_map:
|
|
|
|
|
self._settings[key] = self.__get_setting_value(key, settings_map[key])
|
|
|
|
|
else:
|
|
|
|
|
raise NotImplementedError(key)
|
|
|
|
|
|
|
|
|
|
def __get_mods(self):
|
2024-05-15 14:59:37 +00:00
|
|
|
|
mods_regex = re.compile(
|
|
|
|
|
r"Found\s(enabled|disabled)?\s?mod\s\'(.+?)\'\s(\[.+?\])"
|
|
|
|
|
)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
matches = re.findall(mods_regex, self._log_text)
|
|
|
|
|
if matches:
|
2024-05-15 14:59:37 +00:00
|
|
|
|
mods = [
|
|
|
|
|
{"mod": match[1], "status": match[0], "type": match[2]}
|
|
|
|
|
for match in matches
|
|
|
|
|
]
|
2023-05-05 16:38:01 +00:00
|
|
|
|
mods_status = [
|
2024-05-15 14:59:37 +00:00
|
|
|
|
f"ℹ️ {i['mod']} ({'ExeFS' if i['type'] == '[E]' else 'RomFS'})"
|
2023-05-05 16:38:01 +00:00
|
|
|
|
for i in mods
|
2024-05-15 14:59:37 +00:00
|
|
|
|
if i["status"] == "" or i["status"] == "enabled"
|
2023-05-05 16:38:01 +00:00
|
|
|
|
]
|
|
|
|
|
# Remove duplicated mods from output
|
|
|
|
|
mods_status = list(dict.fromkeys(mods_status))
|
|
|
|
|
|
|
|
|
|
self._game_info["mods"] = "\n".join(mods_status)
|
|
|
|
|
|
|
|
|
|
def __get_cheats(self):
|
2024-05-18 16:24:03 +00:00
|
|
|
|
# Make sure to skip cheats which fail to compile
|
|
|
|
|
cheat_regex = re.compile(
|
|
|
|
|
r"Installing cheat\s'(.+)'(?!\s\d{2}:\d{2}:\d{2}\.\d{3}\s\|E\|\sTamperMachine\sCompile)"
|
|
|
|
|
)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
matches = re.findall(cheat_regex, self._log_text)
|
|
|
|
|
if matches:
|
|
|
|
|
cheats = [f"ℹ️ {match}" for match in matches]
|
|
|
|
|
|
|
|
|
|
self._game_info["cheats"] = "\n".join(cheats)
|
|
|
|
|
|
|
|
|
|
def __get_app_name(self):
|
2023-06-26 06:00:53 +00:00
|
|
|
|
app_match = re.findall(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
r"Loader [A-Za-z]*: Application Loaded:\s([^;\n\r]*)",
|
|
|
|
|
self._log_text,
|
|
|
|
|
re.MULTILINE,
|
|
|
|
|
)
|
2023-06-26 06:00:53 +00:00
|
|
|
|
if app_match:
|
|
|
|
|
self._game_info["game_name"] = app_match[-1].rstrip()
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
def __get_controller_notes(self):
|
|
|
|
|
controllers_regex = re.compile(r"Hid Configure: ([^\r\n]+)")
|
|
|
|
|
controllers = re.findall(controllers_regex, self._log_text)
|
|
|
|
|
if controllers:
|
|
|
|
|
input_status = [f"ℹ {match}" for match in controllers]
|
|
|
|
|
# Hid Configure lines can appear multiple times, so converting to dict keys removes duplicate entries,
|
|
|
|
|
# also maintains the list order
|
|
|
|
|
input_status = list(dict.fromkeys(input_status))
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add("\n".join(input_status))
|
2023-05-05 16:38:01 +00:00
|
|
|
|
# If emulator crashes on startup without game load, there is no need to show controller notification at all
|
|
|
|
|
elif self._game_info["game_name"] != "Unknown":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add("⚠️ No controller information found")
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
def __get_os_notes(self):
|
|
|
|
|
if (
|
|
|
|
|
"Windows" in self._hardware_info["os"]
|
|
|
|
|
and self._settings["graphics_backend"] != "Vulkan"
|
|
|
|
|
):
|
|
|
|
|
if "Intel" in self._hardware_info["gpu"]:
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"**⚠️ Intel iGPU users should consider using Vulkan graphics backend**"
|
|
|
|
|
)
|
|
|
|
|
if "AMD" in self._hardware_info["gpu"]:
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"**⚠️ AMD GPU users should consider using Vulkan graphics backend**"
|
|
|
|
|
)
|
|
|
|
|
|
2024-08-27 17:34:24 +00:00
|
|
|
|
def __get_cpu_notes(self):
|
|
|
|
|
if "VirtualApple" in self._hardware_info["cpu"]:
|
|
|
|
|
self._notes.add("🔴 **Rosetta should be disabled**")
|
|
|
|
|
|
2023-05-05 16:38:01 +00:00
|
|
|
|
def __get_log_notes(self):
|
2023-10-09 20:53:20 +00:00
|
|
|
|
default_logs = ["Info", "Warning", "Error", "Guest"]
|
2023-05-05 16:38:01 +00:00
|
|
|
|
user_logs = []
|
|
|
|
|
if self._emu_info["logs_enabled"] is not None:
|
|
|
|
|
user_logs = (
|
|
|
|
|
self._emu_info["logs_enabled"].rstrip().replace(" ", "").split(",")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if "Debug" in user_logs:
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ **Debug logs enabled will have a negative impact on performance**"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
disabled_logs = set(default_logs).difference(set(user_logs))
|
|
|
|
|
if disabled_logs:
|
|
|
|
|
logs_status = [f"⚠️ {log} log is not enabled" for log in disabled_logs]
|
|
|
|
|
log_string = "\n".join(logs_status)
|
|
|
|
|
else:
|
|
|
|
|
log_string = "✅ Default logs enabled"
|
|
|
|
|
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(log_string)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
def __get_settings_notes(self):
|
|
|
|
|
if self._settings["audio_backend"] == "Dummy":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ Dummy audio backend, consider changing to SDL2 or OpenAL"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if self._settings["pptc"] == "Disabled":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add("🔴 **PPTC cache should be enabled**")
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
if self._settings["shader_cache"] == "Disabled":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add("🔴 **Shader cache should be enabled**")
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
if self._settings["expand_ram"] == "True":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ `Use alternative memory layout` should only be enabled for 4K mods"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if self._settings["memory_manager"] == "SoftwarePageTable":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"🔴 **`Software` setting in Memory Manager Mode will give slower performance than the default setting of `Host unchecked`**"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if self._settings["ignore_missing_services"] == "True":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ `Ignore Missing Services` being enabled can cause instability"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if self._settings["vsync"] == "Disabled":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ V-Sync disabled can cause instability like games running faster than intended or longer load times"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if self._settings["fs_integrity"] == "Disabled":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ Disabling file integrity checks may cause corrupted dumps to not be detected"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if self._settings["backend_threading"] == "Off":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"🔴 **Graphics Backend Multithreading should be set to `Auto`**"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def __sort_notes(self):
|
|
|
|
|
def severity(log_note_string):
|
|
|
|
|
symbols = ["❌", "🔴", "⚠️", "ℹ", "✅"]
|
|
|
|
|
return next(
|
|
|
|
|
i for i, symbol in enumerate(symbols) if symbol in log_note_string
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
game_notes = [note for note in self._notes]
|
|
|
|
|
# Warnings split on the string after the warning symbol for alphabetical ordering
|
|
|
|
|
# Severity key then orders alphabetically sorted warnings to show most severe first
|
|
|
|
|
return sorted(sorted(game_notes, key=lambda x: x.split()[1]), key=severity)
|
|
|
|
|
|
|
|
|
|
def __get_notes(self):
|
|
|
|
|
for common_error in self.get_common_errors():
|
|
|
|
|
match common_error:
|
2023-11-20 18:48:35 +00:00
|
|
|
|
case CommonError.SHADER_CACHE_COLLISION:
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ Cache collision detected. Investigate possible shader cache issues"
|
|
|
|
|
)
|
2023-11-20 18:48:35 +00:00
|
|
|
|
case CommonError.SHADER_CACHE_CORRUPTION:
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ Cache corruption detected. Investigate possible shader cache issues"
|
|
|
|
|
)
|
2023-11-20 18:48:35 +00:00
|
|
|
|
case CommonError.DUMP_HASH:
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ Dump error detected. Investigate possible bad game/firmware dump issues"
|
|
|
|
|
)
|
2023-11-20 18:48:35 +00:00
|
|
|
|
case CommonError.UPDATE_KEYS:
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ Keys or firmware out of date, consider updating them"
|
|
|
|
|
)
|
2023-11-20 18:48:35 +00:00
|
|
|
|
case CommonError.FILE_PERMISSIONS:
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ File permission error. Consider deleting save directory and allowing Ryujinx to make a new one"
|
|
|
|
|
)
|
2023-11-20 18:48:35 +00:00
|
|
|
|
case CommonError.FILE_NOT_FOUND:
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
|
|
|
|
"⚠️ Save not found error. Consider starting game without a save file or using a new save file"
|
2023-05-05 16:38:01 +00:00
|
|
|
|
)
|
2023-11-20 18:48:35 +00:00
|
|
|
|
case CommonError.MISSING_SERVICES:
|
2023-05-05 16:38:01 +00:00
|
|
|
|
if self._settings["ignore_missing_services"] == "False":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ Consider enabling `Ignore Missing Services` in Ryujinx settings"
|
|
|
|
|
)
|
2023-11-20 18:48:35 +00:00
|
|
|
|
case CommonError.VULKAN_OUT_OF_MEMORY:
|
2023-05-05 16:38:01 +00:00
|
|
|
|
if self._settings["texture_recompression"] == "Disabled":
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ Consider enabling `Texture Recompression` in Ryujinx settings"
|
|
|
|
|
)
|
|
|
|
|
case _:
|
|
|
|
|
raise NotImplementedError(common_error)
|
|
|
|
|
|
2024-05-15 15:04:32 +00:00
|
|
|
|
timestamp_regex = re.compile(r"(\d{2}:\d{2}:\d{2}\.\d{3})\s+?\|")
|
2023-05-05 16:38:01 +00:00
|
|
|
|
latest_timestamp = re.findall(timestamp_regex, self._log_text)[-1]
|
|
|
|
|
if latest_timestamp:
|
|
|
|
|
timestamp_message = f"ℹ️ Time elapsed: `{latest_timestamp}`"
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(timestamp_message)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
if self.is_default_user_profile():
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
"⚠️ Default user profile in use, consider creating a custom one."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.__get_controller_notes()
|
|
|
|
|
self.__get_os_notes()
|
2024-08-27 17:34:24 +00:00
|
|
|
|
self.__get_cpu_notes()
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
self._emu_info["ryu_firmware"] == "Unknown"
|
|
|
|
|
and self._game_info["game_name"] != "Unknown"
|
|
|
|
|
):
|
|
|
|
|
firmware_warning = f"**❌ Nintendo Switch firmware not found**"
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(firmware_warning)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
self.__get_settings_notes()
|
|
|
|
|
if self.get_ryujinx_version() == RyujinxVersion.CUSTOM:
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add("**⚠️ Custom builds are not officially supported**")
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
def get_ryujinx_version(self):
|
|
|
|
|
mainline_version = re.compile(r"^\d\.\d\.\d+$")
|
|
|
|
|
old_mainline_version = re.compile(r"^\d\.\d\.(\d){4}$")
|
|
|
|
|
pr_version = re.compile(r"^\d\.\d\.\d\+([a-f]|\d){7}$")
|
|
|
|
|
ldn_version = re.compile(r"^\d\.\d\.\d-ldn\d+\.\d+(?:\.\d+|$)")
|
|
|
|
|
mac_version = re.compile(r"^\d\.\d\.\d-macos\d+(?:\.\d+(?:\.\d+|$)|$)")
|
|
|
|
|
|
|
|
|
|
if re.match(mainline_version, self._emu_info["ryu_version"]):
|
|
|
|
|
return RyujinxVersion.MASTER
|
|
|
|
|
elif re.match(old_mainline_version, self._emu_info["ryu_version"]):
|
|
|
|
|
return RyujinxVersion.OLD_MASTER
|
|
|
|
|
elif re.match(mac_version, self._emu_info["ryu_version"]):
|
|
|
|
|
return RyujinxVersion.MAC
|
|
|
|
|
elif re.match(ldn_version, self._emu_info["ryu_version"]):
|
|
|
|
|
return RyujinxVersion.LDN
|
|
|
|
|
elif re.match(pr_version, self._emu_info["ryu_version"]):
|
|
|
|
|
return RyujinxVersion.PR
|
|
|
|
|
else:
|
|
|
|
|
return RyujinxVersion.CUSTOM
|
|
|
|
|
|
|
|
|
|
def is_default_user_profile(self) -> bool:
|
|
|
|
|
return (
|
|
|
|
|
re.search(r"UserId: 00000000000000010000000000000000", self._log_text)
|
|
|
|
|
is not None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_last_error(self) -> Optional[list[str]]:
|
|
|
|
|
return self._log_errors[-1] if len(self._log_errors) > 0 else None
|
|
|
|
|
|
2023-11-20 18:48:35 +00:00
|
|
|
|
def get_common_errors(self) -> list[CommonError]:
|
2023-05-05 16:38:01 +00:00
|
|
|
|
errors = []
|
|
|
|
|
|
|
|
|
|
if self.contains_errors(["Cache collision found"], self._log_errors):
|
2023-11-20 18:48:35 +00:00
|
|
|
|
errors.append(CommonError.SHADER_CACHE_COLLISION)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
if self.contains_errors(
|
|
|
|
|
[
|
|
|
|
|
"ResultFsInvalidIvfcHash",
|
|
|
|
|
"ResultFsNonRealDataVerificationFailed",
|
|
|
|
|
],
|
|
|
|
|
self._log_errors,
|
|
|
|
|
):
|
2023-11-20 18:48:35 +00:00
|
|
|
|
errors.append(CommonError.DUMP_HASH)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
if self.contains_errors(
|
|
|
|
|
[
|
|
|
|
|
"Ryujinx.Graphics.Gpu.Shader.ShaderCache.Initialize()",
|
|
|
|
|
"System.IO.InvalidDataException: End of Central Directory record could not be found",
|
|
|
|
|
"ICSharpCode.SharpZipLib.Zip.ZipException: Cannot find central directory",
|
|
|
|
|
],
|
|
|
|
|
self._log_errors,
|
|
|
|
|
):
|
2023-11-20 18:48:35 +00:00
|
|
|
|
errors.append(CommonError.SHADER_CACHE_CORRUPTION)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
if self.contains_errors(["MissingKeyException"], self._log_errors):
|
2023-11-20 18:48:35 +00:00
|
|
|
|
errors.append(CommonError.UPDATE_KEYS)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
if self.contains_errors(["ResultFsPermissionDenied"], self._log_errors):
|
2023-11-20 18:48:35 +00:00
|
|
|
|
errors.append(CommonError.FILE_PERMISSIONS)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
if self.contains_errors(["ResultFsTargetNotFound"], self._log_errors):
|
2023-11-20 18:48:35 +00:00
|
|
|
|
errors.append(CommonError.FILE_NOT_FOUND)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
if self.contains_errors(["ServiceNotImplementedException"], self._log_errors):
|
2023-11-20 18:48:35 +00:00
|
|
|
|
errors.append(CommonError.MISSING_SERVICES)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
if self.contains_errors(["ErrorOutOfDeviceMemory"], self._log_errors):
|
2023-11-20 18:48:35 +00:00
|
|
|
|
errors.append(CommonError.VULKAN_OUT_OF_MEMORY)
|
2023-05-05 16:38:01 +00:00
|
|
|
|
|
|
|
|
|
return errors
|
|
|
|
|
|
|
|
|
|
def analyse_discord(
|
|
|
|
|
self, is_channel_allowed: bool, pr_channel: int
|
|
|
|
|
) -> dict[str, dict[str, str]]:
|
|
|
|
|
last_error = self.get_last_error()
|
|
|
|
|
if last_error is not None:
|
2023-05-28 08:33:58 +00:00
|
|
|
|
last_error = "\n".join(last_error[:2])
|
2023-05-05 16:38:01 +00:00
|
|
|
|
self._game_info["errors"] = f"```\n{last_error}\n```"
|
|
|
|
|
else:
|
|
|
|
|
self._game_info["errors"] = "No errors found in log"
|
|
|
|
|
|
|
|
|
|
# Limit mods and cheats to 5 entries
|
|
|
|
|
mods = self._game_info["mods"].splitlines()
|
|
|
|
|
cheats = self._game_info["cheats"].splitlines()
|
|
|
|
|
if len(mods) > 5:
|
|
|
|
|
limit_mods = mods[:5]
|
|
|
|
|
limit_mods.append(f"✂️ {len(mods) - 5} other mods")
|
|
|
|
|
self._game_info["mods"] = "\n".join(limit_mods)
|
|
|
|
|
if len(cheats) > 5:
|
|
|
|
|
limit_cheats = cheats[:5]
|
|
|
|
|
limit_cheats.append(f"✂️ {len(cheats) - 5} other cheats")
|
|
|
|
|
self._game_info["cheats"] = "\n".join(limit_cheats)
|
|
|
|
|
|
|
|
|
|
if is_channel_allowed and self.get_ryujinx_version() == RyujinxVersion.PR:
|
2024-01-06 14:33:01 +00:00
|
|
|
|
self._notes.add(
|
2023-05-05 16:38:01 +00:00
|
|
|
|
f"**⚠️ PR build logs should be posted in <#{pr_channel}> if reporting bugs or tests**"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self._notes = self.__sort_notes()
|
|
|
|
|
full_game_info = self._game_info
|
|
|
|
|
full_game_info["notes"] = (
|
|
|
|
|
"\n".join(self._notes) if len(self._notes) > 0 else "Nothing to note"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"hardware_info": self._hardware_info,
|
|
|
|
|
"emu_info": self._emu_info,
|
|
|
|
|
"game_info": full_game_info,
|
|
|
|
|
"settings": self._settings,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def analyse(self) -> dict[str, Union[dict[str, str], list[str], list[list[str]]]]:
|
|
|
|
|
self._notes = list(self.__sort_notes())
|
|
|
|
|
|
2023-05-28 08:33:58 +00:00
|
|
|
|
last_error = self.get_last_error()
|
|
|
|
|
if last_error is not None:
|
|
|
|
|
last_error = "\n".join(last_error[:2])
|
|
|
|
|
self._game_info["errors"] = f"```\n{last_error}\n```"
|
|
|
|
|
else:
|
|
|
|
|
self._game_info["errors"] = "No errors found in log"
|
|
|
|
|
|
2023-05-05 16:38:01 +00:00
|
|
|
|
return {
|
|
|
|
|
"hardware_info": self._hardware_info,
|
|
|
|
|
"emu_info": self._emu_info,
|
|
|
|
|
"game_info": self._game_info,
|
|
|
|
|
"notes": self._notes,
|
|
|
|
|
"errors": self._log_errors,
|
|
|
|
|
"settings": self._settings,
|
2023-07-05 06:35:00 +00:00
|
|
|
|
"app_info": self.get_app_info(self._log_text),
|
2024-09-01 13:53:44 +00:00
|
|
|
|
"paths": self.get_filepaths(self._log_text),
|
2023-05-05 16:38:01 +00:00
|
|
|
|
}
|
2023-07-05 06:35:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
|
parser.add_argument("log_file", type=str)
|
|
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
if not os.path.isfile(args.log_file):
|
|
|
|
|
print(f"Couldn't find log file: {args.log_file}")
|
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
|
|
with open(args.log_file, "r") as file:
|
|
|
|
|
text = file.read()
|
|
|
|
|
|
|
|
|
|
analyser = LogAnalyser(text)
|
|
|
|
|
result = analyser.analyse()
|
|
|
|
|
|
|
|
|
|
print(json.dumps(result, indent=2))
|