update_gi.sh 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. #!/usr/bin/env bash
  2. # Checker: shellcheck --shell=bash --exclude=SC2006,SC3043 update_gi.sh | less
  3. # Exit on error.
  4. set -e
  5. fatal() {
  6. echo
  7. for arg in "$@"; do
  8. echo " * $arg" >&2
  9. done
  10. echo
  11. exit 1
  12. }
  13. # ======== Global constants
  14. DEBUG=${DEBUG:-0}
  15. # placeholder 1: '%s' : filename
  16. # placeholder 2: '%s' : additional GET fields
  17. declare -A UPDATE_URL_MAP
  18. UPDATE_URL_MAP[OS]='https://sg-hyp-api.hoyoverse.com/hyp/hyp-connect/api/%s?game_ids[]=gopR6Cufr3&launcher_id=VYTpXlbWo8%s'
  19. # Obtained from studiobuttermedia/anime_api @ GitHub
  20. UPDATE_URL_MAP[CN]='https://hyp-api.mihoyo.com/hyp/hyp-connect/api/%s?game_ids[]=1Z8W5NHUQb&launcher_id=jGHBHlcOq1%s'
  21. UPDATE_URL_MAP[BB]='https://hyp-api.mihoyo.com/hyp/hyp-connect/api/%s?game_ids[]=T2S0Gz4Dr2&launcher_id=umfgRO5gh5%s'
  22. [ "$DEBUG" -eq 1 ] && UPDATE_URL_MAP[OS]='http://127.0.0.1:8000/%s.json?%s'
  23. CONFIG_FILE='config.ini'
  24. ANCHOR_FILE='pkg_version'
  25. # ======== Voice pack constants
  26. declare -A LANG_PACKS_PATH_MAP LANG_MAP
  27. LANG_PACKS_PATH_MAP[OS]='GenshinImpact_Data/StreamingAssets/AudioAssets'
  28. LANG_PACKS_PATH_MAP[CN]='YuanShen_Data/StreamingAssets/AudioAssets'
  29. LANG_PACKS_PATH_MAP[BB]=${LANG_PACKS_PATH_MAP[CN]}
  30. LANG_MAP[ENGLISHUS]='en-us'
  31. LANG_MAP[JAPANESE]='ja-jp'
  32. LANG_MAP[KOREAN]='ko-kr'
  33. LANG_MAP[CHINESE]='zh-cn'
  34. # ======== Evaluated variables
  35. THIS_FILE=`basename "$0"`
  36. THIS_PATH=`realpath "$(dirname "$0")"`
  37. REPO_PATH=`dirname "$THIS_PATH"` # parent
  38. reltype="" # OS, CN or BB. filled later.
  39. remove_archives=1 # 0 or 1. filled later.
  40. # ======== Dependency checks
  41. # Check is all required tools installed.
  42. for appname in jq bash 7za xdelta3; do
  43. exepath=`command -v "$appname" | tee`
  44. [ ! -e "$exepath" ] && fatal \
  45. "Required tool not found!" \
  46. "Please install: ${appname}."
  47. done
  48. # ======== Download tool setup
  49. DOWNLOAD_PATH="../_update_gi_download"
  50. #DOWNLOAD_PATH="../download with spaces"
  51. declare -A DL_APP_ARGS_MAP
  52. DL_APP_ARGS_MAP[axel]="-n 15"
  53. DL_APP_ARGS_MAP[aria2c]="--no-conf -c"
  54. DL_APP_ARGS_MAP[fetch]="--force-restart --no-mtime --retry --keep-output --restart"
  55. DL_APP_ARGS_MAP[wget]="-c"
  56. DL_APP_ARGS_MAP[curl]="--disable -O -C -"
  57. # Find first available download tool.
  58. for appname in axel aria2c fetch wget curl; do
  59. # Pipe to "tee" overwrites the exit status
  60. exepath=`command -v "$appname" | tee`
  61. if [ -e "$exepath" ]; then
  62. DL_APP_BIN="$exepath"
  63. read -ra DL_APP_ARGS <<< "${DL_APP_ARGS_MAP[$appname]}"
  64. break
  65. fi
  66. done
  67. [ ! -e "$DL_APP_BIN" ] && fatal \
  68. "No downloader application found." \
  69. "Please install one of: ${DL_APPS_LIST}."
  70. echo "--- Using download application: ${DL_APP_BIN}" "${DL_APP_ARGS[@]}"
  71. # ======== Functions
  72. # MacOS and *BSD do not have md5sum: use md5 instead
  73. exepath=$(command -v md5 | tee)
  74. if [ -e "$exepath" ]; then
  75. md5check() {
  76. # 1 = Checksum, 2 = File
  77. md5 -q -c "$1" "$2" >/dev/null 2>&1
  78. }
  79. else
  80. md5check() {
  81. local input filesum
  82. # 1 = Checksum, 2 = File
  83. input=`<<< "$1" tr '[:upper:]' '[:lower:]'`
  84. filesum=`md5sum "$2" | cut -d ' ' -f1`
  85. if [ "$input" != "$filesum" ]; then
  86. echo "Mismatch!"
  87. exit 1
  88. fi
  89. }
  90. fi
  91. download_file() {
  92. local url="$1"
  93. local dst_path="$2"
  94. local md5="$3"
  95. local filename_args="${dst_path}/${url##*/}"
  96. local filename="${filename_args%%\?*}"
  97. local filename_completed="${filename}.completed"
  98. mkdir -p "$dst_path"
  99. if [ -f "$filename_completed" ]; then
  100. echo " -> Skipping: Already downloaded."
  101. else
  102. echo
  103. (cd "$dst_path" && "$DL_APP_BIN" "${DL_APP_ARGS[@]}" "$url")
  104. if [ -n "$md5" ]; then
  105. echo -n " -> Verifying MD5 checksum ... "
  106. md5check "$md5" "$filename"
  107. echo 'OK'
  108. touch "$filename_completed"
  109. fi
  110. fi
  111. }
  112. # Approximate size of the installed archive
  113. download_json_size() {
  114. local size
  115. # "$1" might be an array, resulting in multiple outputs, thus sum.
  116. size=`<<< "$1" jq -r -M ".size" | jq -s 'add'`
  117. echo "$(((size + 1048576) / 1024 / 1024)) MiB"
  118. }
  119. # 1: { "url": "URL", "md5": "hash" }
  120. download_json_section() {
  121. local json_text
  122. local url md5
  123. json_text="$1"
  124. url=`<<< "$json_text" jq -r -M ".url"`
  125. md5=`<<< "$json_text" jq -r -M ".md5"`
  126. download_file "$url" "$DOWNLOAD_PATH" "$md5"
  127. }
  128. get_ini_value() {
  129. local variable
  130. variable="$1"
  131. grep "^${variable}=" "$CONFIG_FILE" | tr -d '\r\n' | sed -e 's|.*=||g'
  132. }
  133. # ======== Path sanity checks
  134. # There is a good reason for this check. Do not pollute the game directory.
  135. [ -e "${THIS_PATH}/${ANCHOR_FILE}" ] && fatal \
  136. "Please move this script outside the game directory prior executing." \
  137. " -> See README.md for proper installation instructions"
  138. # In case people accidentally want to install the game into the launcher directory.
  139. dllcount=`find ./*.dll 2>/dev/null | wc -l`
  140. [ "$dllcount" -gt 2 ] && fatal \
  141. "This script is likely run in the wrong directory." \
  142. "Found more than two DLL files. (expected: 0...2)" \
  143. "Please run this script in a proper/clean game directory."
  144. # ======== At Exit cleanups
  145. tmp_path="" # json download path
  146. do_remove_config_ini=0 # when the install fails
  147. # shellcheck disable=SC2317
  148. atexit() {
  149. if [[ "$DEBUG" -eq 1 ]]; then
  150. echo "tmp_path: ${tmp_path}"
  151. read -rp "Interrupted before cleanup. Enter to continue" _discard
  152. fi
  153. if [[ -n "$tmp_path" && -d "$tmp_path" ]]; then
  154. rm -r "$tmp_path"
  155. fi
  156. if [ "$do_remove_config_ini" -gt 0 ]; then
  157. rm -f "$CONFIG_FILE"
  158. fi
  159. echo " -- exit cleanup done -- "
  160. }
  161. trap 'atexit' EXIT
  162. # ======== Command line processing
  163. cli_help=0
  164. cli_install=0
  165. cli_predownload=0
  166. json_path=".data .game_packages[0] .main"
  167. for arg in "$@"; do
  168. case "$arg" in
  169. -h|--help|help)
  170. cli_help=1
  171. ;;
  172. install)
  173. cli_install=1
  174. ;;
  175. nodelete)
  176. remove_archives=0
  177. echo "--- Archives will not be deleted after download"
  178. ;;
  179. predownload)
  180. cli_predownload=1
  181. json_path=".data .game_packages[0] .pre_download"
  182. echo "--- Checking for predownload versions"
  183. ;;
  184. *)
  185. fatal "Unknown option: ${arg}"
  186. esac
  187. done
  188. if [ "$cli_help" -eq 1 ]; then
  189. cat << HELP
  190. ${THIS_FILE} [-h|help] [install] [nodelete]
  191. This script will modify the current working directory.
  192. See README.md for details and examples.
  193. "install" : new game installation from scratch
  194. "nodelete" : whether to keep the downloaded archives
  195. "predownload" : Checks and downloads pre-download-game archives
  196. HELP
  197. exit 0
  198. fi
  199. # New game installation option
  200. if [ "$cli_install" -eq 1 ]; then
  201. dircount=`find ./* 2>/dev/null | wc -l`
  202. if [ "$dircount" -gt 0 ]; then
  203. fatal "'${PWD}' contains files and/or subdirectories." \
  204. "Please use an empty directory for a new installation." \
  205. "To update or resume an installation, rerun this script without the 'install' argument."
  206. fi
  207. do_remove_config_ini=1
  208. echo ""
  209. echo " 0 -> Genshin Impact [America/Europe/Asia/TW,HK,MO]"
  210. echo " 1 -> YuanShen [Mainland China]"
  211. echo " 2 -> BiliBili [Mainland China]"
  212. read -rp "Which version shall be installed? [0]/1/2: " choice
  213. if [[ -z "$choice" || "$choice" == "0" ]]; then
  214. reltype="OS"
  215. echo -ne '[General]\r\nchannel=1\r\ncps=mihoyo\r\ngame_version=0.0.0\r\nsdk_version=\r\nsub_channel=0\r\n' >"$CONFIG_FILE"
  216. elif [ "$choice" = "1" ]; then
  217. reltype="CN"
  218. echo -ne '[General]\r\nchannel=1\r\ncps=mihoyo\r\ngame_version=0.0.0\r\nsdk_version=\r\nsub_channel=1\r\n' >"$CONFIG_FILE"
  219. elif [ "$choice" = "2" ]; then
  220. reltype="BB"
  221. echo -ne '[General]\r\nchannel=14\r\ncps=bilibili\r\ngame_version=0.0.0\r\nsdk_version=\r\nsub_channel=0\r\n' >"$CONFIG_FILE"
  222. else
  223. fatal "Invalid selection"
  224. fi
  225. else
  226. # Check for existing installation
  227. if [ -e "GenshinImpact.exe" ]; then
  228. reltype="OS"
  229. elif [ -e "YuanShen.exe" ]; then
  230. if [ -e "$DATADIR/Plugins/PCGameSDK.dll" ]; then
  231. reltype="BB"
  232. else
  233. reltype="CN"
  234. fi
  235. fi
  236. fi
  237. ini_channel=$(get_ini_value "channel")
  238. ini_sub_channel=$(get_ini_value "sub_channel")
  239. # ======== Configuration file parsing
  240. game_not_found_message=(
  241. "Make sure 'Genshin Impact Game' is the current working directory."
  242. "If you would like to install the game append the 'install' option:"
  243. "bash '${THIS_PATH}/${THIS_FILE}' install"
  244. )
  245. [ -z "$reltype" ] && fatal \
  246. "Cannot determine the installed game type." \
  247. "${game_not_found_message[@]}"
  248. LANG_PACKS_PATH=${LANG_PACKS_PATH_MAP[$reltype]}
  249. UPDATE_URL=${UPDATE_URL_MAP[$reltype]}
  250. unset reltype
  251. [ ! -e "$CONFIG_FILE" ] && fatal \
  252. "Game information file ${CONFIG_FILE} not found." \
  253. "${game_not_found_message[@]}"
  254. installed_ver=`get_ini_value 'game_version'`
  255. [ -z "$installed_ver" ] && fatal \
  256. "Cannot read game_version from ${CONFIG_FILE}. File corrupt?"
  257. echo "--- Installed version: ${installed_ver}"
  258. # ======== Update information download + meta checks
  259. # WARNING: File cannot be downloaded to NTFS/FAT* partitions due to special characters
  260. tmp_path=`mktemp -d`
  261. packages_url=$(printf "$UPDATE_URL" "getGamePackages" "")
  262. download_file "$packages_url" "$tmp_path"
  263. RESOURCE_FILE=`find "$tmp_path" -name 'getGamePackages*' | tee`
  264. [ ! -f "${RESOURCE_FILE}" ] && fatal \
  265. "Failed to download version info. Check your internet connection."
  266. upstream_ver=`jq -r -M "${json_path} .major .version" "$RESOURCE_FILE" | tee`
  267. [ "$upstream_ver" = "null" ] && fatal "Could not find any matching update entry"
  268. echo "--- Latest version: ${upstream_ver}"
  269. if [ "$upstream_ver" = "$installed_ver" ]; then
  270. echo
  271. echo "==> Client is up to date."
  272. exit 0
  273. fi
  274. if [ "$cli_predownload" -eq 0 ]; then
  275. # Check whether this version can be patched
  276. patcher_dir="$REPO_PATH"/`<<< "$upstream_ver" tr -d .`
  277. # Optional until patches are needed again (if ever)
  278. if [ ! -d "$patcher_dir" ]; then
  279. echo "No patch script found for this version. Skipping patch step."
  280. patcher_dir=""
  281. fi
  282. fi
  283. # ======== Select update type
  284. # Check is diff update possible.
  285. archive_json=`jq -r -M "${json_path} .patches[] | select(.version==\"${installed_ver}\")" "$RESOURCE_FILE"`
  286. if [ -z "$archive_json" ]; then
  287. # Fallback to full download.
  288. archive_json=`jq -r -M "${json_path} .major" "$RESOURCE_FILE"`
  289. dl_type="new installation"
  290. [ "$cli_install" -eq 0 ] && fatal "Cannot find an update for ${installed_ver} -> ${upstream_ver}." \
  291. "Please use a new directory to install the game from scatch."
  292. else
  293. dl_type="incremental update"
  294. fi
  295. game_archive_json=`<<< "${archive_json}" jq -r -M ".game_pkgs[]"`
  296. size=`download_json_size "$game_archive_json"`
  297. echo "Download type: ${dl_type} (${size})"
  298. if [ "$cli_install" -eq 1 ]; then
  299. updated_ver="$upstream_ver"
  300. else
  301. # The version after updating might differ from the newest available version (bad package selection?)
  302. updated_ver=$(<<< "$archive_json" jq -r -M ".game_pkgs[0] .url")
  303. [[ "$updated_ver" =~ \.[0-9]+\.[0-9]+_([0-9]+\.[0-9]+\.[0-9]+)_ ]];
  304. updated_ver=${BASH_REMATCH[1]}
  305. if [ "$upstream_ver" != "$updated_ver" ]; then
  306. echo -e "=!= WARNING: Cannot find a direct update for ${installed_ver} -> ${upstream_ver}.\n" \
  307. " This script will first update to version ${updated_ver}.\n" \
  308. " Please re-run the update script after completing this update."
  309. fi
  310. fi
  311. # Confirm install/update.
  312. while :; do
  313. if [ "$DEBUG" -eq 0 ]; then
  314. read -rp "Start/continue update? [Y]/n: " input
  315. else
  316. input="y"
  317. fi
  318. case "$input" in
  319. Y|y|'')
  320. echo
  321. break
  322. ;;
  323. n|N)
  324. exit 0
  325. ;;
  326. esac
  327. done
  328. echo "--- Main game archive"
  329. # Download SDK if exists
  330. sdk_url=$(printf "$UPDATE_URL" "getGameChannelSDKs" "&channel=${ini_channel}&sub_channel=${ini_sub_channel}")
  331. download_file "$sdk_url" "$tmp_path"
  332. SDK_FILE=`find "$tmp_path" -name 'getGameChannelSDKs*' | tee`
  333. sdk_json=""
  334. if [ -f "$SDK_FILE" ]; then
  335. sdk_json=$(jq -r -M ".data .game_channel_sdks[0] | select(.!=null)" "$SDK_FILE")
  336. if [[ -n "$sdk_json" ]]; then
  337. sdk_section=$(<<< "$sdk_json" jq -r -M ".channel_sdk_pkg")
  338. download_json_section "$sdk_section"
  339. fi
  340. else
  341. echo "Failed to download SDK json"
  342. fi
  343. # Get all segments (array) or empty string
  344. main_archive_segments=$(<<< "$archive_json" jq -r -M ".game_pkgs")
  345. # Download the main game or update archive(s)
  346. if [ -n "$main_archive_segments" ]; then
  347. i=0
  348. while :; do
  349. section=$(<<< "$main_archive_segments" jq -r -M ".[${i}] | select(.!=null)")
  350. [ -z "$section" ] && break
  351. download_json_section "$section"
  352. i=$((i + 1))
  353. done
  354. else
  355. download_json_section "$archive_json"
  356. fi
  357. # ======== Locate and install voiceover packs
  358. lang_dir_names() {
  359. if [ -d "$LANG_PACKS_PATH" ]; then
  360. # Get voiceover directory name in capitals. Does proper space handling.
  361. find "$LANG_PACKS_PATH" -mindepth 1 -type d -print0 \
  362. | xargs -0 -L1 -r basename \
  363. | tr -d '()' \
  364. | tr '[:lower:]' '[:upper:]'
  365. fi
  366. }
  367. lang_dir_names_str=$(lang_dir_names)
  368. if [[ -z "$lang_dir_names_str" && "$cli_install" -eq 1 ]]; then
  369. # TODO: make the default language(s) selectable
  370. lang_dir_names_str="ENGLISHUS"
  371. fi
  372. # Download langs packs.
  373. while read -r dir_name; do
  374. [ -z "$dir_name" ] && continue
  375. lang_code=${LANG_MAP[$dir_name]}
  376. lang_archive_json=`<<< "${archive_json}" jq -r -M ".audio_pkgs[] | select(.language==\"${lang_code}\")"`
  377. if [ "$lang_archive_json" = 'null' ] || [ "$lang_archive_json" = '' ]; then
  378. echo "--- Cannot find update for language: ${dir_name}"
  379. continue
  380. fi
  381. size=`download_json_size "$lang_archive_json"`
  382. echo "--- Voiceover pack: ${lang_code} (${size})"
  383. download_json_section "$lang_archive_json"
  384. done <<< "$lang_dir_names_str"
  385. # ======== Revert patch & apply update
  386. if [ "$cli_predownload" -eq 1 ]; then
  387. echo
  388. echo "==> Pre-download completed. The archives will be ready on release day."
  389. exit 0
  390. fi
  391. # Run 'patch_revert.sh' on update existing installation.
  392. if [[ -n "$patcher_dir" && -e "$ANCHOR_FILE" ]]; then
  393. echo
  394. echo "============== Reverting previous Wine patch ==============="
  395. [ "$DEBUG" -eq 0 ] && bash "${patcher_dir}/patch_revert.sh"
  396. echo "============================================================"
  397. echo
  398. fi
  399. # Unpack the game files and remove old ones according to deletefiles.txt
  400. echo "--- Updating game files ..."
  401. # -L to allow symlinking the download directory to another drive
  402. zip_files=$(find -L "$DOWNLOAD_PATH" \( -name "*.zip" -o -name "*.zip.001" \))
  403. while read -r archive_name; do
  404. [ ! -f "${archive_name}.completed" ] && fatal \
  405. "Archive '${archive_name}' is not marked as complete!"
  406. bash "${THIS_PATH}/perform_update.sh" "$archive_name"
  407. done <<< "$zip_files"
  408. if [ "$remove_archives" -eq 1 ]; then
  409. # Remove downloads when all updates succeeded
  410. # otherwise keep the archives to fix with "perform_update.sh" manually
  411. echo "--- Removing downloaded archives ..."
  412. while read -r archive_name; do
  413. rm -f "${archive_name}" "${archive_name}.completed"
  414. done <<< "$zip_files"
  415. fi
  416. # ======== Config update and game patching
  417. do_remove_config_ini=0
  418. # Update version in config file.
  419. sed -i "s/game_version=${installed_ver}/game_version=${updated_ver}/" "$CONFIG_FILE"
  420. if [ -n "$sdk_json" ]; then
  421. sdk_version=`<<< "$sdk_json" jq -r -M ".version"`
  422. sed -i "s/^sdk_version=.*/sdk_version=${sdk_version}/" "$CONFIG_FILE"
  423. fi
  424. echo
  425. echo "==> Update to version ${updated_ver} completed"
  426. if [ "$remove_archives" -eq 1 ]; then
  427. while :; do
  428. read -rp "Shall the downloaded (now unused) archives be removed? [Y]/n: " input
  429. case "$input" in
  430. Y|y|'')
  431. break
  432. ;;
  433. n|N)
  434. remove_archives=0
  435. break
  436. ;;
  437. esac
  438. done
  439. if [ "$remove_archives" -eq 1 ]; then
  440. rm -r "$DOWNLOAD_PATH"
  441. fi
  442. fi
  443. if [ -n "$patcher_dir" ]; then
  444. # Run wine compatibility patch script.
  445. echo
  446. echo "================= Applying new Wine patch =================="
  447. [ "$DEBUG" -eq 0 ] && bash "${patcher_dir}/patch.sh"
  448. echo "============================================================"
  449. fi
  450. echo "==> Update script completed successfully"
  451. exit 0