mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-04 01:07:56 -04:00
Mods: HttpService (#2370)
* Update Android toolchain to platform 37 * Mods: HttpService * Update modding.md * Update modding.md * Set minor version back to 0
This commit is contained in:
@@ -257,6 +257,48 @@ mods::file::export_file(location, "report.txt", [](mods::file::PickResult result
|
||||
`export_file` copies an existing file to a user-selected destination and returns the destination location in its
|
||||
callback. Mod-owned persistent files belong in `HostService::data_dir`.
|
||||
|
||||
### HttpService (`mods/svc/http.h`)
|
||||
|
||||
Asynchronous HTTPS requests supporting HTTP/2 and TLS 1.2+. C++ mods should use the helpers in `mods/svc/http.hpp`:
|
||||
|
||||
```cpp
|
||||
#include "mods/svc/http.hpp"
|
||||
|
||||
IMPORT_SERVICE(HttpService, svc_http);
|
||||
|
||||
mods::http::Pending pendingRequest;
|
||||
|
||||
void fetch_manifest() {
|
||||
mods::http::Request request{
|
||||
.url = "https://example.com/manifest.json",
|
||||
.maxBodyBytes = 256 * 1024,
|
||||
};
|
||||
pendingRequest = mods::http::request(request, [](mods::http::Response response) {
|
||||
if (!response.ok()) {
|
||||
handle_fetch_error(response.error, response.statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string manifest{response.body.begin(), response.body.end()};
|
||||
use_manifest(manifest);
|
||||
});
|
||||
if (!pendingRequest) {
|
||||
handle_start_error(pendingRequest.result());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep the returned `Pending` alive until completion. Dropping it or calling `cancel` requests cancellation. Callbacks run
|
||||
on the game thread.
|
||||
|
||||
`Response::ok()` requires a 2xx status. Other HTTP statuses are valid responses, not transport errors, so always check
|
||||
`statusCode`. In-memory responses default to a 1 MiB limit; set `maxBodyBytes` to increase it if needed.
|
||||
|
||||
For large responses, set `downloadPath` to an absolute path in the calling mod's `HostService::data_dir` or
|
||||
`HostService::mod_dir`. The response is streamed to disk instead of loaded in memory. On success, the callback receives
|
||||
an empty `body` and the final path in `downloadPath`. Check `Response::ok()` before using the file.
|
||||
`Pending::progress()` reports download progress when the server provides a total size.
|
||||
|
||||
### HostService (`mods/svc/host.h`)
|
||||
|
||||
Mod metadata and runtime interaction with the loader:
|
||||
|
||||
Vendored
+1
-1
Submodule extern/borealis updated: 32153a5480...4b76f9ea35
@@ -1498,6 +1498,7 @@ set(DUSK_FILES
|
||||
src/dusk/mods/svc/flow.cpp
|
||||
src/dusk/mods/svc/hook.cpp
|
||||
src/dusk/mods/svc/host.cpp
|
||||
src/dusk/mods/svc/http.cpp
|
||||
src/dusk/mods/svc/item.cpp
|
||||
src/dusk/mods/svc/item.hpp
|
||||
src/dusk/mods/svc/log.cpp
|
||||
|
||||
@@ -4,7 +4,7 @@ This directory contains Dusklight's Android shell built on top of Borealis.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Android SDK installed (`ANDROID_HOME`)
|
||||
- Android SDK with Platform 37 installed (`ANDROID_HOME`)
|
||||
- Android NDK version used by CMake presets (`ANDROID_NDK_VERSION`)
|
||||
- JDK 17+
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '8.13.2' apply false
|
||||
id 'com.android.application' version '9.1.1' apply false
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
#Thu Nov 11 18:20:34 PST 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Vendored
+202
-114
@@ -1,74 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn ( ) {
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die ( ) {
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -77,84 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|grep -E -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|grep -E -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
Vendored
+46
-43
@@ -1,4 +1,22 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -8,26 +26,30 @@
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -35,54 +57,35 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
+14
-1
@@ -115,6 +115,14 @@ mod-entry .mod-entry-status.failed {
|
||||
color: #cc4444;
|
||||
}
|
||||
|
||||
mod-entry .mod-entry-network {
|
||||
margin-left: 6dp;
|
||||
padding: 1dp 5dp;
|
||||
border-radius: 5dp;
|
||||
background-color: rgba(67, 151, 219, 20%);
|
||||
color: #6fb7ef;
|
||||
}
|
||||
|
||||
mod-entry .mod-entry-desc {
|
||||
font-size: 14dp;
|
||||
line-height: 1.3;
|
||||
@@ -216,4 +224,9 @@ window.mods .mod-description {
|
||||
.mod-info-label.failed {
|
||||
color: #cc4444;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge.network {
|
||||
color: #6fb7ef;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define HTTP_SERVICE_ID "dev.twilitrealm.dusklight.http"
|
||||
#define HTTP_SERVICE_MAJOR 1u
|
||||
#define HTTP_SERVICE_MINOR 0u
|
||||
|
||||
/* Handle for an in-flight request. 0 is never a valid handle. */
|
||||
typedef uint64_t HttpRequestHandle;
|
||||
|
||||
typedef enum HttpMethod {
|
||||
HTTP_METHOD_GET = 0,
|
||||
HTTP_METHOD_POST = 1,
|
||||
HTTP_METHOD_HEAD = 2,
|
||||
} HttpMethod;
|
||||
|
||||
/* Transport-level outcome. HTTP status errors are reported through status_code. */
|
||||
typedef enum HttpError {
|
||||
HTTP_ERROR_NONE = 0,
|
||||
HTTP_ERROR_INVALID_URL = 1,
|
||||
HTTP_ERROR_UNSUPPORTED_SCHEME = 2,
|
||||
HTTP_ERROR_TIMEOUT = 3,
|
||||
HTTP_ERROR_TOO_LARGE = 4,
|
||||
HTTP_ERROR_CANCELED = 5,
|
||||
HTTP_ERROR_IO = 6,
|
||||
HTTP_ERROR_NETWORK = 7,
|
||||
} HttpError;
|
||||
|
||||
typedef struct HttpHeader {
|
||||
const char* name;
|
||||
const char* value;
|
||||
} HttpHeader;
|
||||
|
||||
typedef struct HttpRequestDesc {
|
||||
uint32_t struct_size;
|
||||
HttpMethod method;
|
||||
const char* url;
|
||||
const HttpHeader* headers;
|
||||
uint32_t header_count;
|
||||
/* Request body; POST only. */
|
||||
const void* body;
|
||||
size_t body_size;
|
||||
/* Absolute destination under this mod's data_dir or mod_dir, or NULL for an in-memory body.
|
||||
* GET and POST only. */
|
||||
const char* download_path;
|
||||
uint32_t connect_timeout_ms; /* 0 = 10 seconds */
|
||||
uint32_t idle_timeout_ms; /* 0 = 10 seconds without network progress */
|
||||
uint32_t total_timeout_ms; /* 0 = no total timeout */
|
||||
size_t max_body_bytes; /* 0 = 1 MiB; ignored for downloads */
|
||||
} HttpRequestDesc;
|
||||
|
||||
#define HTTP_REQUEST_DESC_INIT \
|
||||
{sizeof(HttpRequestDesc), HTTP_METHOD_GET, NULL, NULL, 0u, NULL, 0u, NULL, 0u, 0u, 0u, 0u}
|
||||
|
||||
/* Snapshot valid only for the duration of the completion callback. */
|
||||
typedef struct HttpResult {
|
||||
uint32_t struct_size;
|
||||
HttpError error;
|
||||
const char* error_message;
|
||||
int32_t status_code;
|
||||
const HttpHeader* headers;
|
||||
uint32_t header_count;
|
||||
const void* body;
|
||||
size_t body_size;
|
||||
/* Published absolute destination, or NULL unless a download succeeded. */
|
||||
const char* download_path;
|
||||
} HttpResult;
|
||||
|
||||
/* Runs on the game thread exactly once, unless the calling mod begins deactivation first. */
|
||||
typedef void (*HttpCompleteFn)(
|
||||
ModContext* ctx, HttpRequestHandle request, const HttpResult* result, void* user_data);
|
||||
|
||||
typedef struct HttpProgress {
|
||||
uint32_t struct_size;
|
||||
uint64_t completed_bytes;
|
||||
uint64_t total_bytes;
|
||||
bool total_known;
|
||||
} HttpProgress;
|
||||
|
||||
#define HTTP_PROGRESS_INIT {sizeof(HttpProgress), 0u, 0u, false}
|
||||
|
||||
typedef struct HttpService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Starts an asynchronous HTTPS request. */
|
||||
ModResult (*request)(ModContext* ctx, const HttpRequestDesc* desc, HttpCompleteFn fn,
|
||||
void* user_data, HttpRequestHandle* out_handle);
|
||||
ModResult (*progress)(ModContext* ctx, HttpRequestHandle request, HttpProgress* out_progress);
|
||||
/* Requests cancellation. The completion callback still runs if the mod remains active. */
|
||||
ModResult (*cancel)(ModContext* ctx, HttpRequestHandle request);
|
||||
} HttpService;
|
||||
|
||||
MOD_DECLARE_SERVICE(HttpService, svc_http, HTTP_SERVICE_ID, HTTP_SERVICE_MAJOR, HTTP_SERVICE_MINOR);
|
||||
@@ -0,0 +1,204 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/svc/http.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace mods::http {
|
||||
|
||||
struct Header {
|
||||
std::string name;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
struct Request {
|
||||
HttpMethod method = HTTP_METHOD_GET;
|
||||
std::string url;
|
||||
std::vector<Header> headers;
|
||||
std::string body;
|
||||
std::string downloadPath;
|
||||
uint32_t connectTimeoutMs = 0;
|
||||
uint32_t idleTimeoutMs = 0;
|
||||
uint32_t totalTimeoutMs = 0;
|
||||
size_t maxBodyBytes = 0;
|
||||
};
|
||||
|
||||
struct Response {
|
||||
HttpError error = HTTP_ERROR_NETWORK;
|
||||
std::string errorMessage;
|
||||
int statusCode = 0;
|
||||
std::vector<Header> headers;
|
||||
std::vector<uint8_t> body;
|
||||
std::string downloadPath;
|
||||
|
||||
bool ok() const { return error == HTTP_ERROR_NONE && statusCode >= 200 && statusCode < 300; }
|
||||
|
||||
const std::string* header(std::string_view name) const {
|
||||
const auto equal = [](std::string_view left, std::string_view right) {
|
||||
return left.size() == right.size() &&
|
||||
std::equal(left.begin(), left.end(), right.begin(), [](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||
std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
};
|
||||
const auto iter = std::find_if(headers.begin(), headers.end(),
|
||||
[&](const Header& value) { return equal(value.name, name); });
|
||||
return iter != headers.end() ? &iter->value : nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct Completion {
|
||||
HttpRequestHandle handle = 0;
|
||||
std::function<void(Response)> callback;
|
||||
};
|
||||
|
||||
inline std::unordered_map<HttpRequestHandle, std::unique_ptr<Completion>> completions;
|
||||
|
||||
inline void complete(ModContext*, HttpRequestHandle handle, const HttpResult* raw, void* userData) {
|
||||
const auto iter = completions.find(handle);
|
||||
if (iter == completions.end() || iter->second.get() != userData) {
|
||||
return;
|
||||
}
|
||||
auto completion = std::move(iter->second);
|
||||
completions.erase(iter);
|
||||
|
||||
Response response;
|
||||
if (raw != nullptr) {
|
||||
response.error = raw->error;
|
||||
response.errorMessage = raw->error_message != nullptr ? raw->error_message : "";
|
||||
response.statusCode = raw->status_code;
|
||||
response.headers.reserve(raw->header_count);
|
||||
for (uint32_t i = 0; i < raw->header_count; ++i) {
|
||||
response.headers.push_back({
|
||||
.name = raw->headers[i].name != nullptr ? raw->headers[i].name : "",
|
||||
.value = raw->headers[i].value != nullptr ? raw->headers[i].value : "",
|
||||
});
|
||||
}
|
||||
if (raw->body != nullptr && raw->body_size != 0) {
|
||||
const auto* begin = static_cast<const uint8_t*>(raw->body);
|
||||
response.body.assign(begin, begin + raw->body_size);
|
||||
}
|
||||
response.downloadPath = raw->download_path != nullptr ? raw->download_path : "";
|
||||
}
|
||||
if (completion->callback) {
|
||||
completion->callback(std::move(response));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
class Pending {
|
||||
public:
|
||||
Pending() = default;
|
||||
Pending(HttpRequestHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
|
||||
~Pending() { reset(); }
|
||||
Pending(const Pending&) = delete;
|
||||
Pending& operator=(const Pending&) = delete;
|
||||
Pending(Pending&& other) noexcept { *this = std::move(other); }
|
||||
Pending& operator=(Pending&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reset();
|
||||
mHandle = std::exchange(other.mHandle, 0);
|
||||
mResult = other.mResult;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
if (mResult != MOD_OK || mHandle == 0 || svc_http == nullptr ||
|
||||
!detail::completions.contains(mHandle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
HttpProgress value = HTTP_PROGRESS_INIT;
|
||||
return svc_http->progress(mod_ctx, mHandle, &value) == MOD_OK;
|
||||
}
|
||||
ModResult result() const { return mResult; }
|
||||
HttpRequestHandle handle() const { return mHandle; }
|
||||
|
||||
HttpProgress progress() const {
|
||||
HttpProgress value = HTTP_PROGRESS_INIT;
|
||||
if (mHandle != 0 && svc_http != nullptr) {
|
||||
svc_http->progress(mod_ctx, mHandle, &value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (mHandle != 0 && svc_http != nullptr) {
|
||||
if (svc_http->cancel(mod_ctx, mHandle) != MOD_OK) {
|
||||
detail::completions.erase(mHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void detach() { mHandle = 0; }
|
||||
|
||||
private:
|
||||
void reset() {
|
||||
cancel();
|
||||
mHandle = 0;
|
||||
}
|
||||
|
||||
HttpRequestHandle mHandle = 0;
|
||||
ModResult mResult = MOD_UNAVAILABLE;
|
||||
};
|
||||
|
||||
inline Pending request(const Request& request, std::function<void(Response)> callback) {
|
||||
if (svc_http == nullptr) {
|
||||
return {0, MOD_UNAVAILABLE};
|
||||
}
|
||||
if (!callback) {
|
||||
return {0, MOD_INVALID_ARGUMENT};
|
||||
}
|
||||
|
||||
if (request.headers.size() > std::numeric_limits<uint32_t>::max()) {
|
||||
return {0, MOD_INVALID_ARGUMENT};
|
||||
}
|
||||
std::vector<HttpHeader> headers;
|
||||
headers.reserve(request.headers.size());
|
||||
for (const auto& header : request.headers) {
|
||||
headers.push_back({
|
||||
.name = header.name.c_str(),
|
||||
.value = header.value.c_str(),
|
||||
});
|
||||
}
|
||||
HttpRequestDesc desc = HTTP_REQUEST_DESC_INIT;
|
||||
desc.method = request.method;
|
||||
desc.url = request.url.c_str();
|
||||
desc.headers = headers.empty() ? nullptr : headers.data();
|
||||
desc.header_count = static_cast<uint32_t>(headers.size());
|
||||
desc.body = request.body.empty() ? nullptr : request.body.data();
|
||||
desc.body_size = request.body.size();
|
||||
desc.download_path = request.downloadPath.empty() ? nullptr : request.downloadPath.c_str();
|
||||
desc.connect_timeout_ms = request.connectTimeoutMs;
|
||||
desc.idle_timeout_ms = request.idleTimeoutMs;
|
||||
desc.total_timeout_ms = request.totalTimeoutMs;
|
||||
desc.max_body_bytes = request.maxBodyBytes;
|
||||
|
||||
auto completion = std::make_unique<detail::Completion>();
|
||||
auto* userData = completion.get();
|
||||
completion->callback = std::move(callback);
|
||||
HttpRequestHandle handle = 0;
|
||||
const auto result = svc_http->request(mod_ctx, &desc, detail::complete, userData, &handle);
|
||||
if (result != MOD_OK) {
|
||||
return {0, result};
|
||||
}
|
||||
completion->handle = handle;
|
||||
detail::completions.emplace(handle, std::move(completion));
|
||||
return {handle, result};
|
||||
}
|
||||
|
||||
} // namespace mods::http
|
||||
@@ -568,6 +568,11 @@ void file_initialize() {
|
||||
config::Register(s_pickerOverride);
|
||||
}
|
||||
|
||||
bool file_available() {
|
||||
const auto capabilities = borealis::file_select::capabilities();
|
||||
return capabilities.canOpenFile || capabilities.canOpenFolder || capabilities.canExportFile;
|
||||
}
|
||||
|
||||
ModResult copy_picker_override(
|
||||
std::string_view source, std::string_view destination, std::string& error) {
|
||||
try {
|
||||
@@ -688,6 +693,7 @@ constinit const ServiceModule g_fileModule{
|
||||
.majorVersion = FILE_SERVICE_MAJOR,
|
||||
.minorVersion = FILE_SERVICE_MINOR,
|
||||
.service = &s_fileService,
|
||||
.available = file_available,
|
||||
.initialize = file_initialize,
|
||||
.modDetached = file_remove_mod,
|
||||
.frameBegin = file_frame_begin,
|
||||
|
||||
@@ -0,0 +1,577 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/http.h"
|
||||
|
||||
#include <borealis/http.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/version.h>
|
||||
#include <fmt/format.h>
|
||||
#include <xxhash.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
constexpr size_t MaxRequestsPerMod = 16;
|
||||
constexpr size_t MaxUrlBytes = 8 * 1024;
|
||||
constexpr size_t MaxHeaders = 64;
|
||||
constexpr size_t MaxHeaderBytes = 16 * 1024;
|
||||
constexpr size_t MaxRequestBodyBytes = 16 * 1024 * 1024;
|
||||
constexpr size_t DefaultResponseBodyBytes = 1024 * 1024;
|
||||
constexpr size_t MaxResponseBodyBytes = 64 * 1024 * 1024;
|
||||
constexpr std::chrono::milliseconds DefaultTimeout{10000};
|
||||
|
||||
struct PendingRequest {
|
||||
HttpCompleteFn callback = nullptr;
|
||||
void* userData = nullptr;
|
||||
borealis::Task<borealis::http::Result> task;
|
||||
std::filesystem::path stagingPath;
|
||||
std::filesystem::path downloadPath;
|
||||
bool completing = false;
|
||||
};
|
||||
|
||||
static_assert(std::is_nothrow_move_constructible_v<PendingRequest>);
|
||||
|
||||
SlotMap<PendingRequest> s_requests;
|
||||
|
||||
bool ascii_iequals(std::string_view left, std::string_view right) {
|
||||
return left.size() == right.size() && std::ranges::equal(left, right, [](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||
std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
}
|
||||
|
||||
bool is_reserved_header(std::string_view name) {
|
||||
constexpr std::string_view reserved[]{
|
||||
"User-Agent",
|
||||
"Host",
|
||||
"Content-Length",
|
||||
"Connection",
|
||||
"Accept-Encoding",
|
||||
"Range",
|
||||
"If-Range",
|
||||
};
|
||||
return std::ranges::any_of(
|
||||
reserved, [&](std::string_view value) { return ascii_iequals(name, value); });
|
||||
}
|
||||
|
||||
bool valid_header_name(std::string_view name) {
|
||||
constexpr std::string_view separators{"()<>@,;:\\\"/[]?={} \t"};
|
||||
return !name.empty() && std::ranges::all_of(name, [&](unsigned char value) {
|
||||
return value > 32 && value < 127 &&
|
||||
separators.find(static_cast<char>(value)) == std::string_view::npos;
|
||||
});
|
||||
}
|
||||
|
||||
bool valid_url(std::string_view url) {
|
||||
constexpr std::string_view scheme{"https://"};
|
||||
if (!url.starts_with(scheme) || url.size() <= scheme.size() || url.size() > MaxUrlBytes) {
|
||||
return false;
|
||||
}
|
||||
if (std::ranges::any_of(url, [](unsigned char value) { return value <= 32 || value == 127; })) {
|
||||
return false;
|
||||
}
|
||||
const auto authorityEnd = url.find_first_of("/?#", scheme.size());
|
||||
const auto authority = url.substr(scheme.size(), authorityEnd - scheme.size());
|
||||
return !authority.empty();
|
||||
}
|
||||
|
||||
bool declares_http_import(const LoadedMod& mod) {
|
||||
return std::ranges::any_of(
|
||||
mod.manifestInfo.imports, [](const ModManifestInfo::Import& serviceImport) {
|
||||
return serviceImport.id == HTTP_SERVICE_ID;
|
||||
});
|
||||
}
|
||||
|
||||
std::filesystem::path normalized_absolute(const std::filesystem::path& path, std::error_code& ec) {
|
||||
auto result = std::filesystem::absolute(path, ec);
|
||||
return ec ? std::filesystem::path{} : result.lexically_normal();
|
||||
}
|
||||
|
||||
bool path_is_below(const std::filesystem::path& path, const std::filesystem::path& directory) {
|
||||
const auto [directoryEnd, pathPosition] =
|
||||
std::mismatch(directory.begin(), directory.end(), path.begin(), path.end());
|
||||
return directoryEnd == directory.end() && pathPosition != path.end();
|
||||
}
|
||||
|
||||
bool path_is_at_or_below(
|
||||
const std::filesystem::path& path, const std::filesystem::path& directory) {
|
||||
const auto [directoryEnd, pathPosition] =
|
||||
std::mismatch(directory.begin(), directory.end(), path.begin(), path.end());
|
||||
(void)pathPosition;
|
||||
return directoryEnd == directory.end();
|
||||
}
|
||||
|
||||
std::filesystem::path data_root(const LoadedMod& mod, std::error_code& ec) {
|
||||
if (!mod.dataDirUtf8.empty()) {
|
||||
return normalized_absolute(borealis::io::fs_path_from_utf8(mod.dataDirUtf8), ec);
|
||||
}
|
||||
return normalized_absolute(ConfigPath / "mod_data" / mod.metadata.id, ec);
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> validate_download_path(
|
||||
const LoadedMod& mod, const char* rawPath) {
|
||||
if (rawPath == nullptr) {
|
||||
return std::filesystem::path{};
|
||||
}
|
||||
const auto supplied = borealis::io::fs_path_from_utf8(rawPath);
|
||||
if (!supplied.is_absolute()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const auto path = normalized_absolute(supplied, ec);
|
||||
if (ec) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto modRoot = normalized_absolute(mod.dir, ec);
|
||||
if (ec) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto dataRoot = data_root(mod, ec);
|
||||
if (ec) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto stagingRoot = (modRoot / "downloads").lexically_normal();
|
||||
if ((!path_is_below(path, modRoot) && !path_is_below(path, dataRoot)) ||
|
||||
path_is_at_or_below(path, stagingRoot))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
std::filesystem::path staging_path(const LoadedMod& mod, std::string_view url) {
|
||||
const auto& modId = mod.metadata.id;
|
||||
const auto modHash = XXH64(modId.data(), modId.size(), 0);
|
||||
const auto hash = XXH64(url.data(), url.size(), modHash);
|
||||
return mod.dir / "downloads" / fmt::format("{:016x}.part", hash);
|
||||
}
|
||||
|
||||
HttpError map_error(borealis::http::Error error) {
|
||||
switch (error) {
|
||||
case borealis::http::Error::None:
|
||||
return HTTP_ERROR_NONE;
|
||||
case borealis::http::Error::InvalidUrl:
|
||||
return HTTP_ERROR_INVALID_URL;
|
||||
case borealis::http::Error::UnsupportedScheme:
|
||||
return HTTP_ERROR_UNSUPPORTED_SCHEME;
|
||||
case borealis::http::Error::Timeout:
|
||||
return HTTP_ERROR_TIMEOUT;
|
||||
case borealis::http::Error::TooLarge:
|
||||
return HTTP_ERROR_TOO_LARGE;
|
||||
case borealis::http::Error::Canceled:
|
||||
return HTTP_ERROR_CANCELED;
|
||||
case borealis::http::Error::Io:
|
||||
return HTTP_ERROR_IO;
|
||||
case borealis::http::Error::NoBackend:
|
||||
case borealis::http::Error::NotInitialized:
|
||||
case borealis::http::Error::Network:
|
||||
return HTTP_ERROR_NETWORK;
|
||||
default:
|
||||
return HTTP_ERROR_NETWORK;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<borealis::http::Method> borealis_method(HttpMethod method) {
|
||||
switch (method) {
|
||||
case HTTP_METHOD_GET:
|
||||
return borealis::http::Method::Get;
|
||||
case HTTP_METHOD_POST:
|
||||
return borealis::http::Method::Post;
|
||||
case HTTP_METHOD_HEAD:
|
||||
return borealis::http::Method::Head;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
borealis::http::Result publish_download(borealis::http::Result result,
|
||||
const std::filesystem::path& staging, const std::filesystem::path& destination) noexcept {
|
||||
if (result.error != borealis::http::Error::None) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
std::string renameError;
|
||||
if (borealis::io::atomic_replace(staging, destination, renameError)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::filesystem::path temporary = destination;
|
||||
temporary += "." + borealis::io::fs_path_to_string(staging.filename()) + ".part";
|
||||
std::error_code ec;
|
||||
std::filesystem::copy_file(
|
||||
staging, temporary, std::filesystem::copy_options::overwrite_existing, ec);
|
||||
if (ec) {
|
||||
const std::string copyError = ec.message();
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(temporary, ignored);
|
||||
result.error = borealis::http::Error::Io;
|
||||
result.message = "Failed to publish download: " + copyError;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string replaceError;
|
||||
if (!borealis::io::atomic_replace(temporary, destination, replaceError)) {
|
||||
std::filesystem::remove(temporary, ec);
|
||||
result.error = borealis::http::Error::Io;
|
||||
result.message = "Failed to publish download: " + replaceError;
|
||||
return result;
|
||||
}
|
||||
std::filesystem::remove(staging, ec);
|
||||
return result;
|
||||
} catch (const std::exception& exception) {
|
||||
result.error = borealis::http::Error::Io;
|
||||
result.message = std::string{"Failed to publish download: "} + exception.what();
|
||||
return result;
|
||||
} catch (...) {
|
||||
result.error = borealis::http::Error::Io;
|
||||
result.message = "Failed to publish download";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
void http_frame_begin() {
|
||||
std::vector<HttpRequestHandle> ready;
|
||||
s_requests.for_each([&](const HttpRequestHandle handle, const auto& entry) {
|
||||
const auto& pending = entry.value;
|
||||
if (!pending.completing && pending.task.ready()) {
|
||||
ready.push_back(handle);
|
||||
}
|
||||
});
|
||||
|
||||
for (const auto handle : ready) {
|
||||
auto* entry = s_requests.find(handle);
|
||||
if (entry == nullptr || !entry->owner->active || entry->value.completing) {
|
||||
continue;
|
||||
}
|
||||
auto& pending = entry->value;
|
||||
borealis::http::Result result;
|
||||
try {
|
||||
auto completed = pending.task.try_take();
|
||||
if (!completed.has_value()) {
|
||||
continue;
|
||||
}
|
||||
result = std::move(*completed);
|
||||
} catch (const std::exception& exception) {
|
||||
result = {
|
||||
.error = borealis::http::Error::Io,
|
||||
.message = exception.what(),
|
||||
};
|
||||
} catch (...) {
|
||||
result = {
|
||||
.error = borealis::http::Error::Io,
|
||||
.message = "HTTP request completion failed",
|
||||
};
|
||||
}
|
||||
|
||||
auto* owner = entry->owner;
|
||||
const auto callback = pending.callback;
|
||||
const auto userData = pending.userData;
|
||||
pending.completing = true;
|
||||
|
||||
std::vector<HttpHeader> headers;
|
||||
headers.reserve(result.response.headers.size());
|
||||
for (const auto& header : result.response.headers) {
|
||||
headers.push_back({.name = header.name.c_str(), .value = header.value.c_str()});
|
||||
}
|
||||
const bool downloadSucceeded =
|
||||
!pending.downloadPath.empty() && result.error == borealis::http::Error::None;
|
||||
const auto publishedPath = downloadSucceeded ?
|
||||
borealis::io::fs_path_to_string(pending.downloadPath) :
|
||||
std::string{};
|
||||
const HttpResult snapshot{
|
||||
.struct_size = sizeof(HttpResult),
|
||||
.error = map_error(result.error),
|
||||
.error_message = result.message.c_str(),
|
||||
.status_code = result.response.statusCode,
|
||||
.headers = headers.empty() ? nullptr : headers.data(),
|
||||
.header_count = static_cast<uint32_t>(headers.size()),
|
||||
.body = pending.downloadPath.empty() && !result.response.body.empty() ?
|
||||
result.response.body.data() :
|
||||
nullptr,
|
||||
.body_size = pending.downloadPath.empty() ? result.response.body.size() : 0,
|
||||
.download_path = downloadSucceeded ? publishedPath.c_str() : nullptr,
|
||||
};
|
||||
|
||||
try {
|
||||
callback(owner->context.get(), handle, &snapshot, userData);
|
||||
} catch (const std::exception& exception) {
|
||||
fail_mod(*owner, MOD_ERROR,
|
||||
std::string{"exception in HTTP completion callback: "} + exception.what());
|
||||
} catch (...) {
|
||||
fail_mod(*owner, MOD_ERROR, "unknown exception in HTTP completion callback");
|
||||
}
|
||||
s_requests.erase(handle);
|
||||
}
|
||||
}
|
||||
|
||||
size_t active_request_count(const LoadedMod& mod) {
|
||||
size_t count = 0;
|
||||
s_requests.for_each([&](HttpRequestHandle, const auto& entry) {
|
||||
if (entry.owner == &mod && !entry.value.completing) {
|
||||
++count;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
bool staging_path_in_use(const LoadedMod& mod, const std::filesystem::path& path) {
|
||||
bool inUse = false;
|
||||
s_requests.for_each([&](HttpRequestHandle, const auto& entry) {
|
||||
if (entry.owner == &mod && !entry.value.completing && entry.value.stagingPath == path) {
|
||||
inUse = true;
|
||||
}
|
||||
});
|
||||
return inUse;
|
||||
}
|
||||
|
||||
std::string user_agent_version(std::string_view version) {
|
||||
std::string result{version};
|
||||
for (char& ch : result) {
|
||||
const auto value = static_cast<unsigned char>(ch);
|
||||
if (value <= 32 || value >= 127) {
|
||||
ch = '_';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ModResult start_request(LoadedMod& mod, const HttpRequestDesc& desc, HttpCompleteFn callback,
|
||||
void* userData, HttpRequestHandle& outHandle) {
|
||||
const std::string_view url{desc.url};
|
||||
const auto method = borealis_method(desc.method);
|
||||
if (!valid_url(url) || !method.has_value() ||
|
||||
(desc.header_count != 0 && desc.headers == nullptr) ||
|
||||
(desc.body_size != 0 && desc.body == nullptr) || desc.body_size > MaxRequestBodyBytes ||
|
||||
((desc.method == HTTP_METHOD_GET || desc.method == HTTP_METHOD_HEAD) &&
|
||||
desc.body_size != 0) ||
|
||||
(desc.method == HTTP_METHOD_HEAD && desc.download_path != nullptr) ||
|
||||
desc.header_count > MaxHeaders)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
size_t headerBytes = 0;
|
||||
for (uint32_t i = 0; i < desc.header_count; ++i) {
|
||||
const auto& header = desc.headers[i];
|
||||
if (header.name == nullptr || header.value == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const std::string_view name{header.name};
|
||||
const std::string_view value{header.value};
|
||||
const bool invalidValue = std::ranges::any_of(
|
||||
value, [](unsigned char ch) { return (ch < 32 && ch != '\t') || ch == 127; });
|
||||
if (!valid_header_name(name) || invalidValue || is_reserved_header(name) ||
|
||||
name.size() > MaxHeaderBytes - headerBytes)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
headerBytes += name.size();
|
||||
if (value.size() > MaxHeaderBytes - headerBytes) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
headerBytes += value.size();
|
||||
}
|
||||
|
||||
auto downloadPath = validate_download_path(mod, desc.download_path);
|
||||
if (!downloadPath.has_value() ||
|
||||
(downloadPath->empty() && desc.max_body_bytes > MaxResponseBodyBytes))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (active_request_count(mod) >= MaxRequestsPerMod) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
std::filesystem::path staging;
|
||||
if (!downloadPath->empty()) {
|
||||
staging = staging_path(mod, url);
|
||||
if (staging_path_in_use(mod, staging)) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(staging.parent_path(), ec);
|
||||
if (ec) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
std::filesystem::create_directories(downloadPath->parent_path(), ec);
|
||||
if (ec) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
PendingRequest pending{
|
||||
.callback = callback,
|
||||
.userData = userData,
|
||||
.stagingPath = staging,
|
||||
.downloadPath = *downloadPath,
|
||||
};
|
||||
|
||||
if (!borealis::http::available()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
borealis::http::Request request{
|
||||
.method = *method,
|
||||
.url = std::string{url},
|
||||
.body = desc.body_size != 0 ?
|
||||
std::string{static_cast<const char*>(desc.body), desc.body_size} :
|
||||
std::string{},
|
||||
.downloadTo = staging,
|
||||
.connectTimeout = desc.connect_timeout_ms != 0 ?
|
||||
std::chrono::milliseconds{desc.connect_timeout_ms} :
|
||||
DefaultTimeout,
|
||||
.idleTimeout = desc.idle_timeout_ms != 0 ?
|
||||
std::chrono::milliseconds{desc.idle_timeout_ms} :
|
||||
DefaultTimeout,
|
||||
.totalTimeout = desc.total_timeout_ms != 0 ?
|
||||
std::optional{std::chrono::milliseconds{desc.total_timeout_ms}} :
|
||||
std::nullopt,
|
||||
.maxBodyBytes =
|
||||
desc.max_body_bytes != 0 ? desc.max_body_bytes : DefaultResponseBodyBytes,
|
||||
};
|
||||
request.headers.reserve(desc.header_count + 1);
|
||||
for (uint32_t i = 0; i < desc.header_count; ++i) {
|
||||
request.headers.push_back({desc.headers[i].name, desc.headers[i].value});
|
||||
}
|
||||
request.headers.push_back({
|
||||
.name = "User-Agent",
|
||||
.value = fmt::format("{}/{} {}/{}", AppName, BOREALIS_APP_VERSION, mod.metadata.id,
|
||||
user_agent_version(mod.metadata.version)),
|
||||
});
|
||||
|
||||
auto task = borealis::http::start(std::move(request));
|
||||
if (task.ready()) {
|
||||
auto immediate = task.try_take();
|
||||
if (!immediate.has_value()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (immediate->error == borealis::http::Error::NoBackend ||
|
||||
immediate->error == borealis::http::Error::NotInitialized)
|
||||
{
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
task = borealis::detail::make_ready_task(std::move(*immediate));
|
||||
}
|
||||
if (!downloadPath->empty()) {
|
||||
task = std::move(task).map([staging, destination = *downloadPath](auto&& result) {
|
||||
return publish_download(std::move(result), staging, destination);
|
||||
});
|
||||
}
|
||||
pending.task = std::move(task);
|
||||
|
||||
outHandle = s_requests.emplace(mod, std::move(pending));
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult http_request(ModContext* context, const HttpRequestDesc* desc, HttpCompleteFn callback,
|
||||
void* userData, HttpRequestHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(HttpRequestDesc) ||
|
||||
desc->url == nullptr || callback == nullptr || outHandle == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!declares_http_import(*mod)) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
try {
|
||||
return start_request(*mod, *desc, callback, userData, *outHandle);
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
ModResult http_progress(ModContext* context, HttpRequestHandle handle, HttpProgress* outProgress) {
|
||||
const uint32_t structSize = outProgress != nullptr ? outProgress->struct_size : 0;
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || outProgress == nullptr || structSize < sizeof(HttpProgress)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outProgress = HttpProgress{.struct_size = structSize};
|
||||
const auto* entry = s_requests.find_owned(handle, *mod);
|
||||
if (entry == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
const auto progress = entry->value.task.progress();
|
||||
outProgress->completed_bytes = progress.completed;
|
||||
outProgress->total_bytes = progress.total.value_or(0);
|
||||
outProgress->total_known = progress.total.has_value();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult http_cancel(ModContext* context, HttpRequestHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* entry = s_requests.find_owned(handle, *mod);
|
||||
if (entry == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
entry->value.task.cancel();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void http_mod_deactivating(LoadedMod& mod) {
|
||||
(void)s_requests.take_all(mod);
|
||||
}
|
||||
|
||||
void http_mod_detached(LoadedMod& mod) {
|
||||
bool found = false;
|
||||
s_requests.for_each(
|
||||
[&](HttpRequestHandle, const auto& entry) { found = found || entry.owner == &mod; });
|
||||
assert(!found);
|
||||
}
|
||||
|
||||
void http_shutdown() {
|
||||
s_requests = {};
|
||||
}
|
||||
|
||||
bool http_available() {
|
||||
return borealis::http::available() && borealis::http::initialize();
|
||||
}
|
||||
|
||||
constexpr HttpService s_httpService{
|
||||
.header = SERVICE_HEADER(HttpService, HTTP_SERVICE_MAJOR, HTTP_SERVICE_MINOR),
|
||||
.request = http_request,
|
||||
.progress = http_progress,
|
||||
.cancel = http_cancel,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_httpModule{
|
||||
.id = HTTP_SERVICE_ID,
|
||||
.majorVersion = HTTP_SERVICE_MAJOR,
|
||||
.minorVersion = HTTP_SERVICE_MINOR,
|
||||
.service = &s_httpService,
|
||||
.available = http_available,
|
||||
.modDeactivating = http_mod_deactivating,
|
||||
.modDetached = http_mod_detached,
|
||||
.frameBegin = http_frame_begin,
|
||||
.shutdown = http_shutdown,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -9,10 +9,6 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
@@ -142,6 +138,9 @@ const ServiceRecord* find_service_record(const char* serviceId, const uint16_t m
|
||||
}
|
||||
|
||||
ModResult register_module(const ServiceModule& module) {
|
||||
if (module.available != nullptr && !module.available()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const auto result = register_service(
|
||||
module.id, module.majorVersion, module.minorVersion, module.service, nullptr, false);
|
||||
if (result != MOD_OK) {
|
||||
@@ -214,9 +213,8 @@ void ModLoader::init_services() {
|
||||
&svc::g_hostModule,
|
||||
&svc::g_logModule,
|
||||
&svc::g_resourceModule,
|
||||
#if !defined(__APPLE__) || !TARGET_OS_TV
|
||||
&svc::g_fileModule,
|
||||
#endif
|
||||
&svc::g_httpModule,
|
||||
&svc::g_hookModule,
|
||||
&svc::g_overlayModule,
|
||||
&svc::g_textureModule,
|
||||
|
||||
@@ -26,6 +26,8 @@ struct ServiceModule {
|
||||
uint16_t minorVersion = 0;
|
||||
const void* service = nullptr;
|
||||
|
||||
// False prevents registration when a platform dependency is unavailable.
|
||||
bool (*available)() = nullptr;
|
||||
// One-time setup, at registration (ModLoader::init_services).
|
||||
void (*initialize)() = nullptr;
|
||||
// A mod is beginning deactivation: stop callbacks that may execute concurrently. Service state
|
||||
@@ -69,6 +71,7 @@ extern const ServiceModule g_hostModule;
|
||||
extern const ServiceModule g_logModule;
|
||||
extern const ServiceModule g_resourceModule;
|
||||
extern const ServiceModule g_fileModule;
|
||||
extern const ServiceModule g_httpModule;
|
||||
extern const ServiceModule g_hookModule;
|
||||
extern const ServiceModule g_overlayModule;
|
||||
extern const ServiceModule g_textureModule;
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
#include "fmt/format.h"
|
||||
#include "logs_window.hpp"
|
||||
#include "mod_texture_provider.hpp"
|
||||
#include "mods/svc/http.h"
|
||||
#include "pane.hpp"
|
||||
|
||||
#include "Z2AudioLib/Z2SeMgr.h"
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -39,6 +41,13 @@ ModStatus mod_status(const mods::LoadedMod& mod) {
|
||||
return {"", "Disabled"};
|
||||
}
|
||||
|
||||
bool mod_uses_network(const mods::LoadedMod& mod) {
|
||||
return std::ranges::any_of(
|
||||
mod.manifestInfo.imports, [](const mods::ModManifestInfo::Import& serviceImport) {
|
||||
return serviceImport.id == HTTP_SERVICE_ID;
|
||||
});
|
||||
}
|
||||
|
||||
// Truncates to at most maxBytes without splitting a UTF-8 sequence.
|
||||
std::string snippet(std::string_view text, size_t maxBytes) {
|
||||
if (text.size() <= maxBytes) {
|
||||
@@ -63,16 +72,19 @@ public:
|
||||
iconRml = R"(<icon class="mod-icon placeholder"/>)";
|
||||
}
|
||||
const auto status = mod_status(mod);
|
||||
const auto networkBadge = mod_uses_network(mod) ?
|
||||
R"(<span class="mod-entry-network">Network</span>)" :
|
||||
Rml::String{};
|
||||
mRoot->SetInnerRML(fmt::format(
|
||||
R"({})"
|
||||
R"(<div class="mod-entry-info">)"
|
||||
R"(<div class="mod-entry-name"><span class="mod-entry-name-text">{}</span>)"
|
||||
R"(<span class="mod-entry-version">v{}</span></div>)"
|
||||
R"(<div class="mod-entry-sub">{} - <span class="mod-entry-status {}">{}</span></div>)"
|
||||
R"(<div class="mod-entry-sub">{} - <span class="mod-entry-status {}">{}</span>{}</div>)"
|
||||
R"(<div class="mod-entry-desc">{}</div>)"
|
||||
R"(</div>)",
|
||||
iconRml, escape(mod.metadata.name), escape(mod.metadata.version),
|
||||
escape(mod.metadata.author), status.badgeClass, status.text,
|
||||
escape(mod.metadata.author), status.badgeClass, status.text, networkBadge,
|
||||
escape(snippet(mod.metadata.description, 90))));
|
||||
mRoot->SetClass("inactive", !mod.active);
|
||||
mRoot->SetClass("failed", mod.loadFailed);
|
||||
@@ -225,6 +237,9 @@ void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) {
|
||||
statusBadge = fmt::format(
|
||||
R"( <span class="status-badge {}">{}</span>)", status.badgeClass, status.text);
|
||||
}
|
||||
if (mod_uses_network(mod)) {
|
||||
statusBadge += R"( <span class="status-badge network">Network</span>)";
|
||||
}
|
||||
pane.add_rml(fmt::format(R"(<div class="mod-title">{} )"
|
||||
R"(<span class="mod-title-version">v{}</span>{}</div>)"
|
||||
R"(<div class="mod-author">by {}</div>)",
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include <borealis/aurora_log.h>
|
||||
#include <borealis/cli.hpp>
|
||||
#include <borealis/crash.hpp>
|
||||
#include <borealis/http.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/sentry.hpp>
|
||||
#include <borealis/version.h>
|
||||
@@ -769,6 +770,10 @@ int game_main(int argc, char* argv[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (borealis::http::available() && !borealis::http::initialize()) {
|
||||
DuskLog.warn("Failed to initialize the HTTP worker pool");
|
||||
}
|
||||
|
||||
if (dusk::getSettings().game.enableHighQualityMinimapTextures.getValue()) {
|
||||
dusk::hq_minimap::set_active(true);
|
||||
}
|
||||
@@ -885,6 +890,7 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
// pre game launch ui main loop
|
||||
if (!launchUILoop()) {
|
||||
borealis::http::shutdown();
|
||||
borealis::sentry::shutdown();
|
||||
borealis::log::shutdown();
|
||||
fflush(stdout);
|
||||
@@ -975,6 +981,7 @@ int game_main(int argc, char* argv[]) {
|
||||
OSReport("Starting main01 (Game Loop)...\n");
|
||||
|
||||
main01();
|
||||
borealis::http::shutdown();
|
||||
|
||||
// We need to cleanly shut down the threads to avoid crashes on shutdown.
|
||||
if (daMP_c::m_myObj) {
|
||||
|
||||
Reference in New Issue
Block a user