Add automatic Crowdin synchronization

This commit is contained in:
DisasterMo 2021-12-04 17:28:14 +01:00
parent 80ea450389
commit a0f582bc93
20 changed files with 28396 additions and 244 deletions

33
.github/workflows/crowdin_prep.yml vendored Normal file
View File

@ -0,0 +1,33 @@
# Prepare source texts & upload them to Crowdin
name: Crowdin Source Texts Upload
# on change to the English texts
on:
push:
branches:
- master
paths:
- 'libretro/libretro_core_options.h'
jobs:
upload_source_file:
runs-on: ubuntu-latest
steps:
- name: Setup Java JDK
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Setup Python
uses: actions/setup-python@v2
- name: Checkout
uses: actions/checkout@v2
- name: Upload Source
shell: bash
env:
CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }}
run: |
python3 intl/upload_workflow.py $CROWDIN_API_KEY "nestopia" "libretro/libretro_core_options.h"

46
.github/workflows/crowdin_translate.yml vendored Normal file
View File

@ -0,0 +1,46 @@
# Download translations form Crowdin & Recreate libretro_core_options_intl.h
name: Crowdin Translation Integration
on:
schedule:
# please choose a random time & weekday to avoid all repos synching at the same time
- cron: '15 19 * * 5' # Fridays at 7:15 PM, UTC
jobs:
create_intl_file:
runs-on: ubuntu-latest
steps:
- name: Setup Java JDK
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Setup Python
uses: actions/setup-python@v2
- name: Checkout
uses: actions/checkout@v2
with:
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
- name: Create intl file
shell: bash
env:
CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }}
run: |
python3 intl/download_workflow.py $CROWDIN_API_KEY "nestopia" "libretro/libretro_core_options_intl.h"
- name: Commit files
run: |
git config --local user.email "github-actions@github.com"
git config --local user.name "github-actions[bot]"
git add intl/*_workflow.py "libretro/libretro_core_options_intl.h"
git commit -m "Fetch translations & Recreate libretro_core_options_intl.h"
- name: GitHub Push
uses: ad-m/github-push-action@v0.6.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}

4
intl/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
__pycache__
crowdin-cli.jar
*.h
*.json

70
intl/activate.py Normal file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
import os
import glob
import random as r
# -------------------- MAIN -------------------- #
if __name__ == '__main__':
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
if os.path.basename(DIR_PATH) != "intl":
raise RuntimeError("Script is not in intl folder!")
BASE_PATH = os.path.dirname(DIR_PATH)
WORKFLOW_PATH = os.path.join(BASE_PATH, ".github", "workflows")
PREP_WF = os.path.join(WORKFLOW_PATH, "crowdin_prep.yml")
TRANSLATE_WF = os.path.join(WORKFLOW_PATH, "crowdin_translate.yml")
CORE_NAME = os.path.basename(BASE_PATH)
CORE_OP_FILE = os.path.join(BASE_PATH, "**", "libretro_core_options.h")
core_options_hits = glob.glob(CORE_OP_FILE, recursive=True)
if len(core_options_hits) == 0:
raise RuntimeError("libretro_core_options.h not found!")
elif len(core_options_hits) > 1:
print("More than one libretro_core_options.h file found:\n\n")
for i, file in enumerate(core_options_hits):
print(f"{i} {file}\n")
while True:
user_choice = input("Please choose one ('q' will exit): ")
if user_choice == 'q':
exit(0)
elif user_choice.isdigit():
core_op_file = core_options_hits[int(user_choice)]
break
else:
print("Please make a valid choice!\n\n")
else:
core_op_file = core_options_hits[0]
core_intl_file = os.path.join(os.path.dirname(core_op_file.replace(BASE_PATH, ''))[1:],
'libretro_core_options_intl.h')
core_op_file = os.path.join(os.path.dirname(core_op_file.replace(BASE_PATH, ''))[1:],
'libretro_core_options.h')
minutes = r.randrange(0, 59, 5)
hour = r.randrange(0, 23)
with open(PREP_WF, 'r') as wf_file:
prep_txt = wf_file.read()
prep_txt = prep_txt.replace("<CORE_NAME>", CORE_NAME)
prep_txt = prep_txt.replace("<PATH/TO>/libretro_core_options.h",
core_op_file)
with open(PREP_WF, 'w') as wf_file:
wf_file.write(prep_txt)
with open(TRANSLATE_WF, 'r') as wf_file:
translate_txt = wf_file.read()
translate_txt = translate_txt.replace('<0-59>', f"{minutes}")
translate_txt = translate_txt.replace('<0-23>', f"{hour}")
translate_txt = translate_txt.replace('# Fridays at , UTC',
f"# Fridays at {hour%12}:{minutes} {'AM' if hour < 12 else 'PM'}, UTC")
translate_txt = translate_txt.replace("<CORE_NAME>", CORE_NAME)
translate_txt = translate_txt.replace('<PATH/TO>/libretro_core_options_intl.h',
core_intl_file)
with open(TRANSLATE_WF, 'w') as wf_file:
wf_file.write(translate_txt)

95
intl/core_option_regex.py Normal file
View File

@ -0,0 +1,95 @@
import re
# 0: full struct; 1: up to & including first []; 2: content between first {}
p_struct = re.compile(r'(struct\s*[a-zA-Z0-9_\s]+\[])\s*'
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+)\s*)*'
r'=\s*' # =
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+)\s*)*'
r'{((?:.|[\r\n])*?)\{\s*NULL,\s*NULL,\s*NULL\s*(?:.|[\r\n])*?},?(?:.|[\r\n])*?};') # captures full struct, it's beginning and it's content
# 0: type name[]; 1: type; 2: name
p_type_name = re.compile(r'(retro_core_option_[a-zA-Z0-9_]+)\s*'
r'(option_cats([a-z_]{0,8})|option_defs([a-z_]*))\s*\[]')
# 0: full option; 1: key; 2: description; 3: additional info; 4: key/value pairs
p_option = re.compile(r'{\s*' # opening braces
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r'(\".*?\"|' # key start; group 1
r'[a-zA-Z0-9_]+\s*\((?:.|[\r\n])*?\)|'
r'[a-zA-Z0-9_]+\s*\[(?:.|[\r\n])*?]|'
r'[a-zA-Z0-9_]+\s*\".*?\")\s*' # key end
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r',\s*' # comma
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r'(\".*?\")\s*' # description; group 2
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r',\s*' # comma
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r'((?:' # group 3
r'(?:NULL|\"(?:.|[\r\n])*?\")\s*' # description in category, info, info in category, category
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r',?\s*' # comma
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r')+)'
r'(?:' # defs only start
r'{\s*' # opening braces
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r'((?:' # key/value pairs start; group 4
r'{\s*' # opening braces
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r'(?:NULL|\".*?\")\s*' # option key
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r',\s*' # comma
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r'(?:NULL|\".*?\")\s*' # option value
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r'}\s*' # closing braces
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r',?\s*' # comma
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r')*)' # key/value pairs end
r'}\s*' # closing braces
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r',?\s*' # comma
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r'(?:' # defaults start
r'(?:NULL|\".*?\")\s*' # default value
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r',?\s*' # comma
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r')*' # defaults end
r')?' # defs only end
r'},') # closing braces
# analyse option group 3
p_info = re.compile(r'(NULL|\"(?:.|[\r\n])*?\")\s*' # description in category, info, info in category, category
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r',')
p_info_cat = re.compile(r'(NULL|\"(?:.|[\r\n])*?\")')
# analyse option group 4
p_key_value = re.compile(r'{\s*' # opening braces
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r'(NULL|\".*?\")\s*' # option key; 1
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r',\s*' # comma
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r'(NULL|\".*?\")\s*' # option value; 2
r'(?:(?:\/\*(?:.|[\r\n])*?\*\/|\/\/.*[\r\n]+|#.*[\r\n]+)\s*)*'
r'}')
p_masked = re.compile(r'([A-Z_][A-Z0-9_]+)\s*(\"(?:"\s*"|\\\s*|.)*\")')
p_intl = re.compile(r'(struct retro_core_option_definition \*option_defs_intl\[RETRO_LANGUAGE_LAST]) = {'
r'((?:.|[\r\n])*?)};')
p_set = re.compile(r'static INLINE void libretro_set_core_options\(retro_environment_t environ_cb\)'
r'(?:.|[\r\n])*?};?\s*#ifdef __cplusplus\s*}\s*#endif')
p_yaml = re.compile(r'"project_id": "[0-9]+".*\s*'
r'"api_token": "([a-zA-Z0-9]+)".*\s*'
r'"base_path": "\./intl".*\s*'
r'"base_url": "https://api\.crowdin\.com".*\s*'
r'"preserve_hierarchy": true.*\s*'
r'"files": \[\s*'
r'\{\s*'
r'"source": "/_us/\*\.json",.*\s*'
r'"translation": "/_%two_letters_code%/%original_file_name%",.*\s*'
r'"skip_untranslated_strings": true.*\s*'
r'},\s*'
r']')

View File

@ -0,0 +1,620 @@
#!/usr/bin/env python3
"""Core options text extractor
The purpose of this script is to set up & provide functions for automatic generation of 'libretro_core_options_intl.h'
from 'libretro_core_options.h' using translations from Crowdin.
Both v1 and v2 structs are supported. It is, however, recommended to convert v1 files to v2 using the included
'v1_to_v2_converter.py'.
Usage:
python3 path/to/core_option_translation.py "path/to/where/libretro_core_options.h & libretro_core_options_intl.h/are" "core_name"
This script will:
1.) create key words for & extract the texts from libretro_core_options.h & save them into intl/_us/core_options.h
2.) do the same for any present translations in libretro_core_options_intl.h, saving those in their respective folder
"""
import core_option_regex as cor
import re
import os
import sys
import json
import urllib.request as req
import shutil
# LANG_CODE_TO_R_LANG = {'_ar': 'RETRO_LANGUAGE_ARABIC',
# '_ast': 'RETRO_LANGUAGE_ASTURIAN',
# '_chs': 'RETRO_LANGUAGE_CHINESE_SIMPLIFIED',
# '_cht': 'RETRO_LANGUAGE_CHINESE_TRADITIONAL',
# '_cs': 'RETRO_LANGUAGE_CZECH',
# '_cy': 'RETRO_LANGUAGE_WELSH',
# '_da': 'RETRO_LANGUAGE_DANISH',
# '_de': 'RETRO_LANGUAGE_GERMAN',
# '_el': 'RETRO_LANGUAGE_GREEK',
# '_eo': 'RETRO_LANGUAGE_ESPERANTO',
# '_es': 'RETRO_LANGUAGE_SPANISH',
# '_fa': 'RETRO_LANGUAGE_PERSIAN',
# '_fi': 'RETRO_LANGUAGE_FINNISH',
# '_fr': 'RETRO_LANGUAGE_FRENCH',
# '_gl': 'RETRO_LANGUAGE_GALICIAN',
# '_he': 'RETRO_LANGUAGE_HEBREW',
# '_hu': 'RETRO_LANGUAGE_HUNGARIAN',
# '_id': 'RETRO_LANGUAGE_INDONESIAN',
# '_it': 'RETRO_LANGUAGE_ITALIAN',
# '_ja': 'RETRO_LANGUAGE_JAPANESE',
# '_ko': 'RETRO_LANGUAGE_KOREAN',
# '_nl': 'RETRO_LANGUAGE_DUTCH',
# '_oc': 'RETRO_LANGUAGE_OCCITAN',
# '_pl': 'RETRO_LANGUAGE_POLISH',
# '_pt_br': 'RETRO_LANGUAGE_PORTUGUESE_BRAZIL',
# '_pt_pt': 'RETRO_LANGUAGE_PORTUGUESE_PORTUGAL',
# '_ru': 'RETRO_LANGUAGE_RUSSIAN',
# '_sk': 'RETRO_LANGUAGE_SLOVAK',
# '_sv': 'RETRO_LANGUAGE_SWEDISH',
# '_tr': 'RETRO_LANGUAGE_TURKISH',
# '_uk': 'RETRO_LANGUAGE_UKRAINIAN',
# '_us': 'RETRO_LANGUAGE_ENGLISH',
# '_vn': 'RETRO_LANGUAGE_VIETNAMESE'}
# these are handled by RetroArch directly - no need to include them in core translations
ON_OFFS = {'"enabled"', '"disabled"', '"true"', '"false"', '"on"', '"off"'}
def remove_special_chars(text: str, char_set=0, allow_non_ascii=False) -> str:
"""Removes special characters from a text.
:param text: String to be cleaned.
:param char_set: 0 -> remove all ASCII special chars except for '_' & 'space' (default)
1 -> remove invalid chars from file names
:param allow_non_ascii: False -> all non-ascii characters will be removed (default)
True -> non-ascii characters will be passed through
:return: Clean text.
"""
command_chars = [chr(unicode) for unicode in tuple(range(0, 32)) + (127,)]
special_chars = ([chr(unicode) for unicode in tuple(range(33, 48)) + tuple(range(58, 65)) + tuple(range(91, 95))
+ (96,) + tuple(range(123, 127))],
('\\', '/', ':', '*', '?', '"', '<', '>', '|', '#', '%',
'&', '{', '}', '$', '!', '¸', "'", '@', '+', '='))
res = text if allow_non_ascii \
else text.encode('ascii', errors='ignore').decode('unicode-escape')
for cm in command_chars:
res = res.replace(cm, '_')
for sp in special_chars[char_set]:
res = res.replace(sp, '_')
while res.startswith('_'):
res = res[1:]
while res.endswith('_'):
res = res[:-1]
return res
def clean_file_name(file_name: str) -> str:
"""Removes characters which might make file_name inappropriate for files on some OS.
:param file_name: File name to be cleaned.
:return: The clean file name.
"""
file_name = remove_special_chars(file_name, 1)
file_name = re.sub(r'__+', '_', file_name.replace(' ', '_'))
return file_name
def get_struct_type_name(decl: str) -> tuple:
""" Returns relevant parts of the struct declaration:
type, name of the struct and the language appendix, if present.
:param decl: The struct declaration matched by cor.p_type_name.
:return: Tuple, e.g.: ('retro_core_option_definition', 'option_defs_us', '_us')
"""
struct_match = cor.p_type_name.search(decl)
if struct_match:
if struct_match.group(3):
struct_type_name = struct_match.group(1, 2, 3)
return struct_type_name
elif struct_match.group(4):
struct_type_name = struct_match.group(1, 2, 4)
return struct_type_name
else:
struct_type_name = struct_match.group(1, 2)
return struct_type_name
else:
raise ValueError(f'No or incomplete struct declaration: {decl}!\n'
'Please make sure all structs are complete, including the type and name declaration.')
def is_viable_non_dupe(text: str, comparison) -> bool:
"""text must be longer than 2 ('""'), not 'NULL' and not in comparison.
:param text: String to be tested.
:param comparison: Dictionary or set to search for text in.
:return: bool
"""
return 2 < len(text) and text != 'NULL' and text not in comparison
def is_viable_value(text: str) -> bool:
"""text must be longer than 2 ('""'), not 'NULL' and text.lower() not in
{'"enabled"', '"disabled"', '"true"', '"false"', '"on"', '"off"'}.
:param text: String to be tested.
:return: bool
"""
return 2 < len(text) and text != 'NULL' and text.lower() not in ON_OFFS
def create_non_dupe(base_name: str, opt_num: int, comparison) -> str:
"""Makes sure base_name is not in comparison, and if it is it's renamed.
:param base_name: Name to check/make unique.
:param opt_num: Number of the option base_name belongs to, used in making it unique.
:param comparison: Dictionary or set to search for base_name in.
:return: Unique name.
"""
h = base_name
if h in comparison:
n = 0
h = h + '_O' + str(opt_num)
h_end = len(h)
while h in comparison:
h = h[:h_end] + '_' + str(n)
n += 1
return h
def get_texts(text: str) -> dict:
"""Extracts the strings, which are to be translated/are the translations,
from text and creates macro names for them.
:param text: The string to be parsed.
:return: Dictionary of the form { '_<lang>': { 'macro': 'string', ... }, ... }.
"""
# all structs: group(0) full struct, group(1) beginning, group(2) content
structs = cor.p_struct.finditer(text)
hash_n_string = {}
just_string = {}
for struct in structs:
struct_declaration = struct.group(1)
struct_type_name = get_struct_type_name(struct_declaration)
if 3 > len(struct_type_name):
lang = '_us'
else:
lang = struct_type_name[2]
if lang not in just_string:
hash_n_string[lang] = {}
just_string[lang] = set()
is_v2 = False
pre_name = ''
p = cor.p_info
if 'retro_core_option_v2_definition' == struct_type_name[0]:
is_v2 = True
elif 'retro_core_option_v2_category' == struct_type_name[0]:
pre_name = 'CATEGORY_'
p = cor.p_info_cat
struct_content = struct.group(2)
# 0: full option; 1: key; 2: description; 3: additional info; 4: key/value pairs
struct_options = cor.p_option.finditer(struct_content)
for opt, option in enumerate(struct_options):
# group 1: key
if option.group(1):
opt_name = pre_name + option.group(1)
# no special chars allowed in key
opt_name = remove_special_chars(opt_name).upper().replace(' ', '_')
else:
raise ValueError(f'No option name (key) found in struct {struct_type_name[1]} option {opt}!')
# group 2: description0
if option.group(2):
desc0 = option.group(2)
if is_viable_non_dupe(desc0, just_string[lang]):
just_string[lang].add(desc0)
m_h = create_non_dupe(re.sub(r'__+', '_', f'{opt_name}_LABEL'), opt, hash_n_string[lang])
hash_n_string[lang][m_h] = desc0
else:
raise ValueError(f'No label found in struct {struct_type_name[1]} option {option.group(1)}!')
# group 3: desc1, info0, info1, category
if option.group(3):
infos = option.group(3)
option_info = p.finditer(infos)
if is_v2:
desc1 = next(option_info).group(1)
if is_viable_non_dupe(desc1, just_string[lang]):
just_string[lang].add(desc1)
m_h = create_non_dupe(re.sub(r'__+', '_', f'{opt_name}_LABEL_CAT'), opt, hash_n_string[lang])
hash_n_string[lang][m_h] = desc1
last = None
m_h = None
for j, info in enumerate(option_info):
last = info.group(1)
if is_viable_non_dupe(last, just_string[lang]):
just_string[lang].add(last)
m_h = create_non_dupe(re.sub(r'__+', '_', f'{opt_name}_INFO_{j}'), opt,
hash_n_string[lang])
hash_n_string[lang][m_h] = last
if last in just_string[lang]: # category key should not be translated
hash_n_string[lang].pop(m_h)
just_string[lang].remove(last)
else:
for j, info in enumerate(option_info):
gr1 = info.group(1)
if is_viable_non_dupe(gr1, just_string[lang]):
just_string[lang].add(gr1)
m_h = create_non_dupe(re.sub(r'__+', '_', f'{opt_name}_INFO_{j}'), opt,
hash_n_string[lang])
hash_n_string[lang][m_h] = gr1
else:
raise ValueError(f'Too few arguments in struct {struct_type_name[1]} option {option.group(1)}!')
# group 4:
if option.group(4):
for j, kv_set in enumerate(cor.p_key_value.finditer(option.group(4))):
set_key, set_value = kv_set.group(1, 2)
if not is_viable_value(set_value):
if not is_viable_value(set_key):
continue
set_value = set_key
# re.fullmatch(r'(?:[+-][0-9]+)+', value[1:-1])
if set_value not in just_string[lang] and not re.sub(r'[+-]', '', set_value[1:-1]).isdigit():
clean_key = set_key[1:-1]
clean_key = remove_special_chars(clean_key).upper().replace(' ', '_')
m_h = create_non_dupe(re.sub(r'__+', '_', f"OPTION_VAL_{clean_key}"), opt, hash_n_string[lang])
hash_n_string[lang][m_h] = set_value
just_string[lang].add(set_value)
return hash_n_string
def create_msg_hash(intl_dir_path: str, core_name: str, keyword_string_dict: dict) -> dict:
"""Creates '<core_name>.h' files in 'intl/_<lang>/' containing the macro name & string combinations.
:param intl_dir_path: Path to the intl directory.
:param core_name: Name of the core, used for the files' paths.
:param keyword_string_dict: Dictionary of the form { '_<lang>': { 'macro': 'string', ... }, ... }.
:return: Dictionary of the form { '_<lang>': 'path/to/file (./intl/_<lang>/<core_name>.h)', ... }.
"""
files = {}
for localisation in keyword_string_dict:
path = os.path.join(intl_dir_path, core_name) # intl/<core_name>/
files[localisation] = os.path.join(path, localisation + '.h') # intl/<core_name>/_<lang>.h
if not os.path.exists(path):
os.makedirs(path)
with open(files[localisation], 'w', encoding='utf-8') as crowdin_file:
out_text = ''
for keyword in keyword_string_dict[localisation]:
out_text = f'{out_text}{keyword} {keyword_string_dict[localisation][keyword]}\n'
crowdin_file.write(out_text)
return files
def h2json(file_paths: dict) -> dict:
"""Converts .h files pointed to by file_paths into .jsons.
:param file_paths: Dictionary of the form { '_<lang>': 'path/to/file (./intl/_<lang>/<core_name>.h)', ... }.
:return: Dictionary of the form { '_<lang>': 'path/to/file (./intl/_<lang>/<core_name>.json)', ... }.
"""
jsons = {}
for file_lang in file_paths:
if not os.path.isfile(file_paths[file_lang]):
continue
jsons[file_lang] = file_paths[file_lang][:-2] + '.json'
p = cor.p_masked
with open(file_paths[file_lang], 'r+', encoding='utf-8') as h_file:
text = h_file.read()
result = p.finditer(text)
messages = {}
for msg in result:
key, val = msg.group(1, 2)
if key not in messages:
if key and val:
# unescape & remove "\n"
messages[key] = re.sub(r'"\s*(?:(?:/\*(?:.|[\r\n])*?\*/|//.*[\r\n]+)\s*)*"',
'\\\n', val[1:-1].replace('\\\"', '"'))
else:
print(f"DUPLICATE KEY in {file_paths[file_lang]}: {key}")
with open(jsons[file_lang], 'w', encoding='utf-8') as json_file:
json.dump(messages, json_file, indent=2)
return jsons
def json2h(intl_dir_path: str, file_list) -> None:
"""Converts .json file in json_file_path into an .h ready to be included in C code.
:param intl_dir_path: Path to the intl/<core_name> directory.
:param file_list: Iterator of os.DirEntry objects. Contains localisation files to convert.
:return: None
"""
p = cor.p_masked
def update(s_messages, s_template, s_source_messages, file_name):
translation = ''
template_messages = p.finditer(s_template)
for tp_msg in template_messages:
old_key = tp_msg.group(1)
if old_key in s_messages and s_messages[old_key] != s_source_messages[old_key]:
tl_msg_val = s_messages[old_key]
tl_msg_val = tl_msg_val.replace('"', '\\\"').replace('\n', '') # escape
translation = ''.join((translation, '#define ', old_key, file_name.upper(), f' "{tl_msg_val}"\n'))
else: # Remove English duplicates and non-translatable strings
translation = ''.join((translation, '#define ', old_key, file_name.upper(), ' NULL\n'))
return translation
us_h = os.path.join(intl_dir_path, '_us.h')
us_json = os.path.join(intl_dir_path, '_us.json')
with open(us_h, 'r', encoding='utf-8') as template_file:
template = template_file.read()
with open(us_json, 'r+', encoding='utf-8') as source_json_file:
source_messages = json.load(source_json_file)
for file in file_list:
if file.name.lower().startswith('_us') \
or file.name.lower().endswith('.h') \
or file.is_dir():
continue
with open(file.path, 'r+', encoding='utf-8') as json_file:
messages = json.load(json_file)
new_translation = update(messages, template, source_messages, os.path.splitext(file.name)[0])
with open(os.path.splitext(file.path)[0] + '.h', 'w', encoding='utf-8') as h_file:
h_file.seek(0)
h_file.write(new_translation)
h_file.truncate()
return
def get_crowdin_client(dir_path: str) -> str:
"""Makes sure the Crowdin CLI client is present. If it isn't, it is fetched & extracted.
:return: The path to 'crowdin-cli.jar'.
"""
jar_name = 'crowdin-cli.jar'
jar_path = os.path.join(dir_path, jar_name)
if not os.path.isfile(jar_path):
print('Downloading crowdin-cli.jar')
crowdin_cli_file = os.path.join(dir_path, 'crowdin-cli.zip')
crowdin_cli_url = 'https://downloads.crowdin.com/cli/v3/crowdin-cli.zip'
req.urlretrieve(crowdin_cli_url, crowdin_cli_file)
import zipfile
with zipfile.ZipFile(crowdin_cli_file, 'r') as zip_ref:
jar_dir = zip_ref.namelist()[0]
for file in zip_ref.namelist():
if file.endswith(jar_name):
jar_file = file
break
zip_ref.extract(jar_file)
os.rename(jar_file, jar_path)
os.remove(crowdin_cli_file)
shutil.rmtree(jar_dir)
return jar_path
def create_intl_file(localisation_file_path: str, intl_dir_path: str, text: str, file_path: str) -> None:
"""Creates 'libretro_core_options_intl.h' from Crowdin translations.
:param localisation_file_path: Path to 'libretro_core_options_intl.h'
:param intl_dir_path: Path to the intl/<core_name> directory.
:param text: Content of the 'libretro_core_options.h' being translated.
:param file_path: Path to the '_us.h' file, containing the original English texts.
:return: None
"""
msg_dict = {}
lang_up = ''
def replace_pair(pair_match):
"""Replaces a key-value-pair of an option with the macros corresponding to the language.
:param pair_match: The re match object representing the key-value-pair block.
:return: Replacement string.
"""
offset = pair_match.start(0)
if pair_match.group(1): # key
if pair_match.group(2) in msg_dict: # value
val = msg_dict[pair_match.group(2)] + lang_up
elif pair_match.group(1) in msg_dict: # use key if value not viable (e.g. NULL)
val = msg_dict[pair_match.group(1)] + lang_up
else:
return pair_match.group(0)
else:
return pair_match.group(0)
res = pair_match.group(0)[:pair_match.start(2) - offset] + val \
+ pair_match.group(0)[pair_match.end(2) - offset:]
return res
def replace_info(info_match):
"""Replaces the 'additional strings' of an option with the macros corresponding to the language.
:param info_match: The re match object representing the 'additional strings' block.
:return: Replacement string.
"""
offset = info_match.start(0)
if info_match.group(1) in msg_dict:
res = info_match.group(0)[:info_match.start(1) - offset] + \
msg_dict[info_match.group(1)] + lang_up + \
info_match.group(0)[info_match.end(1) - offset:]
return res
else:
return info_match.group(0)
def replace_option(option_match):
"""Replaces strings within an option
'{ "opt_key", "label", "additional strings", ..., { {"key", "value"}, ... }, ... }'
within a struct with the macros corresponding to the language:
'{ "opt_key", MACRO_LABEL, MACRO_STRINGS, ..., { {"key", MACRO_VALUE}, ... }, ... }'
:param option_match: The re match object representing the option.
:return: Replacement string.
"""
# label
offset = option_match.start(0)
if option_match.group(2):
res = option_match.group(0)[:option_match.start(2) - offset] + msg_dict[option_match.group(2)] + lang_up
else:
return option_match.group(0)
# additional block
if option_match.group(3):
res = res + option_match.group(0)[option_match.end(2) - offset:option_match.start(3) - offset]
new_info = p.sub(replace_info, option_match.group(3))
res = res + new_info
else:
return res + option_match.group(0)[option_match.end(2) - offset:]
# key-value-pairs
if option_match.group(4):
res = res + option_match.group(0)[option_match.end(3) - offset:option_match.start(4) - offset]
new_pairs = cor.p_key_value.sub(replace_pair, option_match.group(4))
res = res + new_pairs + option_match.group(0)[option_match.end(4) - offset:]
else:
res = res + option_match.group(0)[option_match.end(3) - offset:]
return res
# ------------------------------------------------------------------------------------
with open(file_path, 'r+', encoding='utf-8') as template: # intl/<core_name>/_us.h
masked_msgs = cor.p_masked.finditer(template.read())
for msg in masked_msgs:
msg_dict[msg.group(2)] = msg.group(1)
# top of the file - in case there is no file to copy it from
out_txt = "#ifndef LIBRETRO_CORE_OPTIONS_INTL_H__\n" \
"#define LIBRETRO_CORE_OPTIONS_INTL_H__\n\n" \
"#if defined(_MSC_VER) && (_MSC_VER >= 1500 && _MSC_VER < 1900)\n" \
"/* https://support.microsoft.com/en-us/kb/980263 */\n" \
'#pragma execution_character_set("utf-8")\n' \
"#pragma warning(disable:4566)\n" \
"#endif\n\n" \
"#include <libretro.h>\n\n" \
'#ifdef __cplusplus\n' \
'extern "C" {\n' \
'#endif\n'
if os.path.isfile(localisation_file_path):
# copy top of the file for re-use
with open(localisation_file_path, 'r', encoding='utf-8') as intl: # libretro_core_options_intl.h
in_text = intl.read()
intl_start = re.search(re.escape('/*\n'
' ********************************\n'
' * Core Option Definitions\n'
' ********************************\n'
'*/\n'), in_text)
if intl_start:
out_txt = in_text[:intl_start.end(0)]
else:
intl_start = re.search(re.escape('#ifdef __cplusplus\n'
'extern "C" {\n'
'#endif\n'), in_text)
if intl_start:
out_txt = in_text[:intl_start.end(0)]
# only write to file, if there is anything worthwhile to write!
overwrite = False
# iterate through localisation files
files = {}
for file in os.scandir(intl_dir_path):
files[file.name] = {'is_file': file.is_file(), 'path': file.path}
for file in sorted(files): # intl/<core_name>/_*
if files[file]['is_file'] \
and file.startswith('_') \
and file.endswith('.h') \
and not file.startswith('_us'):
translation_path = files[file]['path'] # <core_name>_<lang>.h
# all structs: group(0) full struct, group(1) beginning, group(2) content
struct_groups = cor.p_struct.finditer(text)
lang_low = os.path.splitext(file)[0].lower()
lang_up = lang_low.upper()
out_txt = out_txt + f'/* RETRO_LANGUAGE{lang_up} */\n\n' # /* RETRO_LANGUAGE_NM */
# copy adjusted translations (makros)
with open(translation_path, 'r+', encoding='utf-8') as f_in: # <core name>.h
out_txt = out_txt + f_in.read() + '\n'
# replace English texts with makros
for construct in struct_groups:
declaration = construct.group(1)
struct_type_name = get_struct_type_name(declaration)
if 3 > len(struct_type_name): # no language specifier
new_decl = re.sub(re.escape(struct_type_name[1]), struct_type_name[1] + lang_low, declaration)
else:
new_decl = re.sub(re.escape(struct_type_name[2]), lang_low, declaration)
if '_us' != struct_type_name[2]:
continue
p = cor.p_info
if 'retro_core_option_v2_category' == struct_type_name[0]:
p = cor.p_info_cat
offset_construct = construct.start(0)
start = construct.end(1) - offset_construct
end = construct.start(2) - offset_construct
out_txt = out_txt + new_decl + construct.group(0)[start:end]
content = construct.group(2)
new_content = cor.p_option.sub(replace_option, content)
start = construct.end(2) - offset_construct
out_txt = out_txt + new_content + construct.group(0)[start:] + '\n'
# for v2
if 'retro_core_option_v2_definition' == struct_type_name[0]:
out_txt = out_txt + f'struct retro_core_options_v2 options{lang_low}' \
' = {\n' \
f' option_cats{lang_low},\n' \
f' option_defs{lang_low}\n' \
'};\n\n'
# if it got this far, we've got something to write
overwrite = True
# only write to file, if there is anything worthwhile to write!
if overwrite:
with open(localisation_file_path, 'w', encoding='utf-8') as intl:
intl.write(out_txt + '\n#ifdef __cplusplus\n'
'}\n#endif\n'
'\n#endif')
return
# -------------------- MAIN -------------------- #
if __name__ == '__main__':
try:
if os.path.isfile(sys.argv[1]):
_temp = os.path.dirname(sys.argv[1])
else:
_temp = sys.argv[1]
while _temp.endswith('/') or _temp.endswith('\\'):
_temp = _temp[:-1]
TARGET_DIR_PATH = _temp
except IndexError:
TARGET_DIR_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
print("No path provided, assuming parent directory:\n" + TARGET_DIR_PATH)
CORE_NAME = clean_file_name(sys.argv[2])
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
H_FILE_PATH = os.path.join(TARGET_DIR_PATH, 'libretro_core_options.h')
INTL_FILE_PATH = os.path.join(TARGET_DIR_PATH, 'libretro_core_options_intl.h')
print('Getting texts from libretro_core_options.h')
with open(H_FILE_PATH, 'r+', encoding='utf-8') as _h_file:
_main_text = _h_file.read()
_hash_n_str = get_texts(_main_text)
_files = create_msg_hash(DIR_PATH, CORE_NAME, _hash_n_str)
_source_jsons = h2json(_files)
print('Getting texts from libretro_core_options_intl.h')
if os.path.isfile(INTL_FILE_PATH):
with open(INTL_FILE_PATH, 'r+', encoding='utf-8') as _intl_file:
_intl_text = _intl_file.read()
_hash_n_str_intl = get_texts(_intl_text)
_intl_files = create_msg_hash(DIR_PATH, CORE_NAME, _hash_n_str_intl)
_intl_jsons = h2json(_intl_files)
print('\nAll done!')

13
intl/crowdin.yaml Normal file
View File

@ -0,0 +1,13 @@
"project_id": "380544"
"api_token": "_secret_"
"base_url": "https://api.crowdin.com"
"preserve_hierarchy": true
"files":
[
{
"source": "/intl/_core_name_/_us.json",
"dest": "/_core_name_/_core_name_.json",
"translation": "/intl/_core_name_/_%two_letters_code%.json",
},
]

30
intl/crowdin_prep.py Normal file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
import core_option_translation as t
if __name__ == '__main__':
try:
if t.os.path.isfile(t.sys.argv[1]):
_temp = t.os.path.dirname(t.sys.argv[1])
else:
_temp = t.sys.argv[1]
while _temp.endswith('/') or _temp.endswith('\\'):
_temp = _temp[:-1]
TARGET_DIR_PATH = _temp
except IndexError:
TARGET_DIR_PATH = t.os.path.dirname(t.os.path.dirname(t.os.path.realpath(__file__)))
print("No path provided, assuming parent directory:\n" + TARGET_DIR_PATH)
CORE_NAME = t.clean_file_name(t.sys.argv[2])
DIR_PATH = t.os.path.dirname(t.os.path.realpath(__file__))
H_FILE_PATH = t.os.path.join(TARGET_DIR_PATH, 'libretro_core_options.h')
print('Getting texts from libretro_core_options.h')
with open(H_FILE_PATH, 'r+', encoding='utf-8') as _h_file:
_main_text = _h_file.read()
_hash_n_str = t.get_texts(_main_text)
_files = t.create_msg_hash(DIR_PATH, CORE_NAME, _hash_n_str)
_source_jsons = t.h2json(_files)
print('\nAll done!')

View File

@ -0,0 +1,93 @@
#!/usr/bin/env python3
import re
import os
import shutil
import subprocess
import sys
import urllib.request
import zipfile
import core_option_translation as t
# -------------------- MAIN -------------------- #
if __name__ == '__main__':
# Check Crowdin API Token and core name
try:
API_KEY = sys.argv[1]
CORE_NAME = t.clean_file_name(sys.argv[2])
except IndexError as e:
print('Please provide Crowdin API Token and core name!')
raise e
DIR_PATH = t.os.path.dirname(t.os.path.realpath(__file__))
YAML_PATH = t.os.path.join(DIR_PATH, 'crowdin.yaml')
# Apply Crowdin API Key
with open(YAML_PATH, 'r') as crowdin_config_file:
crowdin_config = crowdin_config_file.read()
crowdin_config = re.sub(r'"api_token": "_secret_"',
f'"api_token": "{API_KEY}"',
crowdin_config, 1)
crowdin_config = re.sub(r'/_core_name_',
f'/{CORE_NAME}'
, crowdin_config)
with open(YAML_PATH, 'w') as crowdin_config_file:
crowdin_config_file.write(crowdin_config)
try:
# Download Crowdin CLI
jar_name = 'crowdin-cli.jar'
jar_path = t.os.path.join(DIR_PATH, jar_name)
crowdin_cli_file = 'crowdin-cli.zip'
crowdin_cli_url = 'https://downloads.crowdin.com/cli/v3/' + crowdin_cli_file
crowdin_cli_path = t.os.path.join(DIR_PATH, crowdin_cli_file)
if not os.path.isfile(t.os.path.join(DIR_PATH, jar_name)):
print('download crowdin-cli.jar')
urllib.request.urlretrieve(crowdin_cli_url, crowdin_cli_path)
with zipfile.ZipFile(crowdin_cli_path, 'r') as zip_ref:
jar_dir = t.os.path.join(DIR_PATH, zip_ref.namelist()[0])
for file in zip_ref.namelist():
if file.endswith(jar_name):
jar_file = file
break
zip_ref.extract(jar_file, path=DIR_PATH)
os.rename(t.os.path.join(DIR_PATH, jar_file), jar_path)
os.remove(crowdin_cli_path)
shutil.rmtree(jar_dir)
print('upload source *.json')
subprocess.run(['java', '-jar', jar_path, 'upload', 'sources', '--config', YAML_PATH])
# Reset Crowdin API Key
with open(YAML_PATH, 'r') as crowdin_config_file:
crowdin_config = crowdin_config_file.read()
crowdin_config = re.sub(r'"api_token": ".*?"',
'"api_token": "_secret_"',
crowdin_config, 1)
# TODO this is NOT safe!
crowdin_config = re.sub(re.escape(f'/{CORE_NAME}'),
'/_core_name_',
crowdin_config)
with open(YAML_PATH, 'w') as crowdin_config_file:
crowdin_config_file.write(crowdin_config)
except Exception as e:
# Try really hard to reset Crowdin API Key
with open(YAML_PATH, 'r') as crowdin_config_file:
crowdin_config = crowdin_config_file.read()
crowdin_config = re.sub(r'"api_token": ".*?"',
'"api_token": "_secret_"',
crowdin_config, 1)
# TODO this is NOT safe!
crowdin_config = re.sub(re.escape(f'/{CORE_NAME}'),
'/_core_name_',
crowdin_config)
with open(YAML_PATH, 'w') as crowdin_config_file:
crowdin_config_file.write(crowdin_config)
raise e

39
intl/crowdin_translate.py Normal file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
import core_option_translation as t
if __name__ == '__main__':
try:
if t.os.path.isfile(t.sys.argv[1]):
_temp = t.os.path.dirname(t.sys.argv[1])
else:
_temp = t.sys.argv[1]
while _temp.endswith('/') or _temp.endswith('\\'):
_temp = _temp[:-1]
TARGET_DIR_PATH = _temp
except IndexError:
TARGET_DIR_PATH = t.os.path.dirname(t.os.path.dirname(t.os.path.realpath(__file__)))
print("No path provided, assuming parent directory:\n" + TARGET_DIR_PATH)
CORE_NAME = t.clean_file_name(t.sys.argv[2])
DIR_PATH = t.os.path.dirname(t.os.path.realpath(__file__))
LOCALISATIONS_PATH = t.os.path.join(DIR_PATH, CORE_NAME)
US_FILE_PATH = t.os.path.join(LOCALISATIONS_PATH, '_us.h')
H_FILE_PATH = t.os.path.join(TARGET_DIR_PATH, 'libretro_core_options.h')
INTL_FILE_PATH = t.os.path.join(TARGET_DIR_PATH, 'libretro_core_options_intl.h')
print('Getting texts from libretro_core_options.h')
with open(H_FILE_PATH, 'r+', encoding='utf-8') as _h_file:
_main_text = _h_file.read()
_hash_n_str = t.get_texts(_main_text)
_files = t.create_msg_hash(DIR_PATH, CORE_NAME, _hash_n_str)
_source_jsons = t.h2json(_files)
print('Converting translations *.json to *.h:')
localisation_files = t.os.scandir(LOCALISATIONS_PATH)
t.json2h(LOCALISATIONS_PATH, localisation_files)
print('Constructing libretro_core_options_intl.h')
t.create_intl_file(INTL_FILE_PATH, LOCALISATIONS_PATH, _main_text, _files["_us"])
print('\nAll done!')

View File

@ -0,0 +1,93 @@
#!/usr/bin/env python3
import re
import os
import shutil
import subprocess
import sys
import urllib.request
import zipfile
import core_option_translation as t
# -------------------- MAIN -------------------- #
if __name__ == '__main__':
# Check Crowdin API Token and core name
try:
API_KEY = sys.argv[1]
CORE_NAME = t.clean_file_name(sys.argv[2])
except IndexError as e:
print('Please provide Crowdin API Token and core name!')
raise e
DIR_PATH = t.os.path.dirname(t.os.path.realpath(__file__))
YAML_PATH = t.os.path.join(DIR_PATH, 'crowdin.yaml')
# Apply Crowdin API Key
with open(YAML_PATH, 'r') as crowdin_config_file:
crowdin_config = crowdin_config_file.read()
crowdin_config = re.sub(r'"api_token": "_secret_"',
f'"api_token": "{API_KEY}"',
crowdin_config, 1)
crowdin_config = re.sub(r'/_core_name_',
f'/{CORE_NAME}'
, crowdin_config)
with open(YAML_PATH, 'w') as crowdin_config_file:
crowdin_config_file.write(crowdin_config)
try:
# Download Crowdin CLI
jar_name = 'crowdin-cli.jar'
jar_path = t.os.path.join(DIR_PATH, jar_name)
crowdin_cli_file = 'crowdin-cli.zip'
crowdin_cli_url = 'https://downloads.crowdin.com/cli/v3/' + crowdin_cli_file
crowdin_cli_path = t.os.path.join(DIR_PATH, crowdin_cli_file)
if not os.path.isfile(t.os.path.join(DIR_PATH, jar_name)):
print('download crowdin-cli.jar')
urllib.request.urlretrieve(crowdin_cli_url, crowdin_cli_path)
with zipfile.ZipFile(crowdin_cli_path, 'r') as zip_ref:
jar_dir = t.os.path.join(DIR_PATH, zip_ref.namelist()[0])
for file in zip_ref.namelist():
if file.endswith(jar_name):
jar_file = file
break
zip_ref.extract(jar_file, path=DIR_PATH)
os.rename(t.os.path.join(DIR_PATH, jar_file), jar_path)
os.remove(crowdin_cli_path)
shutil.rmtree(jar_dir)
print('download translation *.json')
subprocess.run(['java', '-jar', jar_path, 'download', '--config', YAML_PATH])
# Reset Crowdin API Key
with open(YAML_PATH, 'r') as crowdin_config_file:
crowdin_config = crowdin_config_file.read()
crowdin_config = re.sub(r'"api_token": ".*?"',
'"api_token": "_secret_"',
crowdin_config, 1)
# TODO this is NOT safe!
crowdin_config = re.sub(re.escape(f'/{CORE_NAME}'),
'/_core_name_',
crowdin_config)
with open(YAML_PATH, 'w') as crowdin_config_file:
crowdin_config_file.write(crowdin_config)
except Exception as e:
# Try really hard to reset Crowdin API Key
with open(YAML_PATH, 'r') as crowdin_config_file:
crowdin_config = crowdin_config_file.read()
crowdin_config = re.sub(r'"api_token": ".*?"',
'"api_token": "_secret_"',
crowdin_config, 1)
# TODO this is NOT safe!
crowdin_config = re.sub(re.escape(f'/{CORE_NAME}'),
'/_core_name_',
crowdin_config)
with open(YAML_PATH, 'w') as crowdin_config_file:
crowdin_config_file.write(crowdin_config)
raise e

16
intl/download_workflow.py Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env python3
import sys
import subprocess
try:
api_key = sys.argv[1]
core_name = sys.argv[2]
dir_path = sys.argv[3]
except IndexError as e:
print('Please provide path to libretro_core_options.h, Crowdin API Token and core name!')
raise e
subprocess.run(['python3', 'intl/crowdin_prep.py', dir_path, core_name])
subprocess.run(['python3', 'intl/crowdin_translation_download.py', api_key, core_name])
subprocess.run(['python3', 'intl/crowdin_translate.py', dir_path, core_name])

125
intl/initial_sync.py Normal file
View File

@ -0,0 +1,125 @@
#!/usr/bin/env python3
import re
import os
import shutil
import subprocess
import sys
import time
import urllib.request
import zipfile
import core_option_translation as t
# -------------------- MAIN -------------------- #
if __name__ == '__main__':
# Check Crowdin API Token and core name
try:
API_KEY = sys.argv[1]
CORE_NAME = t.clean_file_name(sys.argv[2])
except IndexError as e:
print('Please provide Crowdin API Token and core name!')
raise e
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
YAML_PATH = os.path.join(DIR_PATH, 'crowdin.yaml')
# Apply Crowdin API Key
with open(YAML_PATH, 'r') as crowdin_config_file:
crowdin_config = crowdin_config_file.read()
crowdin_config = re.sub(r'"api_token": "_secret_"',
f'"api_token": "{API_KEY}"',
crowdin_config, 1)
crowdin_config = re.sub(r'/_core_name_',
f'/{CORE_NAME}'
, crowdin_config)
with open(YAML_PATH, 'w') as crowdin_config_file:
crowdin_config_file.write(crowdin_config)
try:
# Download Crowdin CLI
jar_name = 'crowdin-cli.jar'
jar_path = os.path.join(DIR_PATH, jar_name)
crowdin_cli_file = 'crowdin-cli.zip'
crowdin_cli_url = 'https://downloads.crowdin.com/cli/v3/' + crowdin_cli_file
crowdin_cli_path = os.path.join(DIR_PATH, crowdin_cli_file)
if not os.path.isfile(os.path.join(DIR_PATH, jar_name)):
print('download crowdin-cli.jar')
urllib.request.urlretrieve(crowdin_cli_url, crowdin_cli_path)
with zipfile.ZipFile(crowdin_cli_path, 'r') as zip_ref:
jar_dir = os.path.join(DIR_PATH, zip_ref.namelist()[0])
for file in zip_ref.namelist():
if file.endswith(jar_name):
jar_file = file
break
zip_ref.extract(jar_file, path=DIR_PATH)
os.rename(os.path.join(DIR_PATH, jar_file), jar_path)
os.remove(crowdin_cli_path)
shutil.rmtree(jar_dir)
print('upload source & translations *.json')
subprocess.run(['java', '-jar', jar_path, 'upload', 'sources', '--config', YAML_PATH])
subprocess.run(['java', '-jar', jar_path, 'upload', 'translations', '--config', YAML_PATH])
print('wait for crowdin server to process data')
time.sleep(10)
print('download translation *.json')
subprocess.run(['java', '-jar', jar_path, 'download', '--config', YAML_PATH])
# Reset Crowdin API Key
with open(YAML_PATH, 'r') as crowdin_config_file:
crowdin_config = crowdin_config_file.read()
crowdin_config = re.sub(r'"api_token": ".*?"', '"api_token": "_secret_"', crowdin_config, 1)
# TODO this is NOT safe!
crowdin_config = re.sub(re.escape(f'/{CORE_NAME}'),
'/_core_name_',
crowdin_config)
with open(YAML_PATH, 'w') as crowdin_config_file:
crowdin_config_file.write(crowdin_config)
with open('intl/upload_workflow.py', 'r') as workflow:
workflow_config = workflow.read()
workflow_config = workflow_config.replace(
"subprocess.run(['python3', 'intl/core_option_translation.py', dir_path, core_name])",
"subprocess.run(['python3', 'intl/crowdin_prep.py', dir_path, core_name])"
)
workflow_config = workflow_config.replace(
"subprocess.run(['python3', 'intl/initial_sync.py', api_key, core_name])",
"subprocess.run(['python3', 'intl/crowdin_source_upload.py', api_key, core_name])"
)
with open('intl/upload_workflow.py', 'w') as workflow:
workflow.write(workflow_config)
with open('intl/download_workflow.py', 'r') as workflow:
workflow_config = workflow.read()
workflow_config = workflow_config.replace(
"subprocess.run(['python3', 'intl/core_option_translation.py', dir_path, core_name])",
"subprocess.run(['python3', 'intl/crowdin_prep.py', dir_path, core_name])"
)
workflow_config = workflow_config.replace(
"subprocess.run(['python3', 'intl/initial_sync.py', api_key, core_name])",
"subprocess.run(['python3', 'intl/crowdin_translation_download.py', api_key, core_name])"
)
with open('intl/download_workflow.py', 'w') as workflow:
workflow.write(workflow_config)
except Exception as e:
# Try really hard to reset Crowdin API Key
with open(YAML_PATH, 'r') as crowdin_config_file:
crowdin_config = crowdin_config_file.read()
crowdin_config = re.sub(r'"api_token": ".*?"',
'"api_token": "_secret_"',
crowdin_config, 1)
# TODO this is NOT safe!
crowdin_config = re.sub(re.escape(f'/{CORE_NAME}'),
'/_core_name_',
crowdin_config)
with open(YAML_PATH, 'w') as crowdin_config_file:
crowdin_config_file.write(crowdin_config)
raise e

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
with open('intl/upload_workflow.py', 'r') as workflow:
workflow_config = workflow.read()
workflow_config = workflow_config.replace(
"subprocess.run(['python3', 'intl/core_option_translation.py', dir_path, core_name])",
"subprocess.run(['python3', 'intl/crowdin_prep.py', dir_path, core_name])"
)
workflow_config = workflow_config.replace(
"subprocess.run(['python3', 'intl/initial_sync.py', api_key, core_name])",
"subprocess.run(['python3', 'intl/crowdin_source_upload.py', api_key, core_name])"
)
with open('intl/upload_workflow.py', 'w') as workflow:
workflow.write(workflow_config)
with open('intl/download_workflow.py', 'r') as workflow:
workflow_config = workflow.read()
workflow_config = workflow_config.replace(
"subprocess.run(['python3', 'intl/core_option_translation.py', dir_path, core_name])",
"subprocess.run(['python3', 'intl/crowdin_prep.py', dir_path, core_name])"
)
workflow_config = workflow_config.replace(
"subprocess.run(['python3', 'intl/initial_sync.py', api_key, core_name])",
"subprocess.run(['python3', 'intl/crowdin_translation_download.py', api_key, core_name])"
)
with open('intl/download_workflow.py', 'w') as workflow:
workflow.write(workflow_config)

15
intl/upload_workflow.py Normal file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
import sys
import subprocess
try:
api_key = sys.argv[1]
core_name = sys.argv[2]
dir_path = sys.argv[3]
except IndexError as e:
print('Please provide path to libretro_core_options.h, Crowdin API Token and core name!')
raise e
subprocess.run(['python3', 'intl/crowdin_prep.py', dir_path, core_name])
subprocess.run(['python3', 'intl/crowdin_source_upload.py', api_key, core_name])

476
intl/v1_to_v2_converter.py Normal file
View File

@ -0,0 +1,476 @@
#!/usr/bin/env python3
"""Core options v1 to v2 converter
Just run this script as follows, to convert 'libretro_core_options.h' & 'Libretro_coreoptions_intl.h' to v2:
python3 "/path/to/v1_to_v2_converter.py" "/path/to/where/libretro_core_options.h & Libretro_coreoptions_intl.h/are"
The original files will be preserved as *.v1
"""
import core_option_regex as cor
import os
import sys
import glob
def create_v2_code_file(struct_text, file_name):
def replace_option(option_match):
_offset = option_match.start(0)
if option_match.group(3):
res = option_match.group(0)[:option_match.end(2) - _offset] + ',\n NULL' + \
option_match.group(0)[option_match.end(2) - _offset:option_match.end(3) - _offset] + \
'NULL,\n NULL,\n ' + option_match.group(0)[option_match.end(3) - _offset:]
else:
return option_match.group(0)
return res
comment_v1 = '/*\n' \
' ********************************\n' \
' * VERSION: 1.3\n' \
' ********************************\n' \
' *\n' \
' * - 1.3: Move translations to libretro_core_options_intl.h\n' \
' * - libretro_core_options_intl.h includes BOM and utf-8\n' \
' * fix for MSVC 2010-2013\n' \
' * - Added HAVE_NO_LANGEXTRA flag to disable translations\n' \
' * on platforms/compilers without BOM support\n' \
' * - 1.2: Use core options v1 interface when\n' \
' * RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION is >= 1\n' \
' * (previously required RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION == 1)\n' \
' * - 1.1: Support generation of core options v0 retro_core_option_value\n' \
' * arrays containing options with a single value\n' \
' * - 1.0: First commit\n' \
'*/\n'
comment_v2 = '/*\n' \
' ********************************\n' \
' * VERSION: 2.0\n' \
' ********************************\n' \
' *\n' \
' * - 2.0: Add support for core options v2 interface\n' \
' * - 1.3: Move translations to libretro_core_options_intl.h\n' \
' * - libretro_core_options_intl.h includes BOM and utf-8\n' \
' * fix for MSVC 2010-2013\n' \
' * - Added HAVE_NO_LANGEXTRA flag to disable translations\n' \
' * on platforms/compilers without BOM support\n' \
' * - 1.2: Use core options v1 interface when\n' \
' * RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION is >= 1\n' \
' * (previously required RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION == 1)\n' \
' * - 1.1: Support generation of core options v0 retro_core_option_value\n' \
' * arrays containing options with a single value\n' \
' * - 1.0: First commit\n' \
'*/\n'
p_intl = cor.p_intl
p_set = cor.p_set
new_set = 'static INLINE void libretro_set_core_options(retro_environment_t environ_cb,\n' \
' bool *categories_supported)\n' \
'{\n' \
' unsigned version = 0;\n' \
'#ifndef HAVE_NO_LANGEXTRA\n' \
' unsigned language = 0;\n' \
'#endif\n' \
'\n' \
' if (!environ_cb || !categories_supported)\n' \
' return;\n' \
'\n' \
' *categories_supported = false;\n' \
'\n' \
' if (!environ_cb(RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION, &version))\n' \
' version = 0;\n' \
'\n' \
' if (version >= 2)\n' \
' {\n' \
'#ifndef HAVE_NO_LANGEXTRA\n' \
' struct retro_core_options_v2_intl core_options_intl;\n' \
'\n' \
' core_options_intl.us = &options_us;\n' \
' core_options_intl.local = NULL;\n' \
'\n' \
' if (environ_cb(RETRO_ENVIRONMENT_GET_LANGUAGE, &language) &&\n' \
' (language < RETRO_LANGUAGE_LAST) && (language != RETRO_LANGUAGE_ENGLISH))\n' \
' core_options_intl.local = options_intl[language];\n' \
'\n' \
' *categories_supported = environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL,\n' \
' &core_options_intl);\n' \
'#else\n' \
' *categories_supported = environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2,\n' \
' &options_us);\n' \
'#endif\n' \
' }\n' \
' else\n' \
' {\n' \
' size_t i, j;\n' \
' size_t option_index = 0;\n' \
' size_t num_options = 0;\n' \
' struct retro_core_option_definition\n' \
' *option_v1_defs_us = NULL;\n' \
'#ifndef HAVE_NO_LANGEXTRA\n' \
' size_t num_options_intl = 0;\n' \
' struct retro_core_option_v2_definition\n' \
' *option_defs_intl = NULL;\n' \
' struct retro_core_option_definition\n' \
' *option_v1_defs_intl = NULL;\n' \
' struct retro_core_options_intl\n' \
' core_options_v1_intl;\n' \
'#endif\n' \
' struct retro_variable *variables = NULL;\n' \
' char **values_buf = NULL;\n' \
'\n' \
' /* Determine total number of options */\n' \
' while (true)\n' \
' {\n' \
' if (option_defs_us[num_options].key)\n' \
' num_options++;\n' \
' else\n' \
' break;\n' \
' }\n' \
'\n' \
' if (version >= 1)\n' \
' {\n' \
' /* Allocate US array */\n' \
' option_v1_defs_us = (struct retro_core_option_definition *)\n' \
' calloc(num_options + 1, sizeof(struct retro_core_option_definition));\n' \
'\n' \
' /* Copy parameters from option_defs_us array */\n' \
' for (i = 0; i < num_options; i++)\n' \
' {\n' \
' struct retro_core_option_v2_definition *option_def_us = &option_defs_us[i];\n' \
' struct retro_core_option_value *option_values = option_def_us->values;\n' \
' struct retro_core_option_definition *option_v1_def_us = &option_v1_defs_us[i];\n' \
' struct retro_core_option_value *option_v1_values = option_v1_def_us->values;\n' \
'\n' \
' option_v1_def_us->key = option_def_us->key;\n' \
' option_v1_def_us->desc = option_def_us->desc;\n' \
' option_v1_def_us->info = option_def_us->info;\n' \
' option_v1_def_us->default_value = option_def_us->default_value;\n' \
'\n' \
' /* Values must be copied individually... */\n' \
' while (option_values->value)\n' \
' {\n' \
' option_v1_values->value = option_values->value;\n' \
' option_v1_values->label = option_values->label;\n' \
'\n' \
' option_values++;\n' \
' option_v1_values++;\n' \
' }\n' \
' }\n' \
'\n' \
'#ifndef HAVE_NO_LANGEXTRA\n' \
' if (environ_cb(RETRO_ENVIRONMENT_GET_LANGUAGE, &language) &&\n' \
' (language < RETRO_LANGUAGE_LAST) && (language != RETRO_LANGUAGE_ENGLISH) &&\n' \
' options_intl[language])\n' \
' option_defs_intl = options_intl[language]->definitions;\n' \
'\n' \
' if (option_defs_intl)\n' \
' {\n' \
' /* Determine number of intl options */\n' \
' while (true)\n' \
' {\n' \
' if (option_defs_intl[num_options_intl].key)\n' \
' num_options_intl++;\n' \
' else\n' \
' break;\n' \
' }\n' \
'\n' \
' /* Allocate intl array */\n' \
' option_v1_defs_intl = (struct retro_core_option_definition *)\n' \
' calloc(num_options_intl + 1, sizeof(struct retro_core_option_definition));\n' \
'\n' \
' /* Copy parameters from option_defs_intl array */\n' \
' for (i = 0; i < num_options_intl; i++)\n' \
' {\n' \
' struct retro_core_option_v2_definition *option_def_intl = &option_defs_intl[i];\n' \
' struct retro_core_option_value *option_values = option_def_intl->values;\n' \
' struct retro_core_option_definition *option_v1_def_intl = &option_v1_defs_intl[i];\n' \
' struct retro_core_option_value *option_v1_values = option_v1_def_intl->values;\n' \
'\n' \
' option_v1_def_intl->key = option_def_intl->key;\n' \
' option_v1_def_intl->desc = option_def_intl->desc;\n' \
' option_v1_def_intl->info = option_def_intl->info;\n' \
' option_v1_def_intl->default_value = option_def_intl->default_value;\n' \
'\n' \
' /* Values must be copied individually... */\n' \
' while (option_values->value)\n' \
' {\n' \
' option_v1_values->value = option_values->value;\n' \
' option_v1_values->label = option_values->label;\n' \
'\n' \
' option_values++;\n' \
' option_v1_values++;\n' \
' }\n' \
' }\n' \
' }\n' \
'\n' \
' core_options_v1_intl.us = option_v1_defs_us;\n' \
' core_options_v1_intl.local = option_v1_defs_intl;\n' \
'\n' \
' environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL, &core_options_v1_intl);\n' \
'#else\n' \
' environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS, option_v1_defs_us);\n' \
'#endif\n' \
' }\n' \
' else\n' \
' {\n' \
' /* Allocate arrays */\n' \
' variables = (struct retro_variable *)calloc(num_options + 1,\n' \
' sizeof(struct retro_variable));\n' \
' values_buf = (char **)calloc(num_options, sizeof(char *));\n' \
'\n' \
' if (!variables || !values_buf)\n' \
' goto error;\n' \
'\n' \
' /* Copy parameters from option_defs_us array */\n' \
' for (i = 0; i < num_options; i++)\n' \
' {\n' \
' const char *key = option_defs_us[i].key;\n' \
' const char *desc = option_defs_us[i].desc;\n' \
' const char *default_value = option_defs_us[i].default_value;\n' \
' struct retro_core_option_value *values = option_defs_us[i].values;\n' \
' size_t buf_len = 3;\n' \
' size_t default_index = 0;\n' \
'\n' \
' values_buf[i] = NULL;\n' \
'\n' \
' if (desc)\n' \
' {\n' \
' size_t num_values = 0;\n' \
'\n' \
' /* Determine number of values */\n' \
' while (true)\n' \
' {\n' \
' if (values[num_values].value)\n' \
' {\n' \
' /* Check if this is the default value */\n' \
' if (default_value)\n' \
' if (strcmp(values[num_values].value, default_value) == 0)\n' \
' default_index = num_values;\n' \
'\n' \
' buf_len += strlen(values[num_values].value);\n' \
' num_values++;\n' \
' }\n' \
' else\n' \
' break;\n' \
' }\n' \
'\n' \
' /* Build values string */\n' \
' if (num_values > 0)\n' \
' {\n' \
' buf_len += num_values - 1;\n' \
' buf_len += strlen(desc);\n' \
'\n' \
' values_buf[i] = (char *)calloc(buf_len, sizeof(char));\n' \
' if (!values_buf[i])\n' \
' goto error;\n' \
'\n' \
' strcpy(values_buf[i], desc);\n' \
' strcat(values_buf[i], "; ");\n' \
'\n' \
' /* Default value goes first */\n' \
' strcat(values_buf[i], values[default_index].value);\n' \
'\n' \
' /* Add remaining values */\n' \
' for (j = 0; j < num_values; j++)\n' \
' {\n' \
' if (j != default_index)\n' \
' {\n' \
' strcat(values_buf[i], "|");\n' \
' strcat(values_buf[i], values[j].value);\n' \
' }\n' \
' }\n' \
' }\n' \
' }\n' \
'\n' \
' variables[option_index].key = key;\n' \
' variables[option_index].value = values_buf[i];\n' \
' option_index++;\n' \
' }\n' \
'\n' \
' /* Set variables */\n' \
' environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, variables);\n' \
' }\n' \
'\n' \
'error:\n' \
' /* Clean up */\n' \
'\n' \
' if (option_v1_defs_us)\n' \
' {\n' \
' free(option_v1_defs_us);\n' \
' option_v1_defs_us = NULL;\n' \
' }\n' \
'\n' \
'#ifndef HAVE_NO_LANGEXTRA\n' \
' if (option_v1_defs_intl)\n' \
' {\n' \
' free(option_v1_defs_intl);\n' \
' option_v1_defs_intl = NULL;\n' \
' }\n' \
'#endif\n' \
'\n' \
' if (values_buf)\n' \
' {\n' \
' for (i = 0; i < num_options; i++)\n' \
' {\n' \
' if (values_buf[i])\n' \
' {\n' \
' free(values_buf[i]);\n' \
' values_buf[i] = NULL;\n' \
' }\n' \
' }\n' \
'\n' \
' free(values_buf);\n' \
' values_buf = NULL;\n' \
' }\n' \
'\n' \
' if (variables)\n' \
' {\n' \
' free(variables);\n' \
' variables = NULL;\n' \
' }\n' \
' }\n' \
'}\n' \
'\n' \
'#ifdef __cplusplus\n' \
'}\n' \
'#endif'
struct_groups = cor.p_struct.finditer(struct_text)
out_text = struct_text
for construct in struct_groups:
repl_text = ''
declaration = construct.group(1)
struct_match = cor.p_type_name.search(declaration)
if struct_match:
if struct_match.group(3):
struct_type_name_lang = struct_match.group(1, 2, 3)
declaration_end = declaration[struct_match.end(1):]
elif struct_match.group(4):
struct_type_name_lang = struct_match.group(1, 2, 4)
declaration_end = declaration[struct_match.end(1):]
else:
struct_type_name_lang = sum((struct_match.group(1, 2), ('_us',)), ())
declaration_end = f'{declaration[struct_match.end(1):struct_match.end(2)]}_us' \
f'{declaration[struct_match.end(2):]}'
else:
return -1
if 'retro_core_option_definition' == struct_type_name_lang[0]:
import shutil
shutil.copy(file_name, file_name + '.v1')
new_declaration = f'\nstruct retro_core_option_v2_category option_cats{struct_type_name_lang[2]}[] = ' \
'{\n { NULL, NULL, NULL },\n' \
'};\n\n' \
+ declaration[:struct_match.start(1)] + \
'retro_core_option_v2_definition' \
+ declaration_end
offset = construct.start(0)
repl_text = repl_text + cor.re.sub(cor.re.escape(declaration), new_declaration,
construct.group(0)[:construct.start(2) - offset])
content = construct.group(2)
new_content = cor.p_option.sub(replace_option, content)
repl_text = repl_text + new_content + cor.re.sub(r'{\s*NULL,\s*NULL,\s*NULL,\s*{\{0}},\s*NULL\s*},\s*};',
'{ NULL, NULL, NULL, NULL, NULL, NULL, {{0}}, NULL },\n};'
'\n\nstruct retro_core_options_v2 options' +
struct_type_name_lang[2] + ' = {\n'
f' option_cats{struct_type_name_lang[2]},\n'
f' option_defs{struct_type_name_lang[2]}\n'
'};',
construct.group(0)[construct.end(2) - offset:])
out_text = cor.re.sub(cor.re.escape(construct.group(0)), repl_text, out_text)
else:
return -2
with open(file_name, 'w', encoding='utf-8') as code_file:
out_text = cor.re.sub(cor.re.escape(comment_v1), comment_v2, out_text)
intl = p_intl.search(out_text)
if intl:
new_intl = out_text[:intl.start(1)] \
+ 'struct retro_core_options_v2 *options_intl[RETRO_LANGUAGE_LAST]' \
+ out_text[intl.end(1):intl.start(2)] \
+ '\n &options_us, /* RETRO_LANGUAGE_ENGLISH */\n' \
' &options_ja, /* RETRO_LANGUAGE_JAPANESE */\n' \
' &options_fr, /* RETRO_LANGUAGE_FRENCH */\n' \
' &options_es, /* RETRO_LANGUAGE_SPANISH */\n' \
' &options_de, /* RETRO_LANGUAGE_GERMAN */\n' \
' &options_it, /* RETRO_LANGUAGE_ITALIAN */\n' \
' &options_nl, /* RETRO_LANGUAGE_DUTCH */\n' \
' &options_pt_br, /* RETRO_LANGUAGE_PORTUGUESE_BRAZIL */\n' \
' &options_pt_pt, /* RETRO_LANGUAGE_PORTUGUESE_PORTUGAL */\n' \
' &options_ru, /* RETRO_LANGUAGE_RUSSIAN */\n' \
' &options_ko, /* RETRO_LANGUAGE_KOREAN */\n' \
' &options_cht, /* RETRO_LANGUAGE_CHINESE_TRADITIONAL */\n' \
' &options_chs, /* RETRO_LANGUAGE_CHINESE_SIMPLIFIED */\n' \
' &options_eo, /* RETRO_LANGUAGE_ESPERANTO */\n' \
' &options_pl, /* RETRO_LANGUAGE_POLISH */\n' \
' &options_vn, /* RETRO_LANGUAGE_VIETNAMESE */\n' \
' &options_ar, /* RETRO_LANGUAGE_ARABIC */\n' \
' &options_el, /* RETRO_LANGUAGE_GREEK */\n' \
' &options_tr, /* RETRO_LANGUAGE_TURKISH */\n' \
' &options_sv, /* RETRO_LANGUAGE_SLOVAK */\n' \
' &options_fa, /* RETRO_LANGUAGE_PERSIAN */\n' \
' &options_he, /* RETRO_LANGUAGE_HEBREW */\n' \
' &options_ast, /* RETRO_LANGUAGE_ASTURIAN */\n' \
' &options_fi, /* RETRO_LANGUAGE_FINNISH */\n' \
+ out_text[intl.end(2):]
out_text = p_set.sub(new_set, new_intl)
else:
out_text = p_set.sub(new_set, out_text)
code_file.write(out_text)
return 1
# -------------------- MAIN -------------------- #
if __name__ == '__main__':
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
if os.path.basename(DIR_PATH) != "intl":
raise RuntimeError("Script is not in intl folder!")
BASE_PATH = os.path.dirname(DIR_PATH)
CORE_OP_FILE = os.path.join(BASE_PATH, "**", "libretro_core_options.h")
core_options_hits = glob.glob(CORE_OP_FILE, recursive=True)
if len(core_options_hits) == 0:
raise RuntimeError("libretro_core_options.h not found!")
elif len(core_options_hits) > 1:
print("More than one libretro_core_options.h file found:\n\n")
for i, file in enumerate(core_options_hits):
print(f"{i} {file}\n")
while True:
user_choice = input("Please choose one ('q' will exit): ")
if user_choice == 'q':
exit(0)
elif user_choice.isdigit():
core_op_file = core_options_hits[int(user_choice)]
break
else:
print("Please make a valid choice!\n\n")
else:
core_op_file = core_options_hits[0]
H_FILE_PATH = core_op_file
INTL_FILE_PATH = core_op_file.replace("libretro_core_options.h", 'libretro_core_options_intl.h')
for file in (H_FILE_PATH, INTL_FILE_PATH):
if os.path.isfile(file):
with open(file, 'r+', encoding='utf-8') as h_file:
text = h_file.read()
try:
test = create_v2_code_file(text, file)
except Exception as e:
print(e)
test = -1
if -1 > test:
print('Your file looks like it already is v2? (' + file + ')')
continue
if 0 > test:
print('An error occured! Please make sure to use the complete v1 struct! (' + file + ')')
continue
else:
print(file + ' not found.')

File diff suppressed because it is too large Load Diff

View File

@ -65,6 +65,7 @@ static unsigned blargg_ntsc;
static bool fds_auto_insert; static bool fds_auto_insert;
static bool overscan_v; static bool overscan_v;
static bool overscan_h; static bool overscan_h;
static bool libretro_supports_option_categories = false;
static unsigned aspect_ratio_mode; static unsigned aspect_ratio_mode;
static unsigned tpulse; static unsigned tpulse;
@ -433,7 +434,8 @@ void retro_get_system_av_info(struct retro_system_av_info *info)
void retro_set_environment(retro_environment_t cb) void retro_set_environment(retro_environment_t cb)
{ {
environ_cb = cb; environ_cb = cb;
libretro_set_core_options(environ_cb); libretro_set_core_options(environ_cb,
&libretro_supports_option_categories);
static const struct retro_controller_description port1[] = { static const struct retro_controller_description port1[] = {
{ "Auto", RETRO_DEVICE_AUTO }, { "Auto", RETRO_DEVICE_AUTO },
@ -776,6 +778,7 @@ static void check_variables(void)
static bool last_ntsc_val_same; static bool last_ntsc_val_same;
struct retro_variable var = {0}; struct retro_variable var = {0};
struct retro_system_av_info av_info; struct retro_system_av_info av_info;
struct retro_core_option_display option_display;
Api::Sound sound(emulator); Api::Sound sound(emulator);
Api::Video video(emulator); Api::Video video(emulator);
@ -1193,7 +1196,11 @@ static void check_variables(void)
{ {
sound.SetVolume(Api::Sound::CHANNEL_S5B, atoi(var.value)); sound.SetVolume(Api::Sound::CHANNEL_S5B, atoi(var.value));
} }
/* "Show settings" are not required if categories are supported */
option_display.visible = !libretro_supports_option_categories;
option_display.key = "nestopia_show_advanced_av_settings";
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
var.key = "nestopia_show_advanced_av_settings"; var.key = "nestopia_show_advanced_av_settings";
@ -1209,7 +1216,6 @@ static void check_variables(void)
if (show_advanced_av_settings != show_advanced_av_settings_prev) if (show_advanced_av_settings != show_advanced_av_settings_prev)
{ {
size_t i; size_t i;
struct retro_core_option_display option_display;
char av_keys[11][40] = { char av_keys[11][40] = {
"nestopia_audio_vol_sq1", "nestopia_audio_vol_sq1",
"nestopia_audio_vol_sq2", "nestopia_audio_vol_sq2",
@ -1224,7 +1230,8 @@ static void check_variables(void)
"nestopia_audio_vol_s5b" "nestopia_audio_vol_s5b"
}; };
option_display.visible = show_advanced_av_settings; option_display.visible = show_advanced_av_settings
|| libretro_supports_option_categories;
for (i = 0; i < 11; i++) for (i = 0; i < 11; i++)
{ {

View File

@ -13,9 +13,10 @@
/* /*
******************************** ********************************
* VERSION: 1.3 * VERSION: 2.0
******************************** ********************************
* *
* - 2.0: Add support for core options v2 interface
* - 1.3: Move translations to libretro_core_options_intl.h * - 1.3: Move translations to libretro_core_options_intl.h
* - libretro_core_options_intl.h includes BOM and utf-8 * - libretro_core_options_intl.h includes BOM and utf-8
* fix for MSVC 2010-2013 * fix for MSVC 2010-2013
@ -48,11 +49,39 @@ extern "C" {
* - Will be used as a fallback for any missing entries in * - Will be used as a fallback for any missing entries in
* frontend language definition */ * frontend language definition */
struct retro_core_option_definition option_defs_us[] = {
struct retro_core_option_v2_category option_cats_us[] = {
{
"video",
"Video",
"Configure aspect ratio, display cropping, color palette and video filter options."
},
{
"audio",
"Audio",
"Configure audio effects and channel volumes."
},
{
"input",
"Input",
"Configure input devices and other input related parameters."
},
{
"hacks",
"Emulation Hacks",
"Configure processor overclocking and emulation accuracy parameters affecting low-level performance and compatibility."
},
{ NULL, NULL, NULL },
};
struct retro_core_option_v2_definition option_defs_us[] = {
{ {
"nestopia_blargg_ntsc_filter", "nestopia_blargg_ntsc_filter",
"Blargg NTSC Filter", "Blargg NTSC Filter",
NULL,
"Enable Blargg NTSC filters.", "Enable Blargg NTSC filters.",
NULL,
"video",
{ {
{ "disabled", NULL }, { "disabled", NULL },
{ "composite", "Composite Video" }, { "composite", "Composite Video" },
@ -63,10 +92,13 @@ struct retro_core_option_definition option_defs_us[] = {
}, },
"disabled" "disabled"
}, },
{ {/* TODO explain the custom option */
"nestopia_palette", "nestopia_palette",
"Palette", "Palette",
"Choose which color palette is going to be used.", NULL,
"Color palette to be used.",
NULL,
"video",
{ {
{ "cxa2025as", "CXA2025AS" }, { "cxa2025as", "CXA2025AS" },
{ "consumer", "Consumer" }, { "consumer", "Consumer" },
@ -87,7 +119,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_nospritelimit", "nestopia_nospritelimit",
"Remove Sprite Limit", "Remove Sprite Limit",
NULL,
"Remove 8-sprites-per-scanline hardware limit.", "Remove 8-sprites-per-scanline hardware limit.",
NULL,
"hacks",
{ {
{ "disabled", NULL }, { "disabled", NULL },
{ "enabled", NULL }, { "enabled", NULL },
@ -98,7 +133,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_overclock", "nestopia_overclock",
"CPU Speed (Overclock)", "CPU Speed (Overclock)",
NULL,
"Overclock the emulated CPU.", "Overclock the emulated CPU.",
NULL,
"hacks",
{ {
{ "1x", NULL }, { "1x", NULL },
{ "2x", NULL }, { "2x", NULL },
@ -109,7 +147,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_select_adapter", "nestopia_select_adapter",
"4 Player Adapter", "4 Player Adapter",
NULL,
"Manually select a 4 Player Adapter if needed. Some games will not recognize the adapter correctly through the NstDatabase.xml database, this option should help fix that.", "Manually select a 4 Player Adapter if needed. Some games will not recognize the adapter correctly through the NstDatabase.xml database, this option should help fix that.",
NULL,
"input",
{ {
{ "auto", "Auto" }, { "auto", "Auto" },
{ "ntsc", "NTSC" }, { "ntsc", "NTSC" },
@ -121,7 +162,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_fds_auto_insert", "nestopia_fds_auto_insert",
"FDS Auto Insert", "FDS Auto Insert",
NULL,
"Automatically insert first FDS disk on reset.", "Automatically insert first FDS disk on reset.",
NULL,
NULL,
{ {
{ "disabled", NULL }, { "disabled", NULL },
{ "enabled", NULL }, { "enabled", NULL },
@ -132,10 +176,13 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_overscan_v", "nestopia_overscan_v",
"Mask Overscan (Vertical)", "Mask Overscan (Vertical)",
NULL,
"Mask out (vertically) the potentially random glitchy video output that would have been hidden by the bezel around the edge of a standard-definition television screen.", "Mask out (vertically) the potentially random glitchy video output that would have been hidden by the bezel around the edge of a standard-definition television screen.",
NULL,
"video",
{ {
{ "enabled", NULL },
{ "disabled", NULL }, { "disabled", NULL },
{ "enabled", NULL },
{ NULL, NULL }, { NULL, NULL },
}, },
"enabled" "enabled"
@ -143,10 +190,13 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_overscan_h", "nestopia_overscan_h",
"Mask Overscan (Horizontal)", "Mask Overscan (Horizontal)",
NULL,
"Mask out (horizontally) the potentially random glitchy video output that would have been hidden by the bezel around the edge of a standard-definition television screen.", "Mask out (horizontally) the potentially random glitchy video output that would have been hidden by the bezel around the edge of a standard-definition television screen.",
NULL,
"video",
{ {
{ "enabled", NULL },
{ "disabled", NULL }, { "disabled", NULL },
{ "enabled", NULL },
{ NULL, NULL }, { NULL, NULL },
}, },
"disabled" "disabled"
@ -154,7 +204,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_aspect", "nestopia_aspect",
"Preferred Aspect Ratio", "Preferred Aspect Ratio",
"Choose the preferred aspect ratio. RetroArch's aspect ratio must be set to Core provided in the Video settings. 'auto' will use the NstDatabase.xml database file for aspect ratio autodetection. If there is no database present it will default to NTSC for 'auto'.", NULL,
"RetroArch's aspect ratio must be set to Core, provided in the Video settings. 'Auto' will use the NstDatabase.xml database file for aspect ratio autodetection. If there is no database present, it will default to NTSC.",
NULL,
"video",
{ {
{ "auto", "Auto" }, { "auto", "Auto" },
{ "ntsc", "NTSC" }, { "ntsc", "NTSC" },
@ -167,7 +220,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_genie_distortion", "nestopia_genie_distortion",
"Game Genie Sound Distortion", "Game Genie Sound Distortion",
NULL,
"The Game Genie cheat device could inadvertently introduce sound distortion in games. By enabling this, you can simulate the distortion it would add to a game's sound.", "The Game Genie cheat device could inadvertently introduce sound distortion in games. By enabling this, you can simulate the distortion it would add to a game's sound.",
NULL,
"audio",
{ {
{ "disabled", NULL }, { "disabled", NULL },
{ "enabled", NULL }, { "enabled", NULL },
@ -178,7 +234,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_favored_system", "nestopia_favored_system",
"System Region", "System Region",
"Choose which region the system is from. 'Auto' will use the NstDatabase.xml database file for region autodetection. If there is no database present it will default to NTSC for 'Auto'.", NULL,
"'Auto' will use the NstDatabase.xml database file for region autodetection. If there is no database present, it will default to NTSC.",
NULL,
NULL,
{ {
{ "auto", "Auto" }, { "auto", "Auto" },
{ "ntsc", "NTSC" }, { "ntsc", "NTSC" },
@ -192,7 +251,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_ram_power_state", "nestopia_ram_power_state",
"RAM Power-on State", "RAM Power-on State",
"", NULL,
"RAM values on power up. Some games rely on initial RAM values for random number generation as an example.",
NULL,
"hacks",
{ {
{ "0x00", NULL }, { "0x00", NULL },
{ "0xFF", NULL }, { "0xFF", NULL },
@ -204,7 +266,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_button_shift", "nestopia_button_shift",
"Shift Buttons Clockwise", "Shift Buttons Clockwise",
NULL,
"Rotate A/B/X/Y button clockwise.", /* is this right? @gadsby */ "Rotate A/B/X/Y button clockwise.", /* is this right? @gadsby */
NULL,
"input",
{ {
{ "disabled", NULL }, { "disabled", NULL },
{ "enabled", NULL }, { "enabled", NULL },
@ -215,21 +280,27 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_arkanoid_device", "nestopia_arkanoid_device",
"Arkanoid device", "Arkanoid device",
NULL,
"Select the device you wish to use for the Arkanoid paddle.", "Select the device you wish to use for the Arkanoid paddle.",
NULL,
"input",
{ {
{ "mouse", NULL }, { "mouse", "Mouse" },
{ "pointer", NULL }, { "pointer", "Pointer" },
}, },
"mouse" "mouse"
}, },
{ {
"nestopia_zapper_device", "nestopia_zapper_device",
"Zapper device", "Zapper device",
NULL,
"Select the device you wish to use for the Zapper.", "Select the device you wish to use for the Zapper.",
NULL,
"input",
{ {
{ "lightgun", NULL }, { "lightgun", "Light gun" },
{ "mouse", NULL }, { "mouse", "Mouse" },
{ "pointer", NULL }, { "pointer", "Pointer" },
{ NULL, NULL }, { NULL, NULL },
}, },
"lightgun", "lightgun",
@ -237,17 +308,23 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_show_crosshair", "nestopia_show_crosshair",
"Show Crosshair", "Show Crosshair",
NULL,
"Set whether to show the crosshair when the Zapper is used.", "Set whether to show the crosshair when the Zapper is used.",
NULL,
"input",
{ {
{ "enabled", NULL },
{ "disabled", NULL}, { "disabled", NULL},
{ "enabled", NULL },
}, },
"enabled" "enabled"
}, },
{ {
"nestopia_turbo_pulse", "nestopia_turbo_pulse",
"Turbo Pulse Speed", "Turbo Pulse Speed",
NULL,
"Set the turbo pulse speed for the Turbo B and Turbo A buttons.", "Set the turbo pulse speed for the Turbo B and Turbo A buttons.",
NULL,
"input",
{ {
{ "2", NULL }, { "2", NULL },
{ "3", NULL }, { "3", NULL },
@ -264,10 +341,13 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_show_advanced_av_settings", "nestopia_show_advanced_av_settings",
"Show Advanced Audio Settings (Reopen menu)", "Show Advanced Audio Settings (Reopen menu)",
NULL,
"Enable configuration of low-level audio channel parameters.", "Enable configuration of low-level audio channel parameters.",
NULL,
NULL,
{ {
{ "enabled", NULL },
{ "disabled", NULL }, { "disabled", NULL },
{ "enabled", NULL },
{ NULL, NULL}, { NULL, NULL},
}, },
"disabled" "disabled"
@ -275,7 +355,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_audio_vol_sq1", "nestopia_audio_vol_sq1",
"Square 1 Channel Volume %", "Square 1 Channel Volume %",
NULL,
"Modify Square 1 Channel Volume %.", "Modify Square 1 Channel Volume %.",
NULL,
"audio",
{ {
{ "0", NULL }, { "0", NULL },
{ "10", NULL }, { "10", NULL },
@ -297,7 +380,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_audio_vol_sq2", "nestopia_audio_vol_sq2",
"Square 2 Channel Volume %", "Square 2 Channel Volume %",
NULL,
"Modify Square 2 Channel Volume %.", "Modify Square 2 Channel Volume %.",
NULL,
"audio",
{ {
{ "0", NULL }, { "0", NULL },
{ "10", NULL }, { "10", NULL },
@ -319,7 +405,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_audio_vol_tri", "nestopia_audio_vol_tri",
"Triangle Channel Volume %", "Triangle Channel Volume %",
NULL,
"Modify Triangle Channel Volume %.", "Modify Triangle Channel Volume %.",
NULL,
"audio",
{ {
{ "0", NULL }, { "0", NULL },
{ "10", NULL }, { "10", NULL },
@ -341,7 +430,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_audio_vol_noise", "nestopia_audio_vol_noise",
"Noise Channel Volume %", "Noise Channel Volume %",
NULL,
"Modify Noise Channel Volume %.", "Modify Noise Channel Volume %.",
NULL,
"audio",
{ {
{ "0", NULL }, { "0", NULL },
{ "10", NULL }, { "10", NULL },
@ -363,7 +455,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_audio_vol_dpcm", "nestopia_audio_vol_dpcm",
"DPCM Channel Volume %", "DPCM Channel Volume %",
NULL,
"Modify DPCM Channel Volume %.", "Modify DPCM Channel Volume %.",
NULL,
"audio",
{ {
{ "0", NULL }, { "0", NULL },
{ "10", NULL }, { "10", NULL },
@ -385,7 +480,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_audio_vol_fds", "nestopia_audio_vol_fds",
"FDS Channel Volume %", "FDS Channel Volume %",
NULL,
"Modify FDS Channel Volume %.", "Modify FDS Channel Volume %.",
NULL,
"audio",
{ {
{ "0", NULL }, { "0", NULL },
{ "10", NULL }, { "10", NULL },
@ -407,7 +505,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_audio_vol_mmc5", "nestopia_audio_vol_mmc5",
"MMC5 Channel Volume %", "MMC5 Channel Volume %",
NULL,
"Modify MMC5 Channel Volume %.", "Modify MMC5 Channel Volume %.",
NULL,
"audio",
{ {
{ "0", NULL }, { "0", NULL },
{ "10", NULL }, { "10", NULL },
@ -429,7 +530,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_audio_vol_vrc6", "nestopia_audio_vol_vrc6",
"VRC6 Channel Volume %", "VRC6 Channel Volume %",
NULL,
"Modify VRC6 Channel Volume %.", "Modify VRC6 Channel Volume %.",
NULL,
"audio",
{ {
{ "0", NULL }, { "0", NULL },
{ "10", NULL }, { "10", NULL },
@ -451,7 +555,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_audio_vol_vrc7", "nestopia_audio_vol_vrc7",
"VRC7 Channel Volume %", "VRC7 Channel Volume %",
NULL,
"Modify VRC7 Channel Volume %.", "Modify VRC7 Channel Volume %.",
NULL,
"audio",
{ {
{ "0", NULL }, { "0", NULL },
{ "10", NULL }, { "10", NULL },
@ -473,7 +580,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_audio_vol_n163", "nestopia_audio_vol_n163",
"N163 Channel Volume %", "N163 Channel Volume %",
NULL,
"Modify N163 Channel Volume %.", "Modify N163 Channel Volume %.",
NULL,
"audio",
{ {
{ "0", NULL }, { "0", NULL },
{ "10", NULL }, { "10", NULL },
@ -495,7 +605,10 @@ struct retro_core_option_definition option_defs_us[] = {
{ {
"nestopia_audio_vol_s5b", "nestopia_audio_vol_s5b",
"S5B Channel Volume %", "S5B Channel Volume %",
NULL,
"Modify N163 Channel Volume %.", "Modify N163 Channel Volume %.",
NULL,
"audio",
{ {
{ "0", NULL }, { "0", NULL },
{ "10", NULL }, { "10", NULL },
@ -514,7 +627,12 @@ struct retro_core_option_definition option_defs_us[] = {
}, },
"100" "100"
}, },
{ NULL, NULL, NULL, {{0}}, NULL }, { NULL, NULL, NULL, NULL, NULL, NULL, {{0}}, NULL },
};
struct retro_core_options_v2 options_us = {
option_cats_us,
option_defs_us
}; };
/* /*
@ -524,26 +642,31 @@ struct retro_core_option_definition option_defs_us[] = {
*/ */
#ifndef HAVE_NO_LANGEXTRA #ifndef HAVE_NO_LANGEXTRA
struct retro_core_option_definition *option_defs_intl[RETRO_LANGUAGE_LAST] = { struct retro_core_options_v2 *options_intl[RETRO_LANGUAGE_LAST] = {
option_defs_us, /* RETRO_LANGUAGE_ENGLISH */ &options_us, /* RETRO_LANGUAGE_ENGLISH */
NULL, /* RETRO_LANGUAGE_JAPANESE */ &options_ja, /* RETRO_LANGUAGE_JAPANESE */
NULL, /* RETRO_LANGUAGE_FRENCH */ &options_fr, /* RETRO_LANGUAGE_FRENCH */
NULL, /* RETRO_LANGUAGE_SPANISH */ &options_es, /* RETRO_LANGUAGE_SPANISH */
NULL, /* RETRO_LANGUAGE_GERMAN */ &options_de, /* RETRO_LANGUAGE_GERMAN */
NULL, /* RETRO_LANGUAGE_ITALIAN */ &options_it, /* RETRO_LANGUAGE_ITALIAN */
NULL, /* RETRO_LANGUAGE_DUTCH */ &options_nl, /* RETRO_LANGUAGE_DUTCH */
NULL, /* RETRO_LANGUAGE_PORTUGUESE_BRAZIL */ &options_pt_br, /* RETRO_LANGUAGE_PORTUGUESE_BRAZIL */
NULL, /* RETRO_LANGUAGE_PORTUGUESE_PORTUGAL */ &options_pt_pt, /* RETRO_LANGUAGE_PORTUGUESE_PORTUGAL */
NULL, /* RETRO_LANGUAGE_RUSSIAN */ &options_ru, /* RETRO_LANGUAGE_RUSSIAN */
NULL, /* RETRO_LANGUAGE_KOREAN */ &options_ko, /* RETRO_LANGUAGE_KOREAN */
NULL, /* RETRO_LANGUAGE_CHINESE_TRADITIONAL */ &options_cht, /* RETRO_LANGUAGE_CHINESE_TRADITIONAL */
NULL, /* RETRO_LANGUAGE_CHINESE_SIMPLIFIED */ &options_chs, /* RETRO_LANGUAGE_CHINESE_SIMPLIFIED */
NULL, /* RETRO_LANGUAGE_ESPERANTO */ &options_eo, /* RETRO_LANGUAGE_ESPERANTO */
NULL, /* RETRO_LANGUAGE_POLISH */ &options_pl, /* RETRO_LANGUAGE_POLISH */
NULL, /* RETRO_LANGUAGE_VIETNAMESE */ &options_vn, /* RETRO_LANGUAGE_VIETNAMESE */
NULL, /* RETRO_LANGUAGE_ARABIC */ &options_ar, /* RETRO_LANGUAGE_ARABIC */
NULL, /* RETRO_LANGUAGE_GREEK */ &options_el, /* RETRO_LANGUAGE_GREEK */
option_defs_tr, /* RETRO_LANGUAGE_TURKISH */ &options_tr, /* RETRO_LANGUAGE_TURKISH */
&options_sv, /* RETRO_LANGUAGE_SLOVAK */
&options_fa, /* RETRO_LANGUAGE_PERSIAN */
&options_he, /* RETRO_LANGUAGE_HEBREW */
&options_ast, /* RETRO_LANGUAGE_ASTURIAN */
&options_fi, /* RETRO_LANGUAGE_FINNISH */
}; };
#endif #endif
@ -561,39 +684,61 @@ struct retro_core_option_definition *option_defs_intl[RETRO_LANGUAGE_LAST] = {
* be as painless as possible for core devs) * be as painless as possible for core devs)
*/ */
static INLINE void libretro_set_core_options(retro_environment_t environ_cb) static INLINE void libretro_set_core_options(retro_environment_t environ_cb,
bool *categories_supported)
{ {
unsigned version = 0; unsigned version = 0;
#ifndef HAVE_NO_LANGEXTRA
unsigned language = 0;
#endif
if (!environ_cb) if (!environ_cb || !categories_supported)
return; return;
if (environ_cb(RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION, &version) && (version >= 1)) *categories_supported = false;
if (!environ_cb(RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION, &version))
version = 0;
if (version >= 2)
{ {
#ifndef HAVE_NO_LANGEXTRA #ifndef HAVE_NO_LANGEXTRA
struct retro_core_options_intl core_options_intl; struct retro_core_options_v2_intl core_options_intl;
unsigned language = 0;
core_options_intl.us = option_defs_us; core_options_intl.us = &options_us;
core_options_intl.local = NULL; core_options_intl.local = NULL;
if (environ_cb(RETRO_ENVIRONMENT_GET_LANGUAGE, &language) && if (environ_cb(RETRO_ENVIRONMENT_GET_LANGUAGE, &language) &&
(language < RETRO_LANGUAGE_LAST) && (language != RETRO_LANGUAGE_ENGLISH)) (language < RETRO_LANGUAGE_LAST) && (language != RETRO_LANGUAGE_ENGLISH))
core_options_intl.local = option_defs_intl[language]; core_options_intl.local = options_intl[language];
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL, &core_options_intl); *categories_supported = environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL,
&core_options_intl);
#else #else
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS, &option_defs_us); *categories_supported = environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2,
&options_us);
#endif #endif
} }
else else
{ {
size_t i; size_t i, j;
size_t option_index = 0;
size_t num_options = 0; size_t num_options = 0;
struct retro_core_option_definition
*option_v1_defs_us = NULL;
#ifndef HAVE_NO_LANGEXTRA
size_t num_options_intl = 0;
struct retro_core_option_v2_definition
*option_defs_intl = NULL;
struct retro_core_option_definition
*option_v1_defs_intl = NULL;
struct retro_core_options_intl
core_options_v1_intl;
#endif
struct retro_variable *variables = NULL; struct retro_variable *variables = NULL;
char **values_buf = NULL; char **values_buf = NULL;
/* Determine number of options */ /* Determine total number of options */
while (true) while (true)
{ {
if (option_defs_us[num_options].key) if (option_defs_us[num_options].key)
@ -602,86 +747,187 @@ static INLINE void libretro_set_core_options(retro_environment_t environ_cb)
break; break;
} }
/* Allocate arrays */ if (version >= 1)
variables = (struct retro_variable *)calloc(num_options + 1, sizeof(struct retro_variable));
values_buf = (char **)calloc(num_options, sizeof(char *));
if (!variables || !values_buf)
goto error;
/* Copy parameters from option_defs_us array */
for (i = 0; i < num_options; i++)
{ {
const char *key = option_defs_us[i].key; /* Allocate US array */
const char *desc = option_defs_us[i].desc; option_v1_defs_us = (struct retro_core_option_definition *)
const char *default_value = option_defs_us[i].default_value; calloc(num_options + 1, sizeof(struct retro_core_option_definition));
struct retro_core_option_value *values = option_defs_us[i].values;
size_t buf_len = 3;
size_t default_index = 0;
values_buf[i] = NULL; /* Copy parameters from option_defs_us array */
for (i = 0; i < num_options; i++)
if (desc)
{ {
size_t num_values = 0; struct retro_core_option_v2_definition *option_def_us = &option_defs_us[i];
struct retro_core_option_value *option_values = option_def_us->values;
struct retro_core_option_definition *option_v1_def_us = &option_v1_defs_us[i];
struct retro_core_option_value *option_v1_values = option_v1_def_us->values;
/* Determine number of values */ option_v1_def_us->key = option_def_us->key;
option_v1_def_us->desc = option_def_us->desc;
option_v1_def_us->info = option_def_us->info;
option_v1_def_us->default_value = option_def_us->default_value;
/* Values must be copied individually... */
while (option_values->value)
{
option_v1_values->value = option_values->value;
option_v1_values->label = option_values->label;
option_values++;
option_v1_values++;
}
}
#ifndef HAVE_NO_LANGEXTRA
if (environ_cb(RETRO_ENVIRONMENT_GET_LANGUAGE, &language) &&
(language < RETRO_LANGUAGE_LAST) && (language != RETRO_LANGUAGE_ENGLISH) &&
options_intl[language])
option_defs_intl = options_intl[language]->definitions;
if (option_defs_intl)
{
/* Determine number of intl options */
while (true) while (true)
{ {
if (values[num_values].value) if (option_defs_intl[num_options_intl].key)
{ num_options_intl++;
/* Check if this is the default value */
if (default_value)
if (strcmp(values[num_values].value, default_value) == 0)
default_index = num_values;
buf_len += strlen(values[num_values].value);
num_values++;
}
else else
break; break;
} }
/* Build values string */ /* Allocate intl array */
if (num_values > 0) option_v1_defs_intl = (struct retro_core_option_definition *)
calloc(num_options_intl + 1, sizeof(struct retro_core_option_definition));
/* Copy parameters from option_defs_intl array */
for (i = 0; i < num_options_intl; i++)
{ {
size_t j; struct retro_core_option_v2_definition *option_def_intl = &option_defs_intl[i];
struct retro_core_option_value *option_values = option_def_intl->values;
struct retro_core_option_definition *option_v1_def_intl = &option_v1_defs_intl[i];
struct retro_core_option_value *option_v1_values = option_v1_def_intl->values;
buf_len += num_values - 1; option_v1_def_intl->key = option_def_intl->key;
buf_len += strlen(desc); option_v1_def_intl->desc = option_def_intl->desc;
option_v1_def_intl->info = option_def_intl->info;
option_v1_def_intl->default_value = option_def_intl->default_value;
values_buf[i] = (char *)calloc(buf_len, sizeof(char)); /* Values must be copied individually... */
if (!values_buf[i]) while (option_values->value)
goto error;
strcpy(values_buf[i], desc);
strcat(values_buf[i], "; ");
/* Default value goes first */
strcat(values_buf[i], values[default_index].value);
/* Add remaining values */
for (j = 0; j < num_values; j++)
{ {
if (j != default_index) option_v1_values->value = option_values->value;
{ option_v1_values->label = option_values->label;
strcat(values_buf[i], "|");
strcat(values_buf[i], values[j].value); option_values++;
} option_v1_values++;
} }
} }
} }
variables[i].key = key; core_options_v1_intl.us = option_v1_defs_us;
variables[i].value = values_buf[i]; core_options_v1_intl.local = option_v1_defs_intl;
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL, &core_options_v1_intl);
#else
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS, option_v1_defs_us);
#endif
}
else
{
/* Allocate arrays */
variables = (struct retro_variable *)calloc(num_options + 1,
sizeof(struct retro_variable));
values_buf = (char **)calloc(num_options, sizeof(char *));
if (!variables || !values_buf)
goto error;
/* Copy parameters from option_defs_us array */
for (i = 0; i < num_options; i++)
{
const char *key = option_defs_us[i].key;
const char *desc = option_defs_us[i].desc;
const char *default_value = option_defs_us[i].default_value;
struct retro_core_option_value *values = option_defs_us[i].values;
size_t buf_len = 3;
size_t default_index = 0;
values_buf[i] = NULL;
if (desc)
{
size_t num_values = 0;
/* Determine number of values */
while (true)
{
if (values[num_values].value)
{
/* Check if this is the default value */
if (default_value)
if (strcmp(values[num_values].value, default_value) == 0)
default_index = num_values;
buf_len += strlen(values[num_values].value);
num_values++;
}
else
break;
}
/* Build values string */
if (num_values > 0)
{
buf_len += num_values - 1;
buf_len += strlen(desc);
values_buf[i] = (char *)calloc(buf_len, sizeof(char));
if (!values_buf[i])
goto error;
strcpy(values_buf[i], desc);
strcat(values_buf[i], "; ");
/* Default value goes first */
strcat(values_buf[i], values[default_index].value);
/* Add remaining values */
for (j = 0; j < num_values; j++)
{
if (j != default_index)
{
strcat(values_buf[i], "|");
strcat(values_buf[i], values[j].value);
}
}
}
}
variables[option_index].key = key;
variables[option_index].value = values_buf[i];
option_index++;
}
/* Set variables */
environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, variables);
} }
/* Set variables */
environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, variables);
error: error:
/* Clean up */ /* Clean up */
if (option_v1_defs_us)
{
free(option_v1_defs_us);
option_v1_defs_us = NULL;
}
#ifndef HAVE_NO_LANGEXTRA
if (option_v1_defs_intl)
{
free(option_v1_defs_intl);
option_v1_defs_intl = NULL;
}
#endif
if (values_buf) if (values_buf)
{ {
for (i = 0; i < num_options; i++) for (i = 0; i < num_options; i++)

File diff suppressed because it is too large Load Diff