Initial commit
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
build/
|
||||||
|
out/
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles/
|
||||||
|
cmake_install.cmake
|
||||||
|
install/
|
||||||
|
Makefile
|
||||||
|
*.ninja
|
||||||
|
vcpkg_installed/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.a
|
||||||
|
*.lib
|
||||||
|
*.pdb
|
||||||
|
*.idb
|
||||||
|
*.ilk
|
||||||
|
*.exp
|
||||||
|
*.gch
|
||||||
|
*.pch
|
||||||
|
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
client/common/shaders/build/
|
||||||
452
CMakeLists.txt
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.25)
|
||||||
|
|
||||||
|
project(Aya)
|
||||||
|
|
||||||
|
########## Project Options ##########
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||||
|
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
|
||||||
|
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||||
|
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
|
||||||
|
|
||||||
|
# Create many helpful paths to have CMake consume it
|
||||||
|
file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}/third-party" THIRD_PARTY_DIR)
|
||||||
|
list(APPEND CMAKE_PREFIX_PATH "${THIRD_PARTY_DIR}")
|
||||||
|
|
||||||
|
file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}/engine" ENGINE_DIR)
|
||||||
|
file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}/client" CLIENT_DIR)
|
||||||
|
file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}/tools" TOOLS_DIR)
|
||||||
|
|
||||||
|
set(AYA_VERSION_MAJOR 1)
|
||||||
|
set(AYA_VERSION_MINOR 0)
|
||||||
|
set(AYA_VERSION_PATCH 0)
|
||||||
|
set(AYA_VERSION_TYPE beta)
|
||||||
|
|
||||||
|
set(AYA_PROJECT_NAME "Aya" CACHE STRING "Public-facing project name")
|
||||||
|
|
||||||
|
option(AYA_TEST_BUILD "Enable testing utilities" ON)
|
||||||
|
option(ADDRESS_SANITIZER "Enable address sanitization" OFF)
|
||||||
|
option(ENABLE_CHROMIUM_FRAMES "Enable ChromiumFrames with CEF integration" ON)
|
||||||
|
set(CEF_VERSION "140.1.14+geb1c06e+chromium-140.0.7339.185" CACHE STRING "CEF version to use (if ChromiumFrames are enabled)")
|
||||||
|
option(ENABLE_VOICE_CHAT "Enable voice chat" ON)
|
||||||
|
option(ENABLE_DISCORD_INTEGRATION "Enable Discord Rich Presence integration" ON)
|
||||||
|
|
||||||
|
if(AYA_TEST_BUILD AND NOT CI)
|
||||||
|
add_compile_definitions(AYA_TEST_BUILD=1)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT DEFINED AYA_BUILD_TIMESTAMP)
|
||||||
|
if(AYA_TEST_BUILD)
|
||||||
|
set(_AYA_BUILD_TIMESTAMP "0")
|
||||||
|
else()
|
||||||
|
string(TIMESTAMP _AYA_BUILD_TIMESTAMP "%s" UTC)
|
||||||
|
endif()
|
||||||
|
set(AYA_BUILD_TIMESTAMP "${_AYA_BUILD_TIMESTAMP}" CACHE STRING "Aya build timestamp")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if("${CMAKE_SYSTEM_PROCESSOR}" MATCHES "^(arm64|ARM64|aarch64|AARCH64)$")
|
||||||
|
set(AYA_ARCH_ARM64 1)
|
||||||
|
add_compile_definitions(AYA_ARCH_ARM64=1 _ARM64_=1)
|
||||||
|
set(AYA_PLATFORM_NAME "(arm64)")
|
||||||
|
elseif("${CMAKE_SYSTEM_PROCESSOR}" MATCHES "^(x86_64|X86_64|amd64|AMD64)$")
|
||||||
|
set(AYA_ARCH_X64 1)
|
||||||
|
add_compile_definitions(AYA_ARCH_X64=1 _AMD64_=1)
|
||||||
|
set(AYA_PLATFORM_NAME "(x64)")
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "Aya: Unsupported architecture ${CMAKE_SYSTEM_PROCESSOR}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
|
||||||
|
set(AYA_OS_WINDOWS 1)
|
||||||
|
add_compile_definitions(AYA_OS_WINDOWS)
|
||||||
|
set(AYA_PLATFORM_NAME "Windows ${AYA_PLATFORM_NAME}")
|
||||||
|
elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux")
|
||||||
|
set(AYA_OS_LINUX 1)
|
||||||
|
add_compile_definitions(AYA_OS_LINUX)
|
||||||
|
set(AYA_PLATFORM_NAME "Linux ${AYA_PLATFORM_NAME}")
|
||||||
|
elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")
|
||||||
|
set(AYA_OS_MACOS 1)
|
||||||
|
add_compile_definitions(AYA_OS_MACOS)
|
||||||
|
set(AYA_PLATFORM_NAME "MacOS ${AYA_PLATFORM_NAME}")
|
||||||
|
elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "Android")
|
||||||
|
set(AYA_OS_ANDROID 1)
|
||||||
|
add_compile_definitions(AYA_OS_ANDROID)
|
||||||
|
set(AYA_PLATFORM_NAME "Android ${AYA_PLATFORM_NAME}")
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "Aya: Unsupported system ${CMAKE_SYSTEM_NAME}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_compile_definitions(
|
||||||
|
AYA_PROJECT_NAME="${AYA_PROJECT_NAME}"
|
||||||
|
|
||||||
|
AYA_COMPILER_ID="${CMAKE_CXX_COMPILER_ID}"
|
||||||
|
AYA_VERSION_MAJOR=${AYA_VERSION_MAJOR}
|
||||||
|
AYA_VERSION_MAJOR_STR="${AYA_VERSION_MAJOR}"
|
||||||
|
AYA_VERSION_MINOR=${AYA_VERSION_MINOR}
|
||||||
|
AYA_VERSION_MINOR_STR="${AYA_VERSION_MINOR}"
|
||||||
|
AYA_VERSION_PATCH=${AYA_VERSION_PATCH}
|
||||||
|
AYA_VERSION_PATCH_STR="${AYA_VERSION_PATCH}"
|
||||||
|
AYA_VERSION_TYPE="${AYA_VERSION_TYPE}"
|
||||||
|
AYA_BUILD_TIMESTAMP=${AYA_BUILD_TIMESTAMP}
|
||||||
|
AYA_PLATFORM_NAME="${AYA_PLATFORM_NAME}"
|
||||||
|
)
|
||||||
|
|
||||||
|
########## Compile Options ##########
|
||||||
|
|
||||||
|
# ChromiumFrames and related SSE3 options enabled required for rendering optimizations
|
||||||
|
# Building CEF on nixpkgs for MacOS is non-existent
|
||||||
|
if(AYA_OS_ANDROID OR (AYA_OS_MACOS AND CI))
|
||||||
|
set(ENABLE_CHROMIUM_FRAMES OFF)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ENABLE_CHROMIUM_FRAMES)
|
||||||
|
add_compile_definitions(ENABLE_CHROMIUM_FRAMES=1)
|
||||||
|
add_compile_definitions(CEF_VERSION="${CEF_VERSION}")
|
||||||
|
set(CEF_CMAKE_LOCALES_OUTPUT_DIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/cef/locales")
|
||||||
|
set(CEF_CMAKE_RESOURCES_OUTPUT_DIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/cef/resources")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT AYA_ARCH_ARM64)
|
||||||
|
add_compile_options(-mssse3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ADDRESS_SANITIZER AND NOT AYA_ARCH_ARM64)
|
||||||
|
add_compile_options(-fsanitize=address)
|
||||||
|
add_link_options(-fsanitize=address)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ENABLE_VOICE_CHAT)
|
||||||
|
add_compile_definitions(ENABLE_VOICE_CHAT=1)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Disable some annoying warnings
|
||||||
|
add_compile_options(-w)
|
||||||
|
add_compile_options(-Wnonportable-include-path)
|
||||||
|
add_compile_options(-Wundefined-var-template)
|
||||||
|
add_compile_options(-Wdelete-abstract-non-virtual-dtor)
|
||||||
|
|
||||||
|
########## Windows/MSVC Configuration ##########
|
||||||
|
|
||||||
|
add_compile_definitions(
|
||||||
|
_HAS_AUTO_PTR_ETC=1
|
||||||
|
_ENFORCE_MATCHING_ALLOCATORS=0
|
||||||
|
WIN32_LEAN_AND_MEAN=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
set(CMAKE_PDB_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
|
||||||
|
set(CMAKE_COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
|
||||||
|
|
||||||
|
include_directories($ENV{INCLUDE})
|
||||||
|
|
||||||
|
if(ATL_PATH)
|
||||||
|
include_directories("${ATL_PATH}/include")
|
||||||
|
link_directories("${ATL_PATH}/lib/x64")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Windows 10
|
||||||
|
# https://learn.microsoft.com/en-us/cpp/porting/modifying-winver-and-win32-winnt?view=msvc-170
|
||||||
|
add_compile_definitions(
|
||||||
|
WINVER=0x0A00
|
||||||
|
_WIN32_WINNT=0x0A00
|
||||||
|
_WINSOCKAPI_=1 # Prevents winsock.h from defining macros that conflict with winsock2.h
|
||||||
|
)
|
||||||
|
|
||||||
|
# Control Flow Guard
|
||||||
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /guard:cf")
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /guard:cf")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /guard:cf")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
########## MacOS Configuration ##########
|
||||||
|
|
||||||
|
if(AYA_OS_MACOS)
|
||||||
|
add_compile_options(-D_GLIBCXX_USE_DEPRECATED)
|
||||||
|
add_compile_definitions(
|
||||||
|
__APPLE__
|
||||||
|
_LIBCPP_ENABLE_CXX17_REMOVED_AUTO_PTR
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
########## Dependencies ##########
|
||||||
|
|
||||||
|
# vcpkg dependencies
|
||||||
|
find_package(assimp CONFIG REQUIRED)
|
||||||
|
find_package(bgfx CONFIG REQUIRED)
|
||||||
|
find_package(Boost COMPONENTS filesystem system iostreams thread exception program_options url chrono CONFIG REQUIRED)
|
||||||
|
set(CGAL_DO_NOT_WARN_ABOUT_CMAKE_BUILD_TYPE 1)
|
||||||
|
find_package(CGAL CONFIG REQUIRED)
|
||||||
|
find_package(CURL REQUIRED)
|
||||||
|
find_package(Freetype REQUIRED)
|
||||||
|
find_package(imgui CONFIG REQUIRED)
|
||||||
|
find_package(implot CONFIG REQUIRED)
|
||||||
|
find_package(JPEG REQUIRED)
|
||||||
|
find_package(LibArchive REQUIRED)
|
||||||
|
find_package(lz4 CONFIG REQUIRED)
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
find_package(pugixml CONFIG REQUIRED)
|
||||||
|
find_package(PNG REQUIRED)
|
||||||
|
find_package(SDL3 CONFIG REQUIRED)
|
||||||
|
find_package(Vulkan REQUIRED)
|
||||||
|
find_package(ZLIB REQUIRED)
|
||||||
|
find_package(zstd REQUIRED)
|
||||||
|
|
||||||
|
# libjxl hack
|
||||||
|
find_path(JXL_INCLUDE_DIR jxl/decode.h)
|
||||||
|
find_library(JXL_LIBRARY NAMES jxl)
|
||||||
|
find_library(JXL_CMS_LIBRARY NAMES jxl_cms)
|
||||||
|
find_library(JXL_THREADS_LIBRARY NAMES jxl_threads)
|
||||||
|
find_library(BROTLIDEC_LIBRARY NAMES brotlidec)
|
||||||
|
find_library(BROTLIENC_LIBRARY NAMES brotlienc)
|
||||||
|
find_library(BROTLICOMMON_LIBRARY NAMES brotlicommon)
|
||||||
|
find_library(LCMS2_LIBRARY NAMES lcms2)
|
||||||
|
find_library(HWY_LIBRARY NAMES hwy)
|
||||||
|
|
||||||
|
if(NOT JXL_LIBRARY OR NOT JXL_CMS_LIBRARY OR NOT JXL_THREADS_LIBRARY)
|
||||||
|
message(FATAL_ERROR "JPEG XL libraries not found")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include_directories(${JXL_INCLUDE_DIR})
|
||||||
|
|
||||||
|
# Boost
|
||||||
|
add_compile_definitions(BOOST_THREAD_PROVIDES_FUTURE)
|
||||||
|
add_compile_definitions(BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION)
|
||||||
|
|
||||||
|
find_package(Boost CONFIG REQUIRED COMPONENTS
|
||||||
|
system
|
||||||
|
exception
|
||||||
|
container
|
||||||
|
thread
|
||||||
|
filesystem
|
||||||
|
iostreams
|
||||||
|
math
|
||||||
|
program_options
|
||||||
|
url
|
||||||
|
chrono
|
||||||
|
)
|
||||||
|
|
||||||
|
# Discord Rich Presence
|
||||||
|
if(ENABLE_DISCORD_INTEGRATION)
|
||||||
|
find_path(DISCORD_RPC_INCLUDE_DIRS "discord_rpc.h")
|
||||||
|
find_library(DISCORD_RPC_LIBRARY discord-rpc REQUIRED)
|
||||||
|
add_compile_definitions(ENABLE_DISCORD_INTEGRATION)
|
||||||
|
include_directories(${DISCORD_RPC_INCLUDE_DIRS})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Voice Chat
|
||||||
|
if(ENABLE_VOICE_CHAT)
|
||||||
|
find_package(portaudio CONFIG REQUIRED)
|
||||||
|
find_path(OPUS_INCLUDE_DIR opus.h PATH_SUFFIXES opus)
|
||||||
|
find_library(OPUS_LIBRARY opus)
|
||||||
|
include_directories(${OPUS_INCLUDE_DIR})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Chromium Embedded Framework (CEF) integration
|
||||||
|
if(ENABLE_CHROMIUM_FRAMES)
|
||||||
|
if(USE_CEF_FIND_PACKAGE)
|
||||||
|
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} $ENV{CEF_ROOT}/cmake)
|
||||||
|
set(CEF_ROOT $ENV{CEF_ROOT})
|
||||||
|
find_package(CEF REQUIRED)
|
||||||
|
include_directories(${CEF_INCLUDE_PATH})
|
||||||
|
else()
|
||||||
|
include(${THIRD_PARTY_DIR}/cef/cmake/cef_cmake.cmake)
|
||||||
|
add_subdirectory(${THIRD_PARTY_DIR}/cef)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT USE_CEF_FIND_PACKAGE)
|
||||||
|
set(CEF_WRAPPER cefdll_wrapper)
|
||||||
|
else()
|
||||||
|
set(CEF_WRAPPER $ENV{CEF_ROOT}/build/libcef_dll_wrapper/libcef_dll_wrapper.a)
|
||||||
|
if(CMAKE_BUILD_TYPE EQUAL "Debug")
|
||||||
|
set(CEF_LIB ${CEF_LIB_DEBUG})
|
||||||
|
else()
|
||||||
|
set(CEF_LIB ${CEF_LIB_RELEASE})
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
set(CEF_LIBRARIES ${CEF_WRAPPER} ${CEF_LIB})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# FMOD
|
||||||
|
include_directories(${THIRD_PARTY_DIR}/fmod/include)
|
||||||
|
|
||||||
|
option(USE_CEF_FIND_PACKAGE OFF)
|
||||||
|
|
||||||
|
if(AYA_ARCH_X64)
|
||||||
|
set(FMOD_ARCHITECTURE x64)
|
||||||
|
elseif(AYA_ARCH_ARM64)
|
||||||
|
set(FMOD_ARCHITECTURE arm64)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(AYA_OS_WINDOWS)
|
||||||
|
link_directories(${THIRD_PARTY_DIR}/fmod/lib/windows/${FMOD_ARCHITECTURE})
|
||||||
|
set(RUNTIME_FILES ${THIRD_PARTY_DIR}/fmod/lib/windows/${FMOD_ARCHITECTURE}/fmod.dll)
|
||||||
|
elseif(AYA_OS_MACOS)
|
||||||
|
link_directories(${THIRD_PARTY_DIR}/fmod/lib/mac)
|
||||||
|
set(RUNTIME_FILES ${THIRD_PARTY_DIR}/fmod/lib/mac/libfmod.dylib)
|
||||||
|
|
||||||
|
add_library(fmod SHARED IMPORTED)
|
||||||
|
set_property(TARGET fmod PROPERTY IMPORTED_LOCATION ${THIRD_PARTY_DIR}/fmod/lib/mac/libfmod.dylib)
|
||||||
|
elseif(AYA_OS_LINUX)
|
||||||
|
link_directories(${THIRD_PARTY_DIR}/fmod/lib/linux/${FMOD_ARCHITECTURE})
|
||||||
|
set(RUNTIME_FILES ${THIRD_PARTY_DIR}/fmod/lib/linux/${FMOD_ARCHITECTURE}/libfmod.so.13)
|
||||||
|
|
||||||
|
add_library(fmod SHARED IMPORTED)
|
||||||
|
set_property(TARGET fmod PROPERTY IMPORTED_LOCATION ${THIRD_PARTY_DIR}/fmod/lib/linux/${FMOD_ARCHITECTURE}/libfmod.so)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Qt
|
||||||
|
# Arch: pacman -S qt6-base qt6-webengine
|
||||||
|
# Ubuntu: apt install qt6-base-dev qt6-webengine-dev qt6-webengine-dev-tools libqt6webenginecore6-bin
|
||||||
|
find_package(Qt6 COMPONENTS Core Widgets Concurrent WebEngineWidgets Gui GuiPrivate Designer Xml REQUIRED)
|
||||||
|
|
||||||
|
# QtitanRibbon
|
||||||
|
if(NOT AYA_OS_ANDROID)
|
||||||
|
add_subdirectory(${THIRD_PARTY_DIR}/QtitanRibbon)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Other dependencies
|
||||||
|
add_subdirectory(${THIRD_PARTY_DIR}/BulletPhysics)
|
||||||
|
add_subdirectory(${THIRD_PARTY_DIR}/RakNet)
|
||||||
|
|
||||||
|
########## Aya Project ##########
|
||||||
|
|
||||||
|
include_directories(${CLIENT_DIR}/common)
|
||||||
|
|
||||||
|
add_subdirectory(${ENGINE_DIR}/3d)
|
||||||
|
add_subdirectory(${ENGINE_DIR}/app)
|
||||||
|
add_subdirectory(${ENGINE_DIR}/core)
|
||||||
|
add_subdirectory(${ENGINE_DIR}/network)
|
||||||
|
add_subdirectory(${ENGINE_DIR}/gfx)
|
||||||
|
|
||||||
|
if(NOT AYA_OS_ANDROID)
|
||||||
|
if(AYA_OS_WINDOWS)
|
||||||
|
include(${CLIENT_DIR}/windeployqt.cmake)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_subdirectory(${TOOLS_DIR}/core-script-compiler)
|
||||||
|
add_subdirectory(${CLIENT_DIR}/bootstrapper)
|
||||||
|
|
||||||
|
add_subdirectory(${CLIENT_DIR}/player)
|
||||||
|
add_subdirectory(${CLIENT_DIR}/server)
|
||||||
|
add_subdirectory(${CLIENT_DIR}/studio)
|
||||||
|
else()
|
||||||
|
add_subdirectory(${CLIENT_DIR}/mobile)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ENABLE_CHROMIUM_FRAMES)
|
||||||
|
add_subdirectory(${TOOLS_DIR}/cef-subprocess)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(AYA_OS_WINDOWS)
|
||||||
|
add_subdirectory(${TOOLS_DIR}/thumbnail-helper)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(CLIENT_THIRD_PARTY_LIBRARIES
|
||||||
|
assimp::assimp
|
||||||
|
bgfx::bx bgfx::bgfx bgfx::bimg bgfx::bimg_decode
|
||||||
|
Boost::chrono
|
||||||
|
Boost::container
|
||||||
|
Boost::exception
|
||||||
|
Boost::filesystem
|
||||||
|
Boost::iostreams
|
||||||
|
Boost::math
|
||||||
|
Boost::program_options
|
||||||
|
Boost::system
|
||||||
|
Boost::thread
|
||||||
|
Boost::url
|
||||||
|
CGAL::CGAL
|
||||||
|
CURL::libcurl
|
||||||
|
fmod
|
||||||
|
Freetype::Freetype
|
||||||
|
imgui::imgui
|
||||||
|
implot::implot
|
||||||
|
JPEG::JPEG
|
||||||
|
lz4::lz4
|
||||||
|
OpenSSL::SSL OpenSSL::Crypto
|
||||||
|
PNG::PNG
|
||||||
|
pugixml::pugixml
|
||||||
|
SDL3::SDL3
|
||||||
|
Vulkan::Vulkan
|
||||||
|
ZLIB::ZLIB
|
||||||
|
$<IF:$<TARGET_EXISTS:zstd::libzstd_shared>,zstd::libzstd_shared,zstd::libzstd_static>
|
||||||
|
${JXL_LIBRARY}
|
||||||
|
${JXL_CMS_LIBRARY}
|
||||||
|
${JXL_THREADS_LIBRARY}
|
||||||
|
${BROTLIDEC_LIBRARY}
|
||||||
|
${BROTLIENC_LIBRARY}
|
||||||
|
${BROTLICOMMON_LIBRARY}
|
||||||
|
${LCMS2_LIBRARY}
|
||||||
|
${HWY_LIBRARY}
|
||||||
|
)
|
||||||
|
|
||||||
|
set(BOOTSTRAPPER_THIRD_PARTY_LIBRARIES
|
||||||
|
Boost::program_options
|
||||||
|
CURL::libcurl
|
||||||
|
LibArchive::LibArchive
|
||||||
|
ZLIB::ZLIB
|
||||||
|
$<IF:$<TARGET_EXISTS:zstd::libzstd_shared>,zstd::libzstd_shared,zstd::libzstd_static>
|
||||||
|
)
|
||||||
|
|
||||||
|
if(ENABLE_VOICE_CHAT)
|
||||||
|
set(CLIENT_THIRD_PARTY_LIBRARIES ${CLIENT_THIRD_PARTY_LIBRARIES} ${OPUS_LIBRARY})
|
||||||
|
set(CLIENT_THIRD_PARTY_LIBRARIES ${CLIENT_THIRD_PARTY_LIBRARIES} portaudio_static)
|
||||||
|
set(CLIENT_THIRD_PARTY_LIBRARIES ${CLIENT_THIRD_PARTY_LIBRARIES} portaudio)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
# annoying hack for PortAudio shittiness
|
||||||
|
add_link_options(/FORCE:MULTIPLE)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(VCPKG_LIBRARY_LINKAGE STREQUAL "static" OR AYA_OS_LINUX OR AYA_OS_MACOS)
|
||||||
|
set(CLIENT_THIRD_PARTY_LIBRARIES ${CLIENT_THIRD_PARTY_LIBRARIES} pugixml::pugixml)
|
||||||
|
else()
|
||||||
|
set(CLIENT_THIRD_PARTY_LIBRARIES ${CLIENT_THIRD_PARTY_LIBRARIES} pugixml::shared)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(MSVC OR AYA_OS_WINDOWS)
|
||||||
|
set(MSVC_LIBRARIES
|
||||||
|
pdh
|
||||||
|
Ws2_32
|
||||||
|
wininet
|
||||||
|
opengl32
|
||||||
|
Version
|
||||||
|
bcrypt
|
||||||
|
Iphlpapi
|
||||||
|
d3d11
|
||||||
|
dxgi
|
||||||
|
Winmm
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ENABLE_CHROMIUM_FRAMES AND NOT AYA_OS_MACOS)
|
||||||
|
add_dependencies(Player CefSubprocess)
|
||||||
|
add_dependencies(Studio CefSubprocess)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT AYA_TEST_BUILD)
|
||||||
|
add_dependencies(Player CompileShaders CompileCoreScripts)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(QT_LIBRARIES
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::Widgets
|
||||||
|
Qt6::Concurrent
|
||||||
|
Qt6::Gui
|
||||||
|
Qt6::GuiPrivate
|
||||||
|
Qt6::WebEngineWidgets
|
||||||
|
Qt6::Xml
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(Player ${CLIENT_THIRD_PARTY_LIBRARIES} ${MSVC_LIBRARIES} ${CEF_LIBRARIES} ${QT_LIBRARIES} ${DISCORD_RPC_LIBRARY})
|
||||||
|
target_link_libraries(Studio ${CLIENT_THIRD_PARTY_LIBRARIES} ${MSVC_LIBRARIES} ${CEF_LIBRARIES} ${QT_LIBRARIES})
|
||||||
|
target_link_libraries(Server ${CLIENT_THIRD_PARTY_LIBRARIES} ${MSVC_LIBRARIES} ${QT_LIBRARIES})
|
||||||
|
target_link_libraries(CoreScriptCompiler ${CLIENT_THIRD_PARTY_LIBRARIES} ${MSVC_LIBRARIES})
|
||||||
|
target_link_libraries(Bootstrapper ${BOOTSTRAPPER_THIRD_PARTY_LIBRARIES} ${MSVC_LIBRARIES} ${QT_LIBRARIES})
|
||||||
|
|
||||||
|
message(NOTICE "")
|
||||||
|
message(NOTICE "-- To compile CoreScripts for Player, run target 'CompileCoreScripts'")
|
||||||
|
message(NOTICE "-- To compile shaders, run target 'CompileShaders'")
|
||||||
|
message(NOTICE "-- All build artifacts are available in '${CMAKE_RUNTIME_OUTPUT_DIRECTORY}'")
|
||||||
|
message(NOTICE "")
|
||||||
45
CMakePresets.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"version": 8,
|
||||||
|
"configurePresets": [
|
||||||
|
{
|
||||||
|
"name": "ci",
|
||||||
|
"generator": "Ninja",
|
||||||
|
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake",
|
||||||
|
"CMAKE_BUILD_TYPE": "MinSizeRel",
|
||||||
|
"CMAKE_C_COMPILER": "/usr/local/bin/clang",
|
||||||
|
"CMAKE_CXX_COMPILER": "/usr/local/bin/clang++",
|
||||||
|
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
|
||||||
|
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
|
||||||
|
"CI": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ci-cross-arm",
|
||||||
|
"generator": "Ninja",
|
||||||
|
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake",
|
||||||
|
"CMAKE_SYSTEM_PROCESOR": "aarch64",
|
||||||
|
"CMAKE_BUILD_TYPE": "MinSizeRel",
|
||||||
|
"CMAKE_C_COMPILER": "/usr/bin/aarch64-linux-gnu-gcc",
|
||||||
|
"CMAKE_CXX_COMPILER": "/usr/bin/aarch64-linux-gnu-g++",
|
||||||
|
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
|
||||||
|
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
|
||||||
|
"CI": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buildPresets": [
|
||||||
|
{
|
||||||
|
"name": "ci",
|
||||||
|
"configurePreset": "ci"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ci-cross-arm",
|
||||||
|
"configurePreset": "ci-cross-arm"
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
78
LICENSE.txt
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
|
||||||
|
Aya Licensing
|
||||||
|
=============
|
||||||
|
|
||||||
|
Due to the fact that Aya is a fork of previously leaked proprietary Roblox
|
||||||
|
source code, it is not possible to apply a traditional license to Aya.
|
||||||
|
Therefore, all authors of custom non-proprietary code and related resources
|
||||||
|
used in Aya hereby irrevocably waive all copyright and related rights to
|
||||||
|
their contributions, to the maximum extent allowed by law.
|
||||||
|
|
||||||
|
Please note that this waiver of copyright applies only to the non
|
||||||
|
proprietary items found in Aya. Any proprietary code that may still exist
|
||||||
|
in Aya remains subject to the copyright and licensing conditions imposed by
|
||||||
|
its original authors or owners; in particular, Roblox Corporation.
|
||||||
|
|
||||||
|
Aya uses a variety of third party dependencies. A list of all the third
|
||||||
|
party dependencies used in Aya alongside a link to their respective license
|
||||||
|
are available below:
|
||||||
|
|
||||||
|
- assimp: <https://github.com/assimp/assimp/blob/master/LICENSE>
|
||||||
|
- BGFX: <https://github.com/bkaradzic/bgfx/blob/master/LICENSE>
|
||||||
|
- Boost: <https://www.boost.org/users/license.html>
|
||||||
|
- Bullet Physics SDK: <https://github.com/bulletphysics/bullet3/blob/master/LICENSE.txt>
|
||||||
|
- cURL: <https://curl.se/docs/copyright.html>
|
||||||
|
- CGAL: <https://www.cgal.org/license.html>
|
||||||
|
- Discord RPC: <https://github.com/discord/discord-rpc/blob/master/LICENSE>
|
||||||
|
- GLAD: <https://github.com/Dav1dde/glad/blob/glad2/LICENSE>
|
||||||
|
- libjpeg-turbo: <https://github.com/libjpeg-turbo/libjpeg-turbo/blob/main/LICENSE.md>
|
||||||
|
- FreeType: <https://freetype.org/license.html>
|
||||||
|
- ImGui: <https://www.dearimgui.com/licenses/>
|
||||||
|
- ImPlot: <https://github.com/epezent/implot/blob/master/LICENSE>
|
||||||
|
- libarchive: <https://raw.githubusercontent.com/libarchive/libarchive/master/COPYING>
|
||||||
|
- libjxl: <https://github.com/libjxl/libjxl/blob/main/LICENSE>
|
||||||
|
- libpng: <http://www.libpng.org/pub/png/src/libpng-LICENSE.txt>
|
||||||
|
- lz4: <https://github.com/lz4/lz4/blob/dev/LICENSE>
|
||||||
|
- microprofile: <https://github.com/jonasmr/microprofile/blob/master/LICENSE>
|
||||||
|
- OpenSSL: <https://www.openssl.org/source/apache-license-2.0.txt>
|
||||||
|
- opus: <https://opus-codec.org/license/>
|
||||||
|
- PortAudio: <https://files.portaudio.com/docs/v19-doxydocs/License.html>
|
||||||
|
- pugixml: <https://pugixml.org/license.html>
|
||||||
|
- Qt 6: <https://doc.qt.io/qt-6/lgpl.html>
|
||||||
|
- RakNet: <https://github.com/facebookarchive/RakNet/blob/master/LICENSE>
|
||||||
|
- RapidJSON: <https://github.com/Tencent/rapidjson/blob/master/license.txt>
|
||||||
|
- SDL3: <https://www.libsdl.org/license.php>
|
||||||
|
- xxHash: <https://github.com/Cyan4973/xxHash/blob/dev/LICENSE>
|
||||||
|
- zlib: <https://www.zlib.net/zlib_license.html>
|
||||||
|
- zstd: <https://github.com/facebook/zstd/blob/dev/LICENSE>
|
||||||
|
|
||||||
|
The authors of Aya have made a concerted effort to rid the original
|
||||||
|
codebase of any and all proprietary or otherwise closed source
|
||||||
|
dependencies, and to replace them with free and open source alternatives.
|
||||||
|
The only proprietary or non-free items that still remain in this repository
|
||||||
|
is code and artwork created by Roblox Corporation, which themselves have
|
||||||
|
undergone substantial modification to the extent that they no longer
|
||||||
|
resemble their original versions.
|
||||||
|
|
||||||
|
It is the duty of anyone who uses Aya to be fully aware of the legal
|
||||||
|
circumstances surrounding its use. The original authors of Aya expressly
|
||||||
|
disclaim all liability for any and all uses of Aya, including, without
|
||||||
|
limitation, any direct, indirect, incidental, special, consequential, or
|
||||||
|
exemplary damages, even if advised of the possibility of such damages. The
|
||||||
|
original authors of Aya further disclaim any and all responsibility for any
|
||||||
|
third party's use or misuse of Aya.
|
||||||
|
|
||||||
|
THE MATERIALS IN THIS REPOSITORY, INCLUDING ALL SOURCE CODE AND OTHER
|
||||||
|
RELATED ITEMS, SUCH AS DOCUMENTATION, ARE PROVIDED "AS IS", WITHOUT
|
||||||
|
WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||||
|
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR ANY OTHER COPYRIGHT
|
||||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
MATERIALS.
|
||||||
|
|
||||||
|
In addition to the legal responsibilities outlined above, we strongly
|
||||||
|
encourage all users of Aya to use this software in a responsible and
|
||||||
|
ethical manner. Please respect the rights and dignity of others, and use
|
||||||
|
Aya only in a way that contributes positively to the world.
|
||||||
98
README.txt
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
|
||||||
|
Aya
|
||||||
|
===
|
||||||
|
|
||||||
|
0. Overview
|
||||||
|
1. Documentation
|
||||||
|
1.1. Overall documentation
|
||||||
|
1.2. Project structure
|
||||||
|
2. Noteworthy changes
|
||||||
|
|
||||||
|
0. Overview
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Aya is a robust, customizable, and otherwise heavily modified fork of the
|
||||||
|
Roblox client codebase which was leaked to the public internet around March
|
||||||
|
2016. Relative to the original codebase, Aya contains numerous improvements,
|
||||||
|
enhancements, and other features, all of which are listed in the CHANGELOG.
|
||||||
|
|
||||||
|
Aya was created with the aspiration to foster positive change and further
|
||||||
|
innovation in the legacy Roblox community. It is our hope that every use of
|
||||||
|
Aya aligns with this vision.
|
||||||
|
|
||||||
|
Please read all the provided documentation files to get a full grasp on how
|
||||||
|
to use Aya. This file contains a glossary of all relevant information,
|
||||||
|
whereas INSTALL contains information how to fully set up and use Aya.
|
||||||
|
You should definitely read LICENSE, as it describes the relevant terms for
|
||||||
|
using Aya, as well as key legal information you need to know if you choose
|
||||||
|
to use Aya.
|
||||||
|
|
||||||
|
1. Documentation
|
||||||
|
----------------
|
||||||
|
|
||||||
|
1.1. Overall documentation
|
||||||
|
|
||||||
|
README.txt This file
|
||||||
|
docs/API.md Aya custom API reference
|
||||||
|
docs/INSTALL.md Installation/compile instructions for Aya
|
||||||
|
docs/CHANGELOG.md Full list of changes from the original 2016
|
||||||
|
Roblox source code
|
||||||
|
|
||||||
|
1.2. Project structure
|
||||||
|
|
||||||
|
client Implements the Aya game, delivered to end-users
|
||||||
|
client/app Universal Svelte app UI for Player/Studio/Server
|
||||||
|
client/core Foundational code used across the client apps
|
||||||
|
client/player Aya Player, used to connect to multiplayer games
|
||||||
|
client/studio Aya Studio, used to edit and test levels
|
||||||
|
client/server Aya Server, used to host multiplayer game sessions
|
||||||
|
client/web-helper Aya CEF integration
|
||||||
|
client/thumbnail-helper Call with regsvr32 on Windows for .ayal thumbnails
|
||||||
|
in the Windows File Explorer
|
||||||
|
client/updater Aya auto-updater for instances
|
||||||
|
|
||||||
|
engine The underlying engine that the client applications
|
||||||
|
rely on. This is a heavily modified version of the
|
||||||
|
Roblox game engine source code leak, dated 3/16/2016
|
||||||
|
engine/app The heart of the engine
|
||||||
|
engine/core Fundamental components used throughout the engine
|
||||||
|
engine/gfx 3D rendering
|
||||||
|
engine/network Multiplayer component of the game engine
|
||||||
|
|
||||||
|
docs Documentation on how to use and install Aya
|
||||||
|
resources Static resources such as images that are used in Aya
|
||||||
|
|
||||||
|
third-party Third party dependencies accessible as Git submodules
|
||||||
|
|
||||||
|
|
||||||
|
1. Noteworthy changes
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Aya contains many significant changes relative to the original codebase.
|
||||||
|
Here is a list of some of the more noteworthy ones that are important to
|
||||||
|
know when using Aya.
|
||||||
|
|
||||||
|
- Most of Roblox's anti-cheat measures have been removed or stripped
|
||||||
|
out of the codebase entirely, including:
|
||||||
|
|
||||||
|
* The real-time program memory checker (NetPmc) as well as the
|
||||||
|
golden hash checker. The justification for this is because it is
|
||||||
|
too difficult to port to non-x86 Windows systems as a lot of the
|
||||||
|
functionality relies on fundamental properties of the Microsoft
|
||||||
|
Visual C++ compiler and the PE file structure.
|
||||||
|
|
||||||
|
* VMProtect integration has been removed entirely. Unlike other
|
||||||
|
proprietary libraries, there is no alternative put in its place.
|
||||||
|
However, you may want to tackle obfuscating code during the
|
||||||
|
compilation stage with projects such as obfuscated-llvm so that
|
||||||
|
the control flow of the application is obfuscated to the point that
|
||||||
|
it is near-impossible for malicious actors to exploit.
|
||||||
|
|
||||||
|
- All proprietary or otherwise closed-source dependencies have been
|
||||||
|
removed entirely and replaced with open source alternatives.
|
||||||
|
|
||||||
|
- The client applications have been almost entirely rewritten with the
|
||||||
|
goal of being as lightweight as possible as well as having equivalent
|
||||||
|
functionality across all platforms (in particular, Windows and Linux).
|
||||||
|
|
||||||
|
- All dependencies have been updated to their newest versions.
|
||||||
75
client/app/config.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
import autoprefixer from "autoprefixer";
|
||||||
|
import tailwindcss from "tailwindcss";
|
||||||
|
import cssnano from "cssnano";
|
||||||
|
import defaultTheme from "tailwindcss/defaultTheme";
|
||||||
|
import forms from "@tailwindcss/forms";
|
||||||
|
import typography from "@tailwindcss/typography";
|
||||||
|
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: path.resolve(__dirname),
|
||||||
|
base: "./",
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [
|
||||||
|
tailwindcss(
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
{
|
||||||
|
content: [path.resolve(__dirname, "src") + "/**/*.{html,js,svelte,ts}"],
|
||||||
|
darkMode: ["selector", '[data-mode="dark"]'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["'Source Sans 3'", ...defaultTheme.fontFamily.sans],
|
||||||
|
mono: ["'Source Code Pro'", ...defaultTheme.fontFamily.mono],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
aya: {
|
||||||
|
100: "#e7d9fd",
|
||||||
|
200: "#ceb3fb",
|
||||||
|
300: "#b68cf9",
|
||||||
|
400: "#9d66f7",
|
||||||
|
500: "#8540f5",
|
||||||
|
600: "#6a33c4",
|
||||||
|
700: "#502693",
|
||||||
|
800: "#351a62",
|
||||||
|
900: "#1b0d31",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
extend: {
|
||||||
|
backgroundColor: ["active", "focus"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [forms, typography],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
autoprefixer(),
|
||||||
|
cssnano(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, "../common/content/ui"),
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: undefined,
|
||||||
|
inlineDynamicImports: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [svelte()],
|
||||||
|
});
|
||||||
252
client/app/css/app.css
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
@import url("./fonts.css");
|
||||||
|
@import url("./icons.css");
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind utilities;
|
||||||
|
@tailwind components;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation-duration: 100ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.aya-anim-pop:active:hover:not(:disabled),
|
||||||
|
.aya-anim-pop:active:focus:not(:disabled) {
|
||||||
|
transform: scale(0.97);
|
||||||
|
animation: button-pop 0s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-page-btn {
|
||||||
|
@apply aya-anim-pop flex cursor-default items-center justify-center duration-100;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition-property: border, border-top, background-color, border-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-page-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1) !important;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-page-btn-selected {
|
||||||
|
@apply aya-page-btn;
|
||||||
|
background-color: rgba(255, 255, 255);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.065) !important;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.122) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-page-btn-selected:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.065) !important;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.105) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-mode="dark"] {
|
||||||
|
.aya-page-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid transparent !important;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.04) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-page-btn-selected {
|
||||||
|
background-color: rgba(255, 255, 255, 0.045);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.03) !important;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.07) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-page-btn-selected:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.075);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1) !important;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.025) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hue-rotate {
|
||||||
|
0% {
|
||||||
|
filter: hue-rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
filter: hue-rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-bg-rainbow {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-bg-rainbow::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(45deg, theme("colors.indigo.500") 0%, theme("colors.red.500") 100%);
|
||||||
|
animation: hue-rotate 20s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-bg-rainbow > * {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.aya-anim-pop:active:hover:not(:disabled),
|
||||||
|
.aya-anim-pop:active:focus:not(:disabled) {
|
||||||
|
animation: button-pop 0.25s ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-btn-sm {
|
||||||
|
@apply !h-8 !min-h-8 !px-3 !text-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-btn-xs {
|
||||||
|
@apply !h-6 !min-h-6 !px-2 !text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-btn-lg {
|
||||||
|
@apply !h-[3rem] !min-h-[3rem] !px-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-btn-green {
|
||||||
|
@apply !bg-green-600/85 hover:!bg-green-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-btn-link-sm {
|
||||||
|
@apply !px-1 !py-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-btn-alt {
|
||||||
|
@apply !bg-aya-700 hover:!bg-aya-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AppLayout */
|
||||||
|
.aya-nav-link {
|
||||||
|
@apply aya-anim-pop me-1 rounded-lg px-3 py-2 text-white text-opacity-50 transition duration-200 hover:bg-white/10 hover:text-opacity-100 hover:shadow-sm focus:text-white active:bg-white/20 dark:hover:bg-black/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-nav-link:not(.aya-nav-link-selected) {
|
||||||
|
@apply focus:bg-aya-400/75 dark:focus:bg-aya-600/75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-nav-link-sm {
|
||||||
|
@apply aya-anim-pop me-1 rounded-lg px-2 py-1 text-white text-opacity-50 transition duration-200 hover:bg-white/5 hover:text-opacity-100 focus:bg-white/10 focus:text-white active:bg-white/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-nav-link-selected {
|
||||||
|
@apply bg-aya-600/75 text-opacity-100 hover:bg-aya-600 dark:bg-aya-600/50 dark:hover:bg-aya-600/75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-nav-link-sm.aya-nav-link-selected {
|
||||||
|
@apply bg-white/10 text-opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-footer-link {
|
||||||
|
@apply aya-anim-pop flex-grow rounded-lg bg-transparent px-2 py-2 text-center text-xl opacity-35 transition duration-200 hover:bg-white hover:bg-opacity-10 hover:opacity-100 hover:shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-games-genre-list {
|
||||||
|
@apply aya-anim-pop ms-3 rounded-md px-2 py-0.5 text-start text-neutral-600 transition duration-200 hover:bg-black/10 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-white/10 dark:hover:text-neutral-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-games-sort-list {
|
||||||
|
@apply aya-anim-pop ms-3 rounded-md px-2 py-0.5 text-start text-neutral-600 transition duration-200 hover:bg-black/10 dark:text-neutral-400 dark:hover:bg-white/10 dark:hover:text-neutral-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-limited-label,
|
||||||
|
.aya-limited-unique-label,
|
||||||
|
.aya-limited-label-lg,
|
||||||
|
.aya-limited-unique-label-lg {
|
||||||
|
background-image: url("../img/itemlabels.svg");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: auto auto;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-limited-label,
|
||||||
|
.aya-limited-unique-label {
|
||||||
|
width: 70px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-limited-unique-label {
|
||||||
|
background-position: 0 -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-limited-label {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-limited-label-lg,
|
||||||
|
.aya-limited-unique-label-lg {
|
||||||
|
width: 105px;
|
||||||
|
height: 22px;
|
||||||
|
background-size: 105px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-limited-unique-label-lg {
|
||||||
|
background-position: 0 -26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-limited-label-lg {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aya-character-body-selector div {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* character body */
|
||||||
|
/* ugly! */
|
||||||
|
.body-head {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: block;
|
||||||
|
margin-left: 72px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-torso {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-left-arm,
|
||||||
|
.body-right-arm {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-left-leg,
|
||||||
|
.body-right-leg {
|
||||||
|
display: inline-block;
|
||||||
|
width: 49px !important;
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-left-arm,
|
||||||
|
.body-right-arm {
|
||||||
|
width: 49px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-left-arm,
|
||||||
|
.body-right-arm,
|
||||||
|
.body-left-leg,
|
||||||
|
.body-right-leg {
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-left-leg {
|
||||||
|
margin-left: 52px;
|
||||||
|
}
|
||||||
|
}
|
||||||
253
client/app/css/fonts.css
Normal file
71
client/app/css/icons.css
Normal file
25
client/app/index.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US" class="bg-white dark:bg-neutral-900">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<link rel="icon" href="./img/aya.png" />
|
||||||
|
<title>Aya</title>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script type="module" src="./main.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
document.documentElement.setAttribute("data-mode", "dark");
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute("data-mode", "light");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="font-sans antialiased">
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
client/app/main.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import "./css/app.css"
|
||||||
|
import App from "./src/App.svelte"
|
||||||
|
import { mount } from "svelte";
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
const mode = params.get("mode") || "player" // can be of [player, studio, server] to access 3 different UIs
|
||||||
|
|
||||||
|
const app = mount(App, {
|
||||||
|
target: document.getElementById("app"),
|
||||||
|
props: { mode }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
||||||
BIN
client/app/public/img/aya-server-error.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
client/app/public/img/aya-server-progress.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
client/app/public/img/aya-server.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
client/app/public/img/aya-studio.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
client/app/public/img/aya.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
client/app/public/img/devs/3d_glasses.png
Normal file
|
After Width: | Height: | Size: 471 B |
BIN
client/app/public/img/devs/angel.png
Normal file
|
After Width: | Height: | Size: 636 B |
BIN
client/app/public/img/devs/bomb.png
Normal file
|
After Width: | Height: | Size: 740 B |
BIN
client/app/public/img/devs/bricks.png
Normal file
|
After Width: | Height: | Size: 769 B |
BIN
client/app/public/img/devs/cat.png
Normal file
|
After Width: | Height: | Size: 809 B |
BIN
client/app/public/img/devs/heart.png
Normal file
|
After Width: | Height: | Size: 570 B |
BIN
client/app/public/img/devs/weed.png
Normal file
|
After Width: | Height: | Size: 392 B |
BIN
client/app/public/img/icons/avatar.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
client/app/public/img/icons/extensions.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
client/app/public/img/icons/favorites.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
client/app/public/img/icons/host.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
client/app/public/img/icons/item.png
Normal file
|
After Width: | Height: | Size: 613 B |
BIN
client/app/public/img/icons/level.png
Normal file
|
After Width: | Height: | Size: 928 B |
BIN
client/app/public/img/icons/levels.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
client/app/public/img/icons/model.png
Normal file
|
After Width: | Height: | Size: 796 B |
BIN
client/app/public/img/icons/packages.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
client/app/public/img/icons/play.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
client/app/public/img/icons/server.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
client/app/public/img/icons/studio.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
client/app/public/img/mega.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
client/app/public/img/pbs.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
24
client/app/src/App.svelte
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import PlayerApp from "@/Player.svelte"
|
||||||
|
import StudioApp from "@/Studio.svelte"
|
||||||
|
import ServerApp from "@/Server.svelte"
|
||||||
|
|
||||||
|
let { mode } = $props();
|
||||||
|
|
||||||
|
let transport = $state(null)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
new QWebChannel(qt.webChannelTransport, (channel) => {
|
||||||
|
transport = channel.objects.transport
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if mode === "player"}
|
||||||
|
<PlayerApp {transport} />
|
||||||
|
{:else if mode === "studio"}
|
||||||
|
<StudioApp {transport} />
|
||||||
|
{:else if mode === "server"}
|
||||||
|
<ServerApp {transport} />
|
||||||
|
{/if}
|
||||||
20
client/app/src/Components/Card.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script>
|
||||||
|
let { title = null, icon = null, class: className = "", children, ...restProps } = $props()
|
||||||
|
|
||||||
|
let classes = [className, "rounded-md border border-slate-200 bg-gray-50 dark:border-neutral-700 dark:bg-neutral-800/80"].filter(Boolean).join(" ")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={classes} {...restProps}>
|
||||||
|
{#if title === null && children?.title}
|
||||||
|
{@render children.title()}
|
||||||
|
{:else if title !== null}
|
||||||
|
<div class="flex w-full select-none items-center rounded-t-md border-b border-slate-200 bg-gray-100 px-2 py-1 font-semibold dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-50">
|
||||||
|
<i class="fa-regular fa-light fa-fw {icon} me-1" />
|
||||||
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="p-3">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
11
client/app/src/Components/PlayerPageButton.svelte
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
let { page, selectedPage, onClick } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={onClick} data-page={page.id} title={page.title} class="mb-1.5 rounded-lg p-2 {page.id == selectedPage ? 'aya-page-btn-selected' : 'aya-page-btn'}">
|
||||||
|
{#if page.id == "about" || page.id == "settings"}
|
||||||
|
<i class="{page.id == selectedPage ? 'fa-solid' : 'fa-light'} {page.icon} fa-fw fa-lg text-zinc-800 dark:text-neutral-50"></i>
|
||||||
|
{:else}
|
||||||
|
<img src="./img/icons/{page.id}.png" alt={page.title} />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
36
client/app/src/Components/ServerBrowser.svelte
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import Card from "@/Components/Card.svelte"
|
||||||
|
import Button from "@/Controls/Button.svelte";
|
||||||
|
import InputError from "@/Controls/InputError.svelte"
|
||||||
|
|
||||||
|
let transport
|
||||||
|
let servers = $state([]);
|
||||||
|
let error = $state("");
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
new QWebChannel(qt.webChannelTransport, (channel) => {
|
||||||
|
transport = channel.objects.jsHelpers
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card class="col-span-1 h-full" icon="fa-server">
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<InputError message={error} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each servers as server, i}
|
||||||
|
<div class="flex w-full items-center rounded-md border border-gray-200 bg-gray-100 px-3 py-1.5">
|
||||||
|
<div class="w-full">
|
||||||
|
<span class="text-gray-600">({server.player_count}/{server.player_limit})</span>
|
||||||
|
<span class="font-semibold">{server.host}</span>'s {server.server_name} {i}
|
||||||
|
<Button text="Join" class="ms-1 aya-btn-sm" onclick={() => joinServer(i)} />
|
||||||
|
</div>
|
||||||
|
<!-- <img class="ms-1" height="10" src="./img/pbs.png" /> -->
|
||||||
|
<!-- <img class="ms-1" height="10" src="./img/mega.png" /> -->
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Card>
|
||||||
8
client/app/src/Components/ServerPageButton.svelte
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
let { page, selectedPage, onClick } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={onClick} data-page={page.id} title={page.title} class="flex min-h-12 min-w-12 flex-col items-center rounded {page.id == selectedPage ? 'aya-page-btn-selected text-neutral-600 dark:text-gray-200' : 'aya-page-btn text-neutral-500 dark:text-gray-500'}">
|
||||||
|
<i class="{page.id == selectedPage ? 'fa-solid' : 'fa-light'} {page.icon} fa-fw"></i>
|
||||||
|
<span class="mt-1 text-xs {page.id == selectedPage ? 'font-semibold' : 'font-medium'}">{page.title}</span>
|
||||||
|
</button>
|
||||||
6
client/app/src/Controls/Breadcrumb.svelte
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
let { class: className = "", children } = $props();
|
||||||
|
</script>
|
||||||
|
<div class="{className} mb-3 flex w-full rounded-lg bg-gray-200 dark:bg-zinc-800 px-4 py-2">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
18
client/app/src/Controls/BreadcrumbItem.svelte
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Props
|
||||||
|
* @property {boolean} [active]
|
||||||
|
* @property {import('svelte').Snippet} [children]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {Props} */
|
||||||
|
let { active = false, children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dark:text-white {active ? '' : 'me-2'}">
|
||||||
|
{@render children?.()}
|
||||||
|
|
||||||
|
{#if !active}
|
||||||
|
<i class="fa-light fa-chevron-right fa-fw text-neutral-500"></i>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
41
client/app/src/Controls/Button.svelte
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
let { icon = null, text = null, disabled = false, class: className = "", onclick = null, children, ...restProps } = $props()
|
||||||
|
|
||||||
|
const iconStore = writable(icon)
|
||||||
|
|
||||||
|
onMount(() => iconStore.set(`fa-regular ${icon}`))
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (!disabled) iconStore.set(`fa-solid ${icon}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (!disabled) iconStore.set(`fa-regular ${icon}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseClick = (e) => {
|
||||||
|
if (!disabled && onclick) onclick(e)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="aya-anim-pop inline-flex h-10 min-h-10 items-center justify-center gap-2 rounded-lg bg-aya-500 px-3 text-lg text-white transition duration-200 hover:bg-aya-600 disabled:opacity-35 {className}"
|
||||||
|
onmouseenter={handleMouseEnter}
|
||||||
|
onmouseleave={handleMouseLeave}
|
||||||
|
onclick={handleMouseClick}
|
||||||
|
disabled={disabled || null}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#if icon}
|
||||||
|
<i class={$iconStore} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if text}
|
||||||
|
<span>{text}</span>
|
||||||
|
{:else}
|
||||||
|
{@render children?.()}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
11
client/app/src/Controls/ButtonLink.svelte
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
let { href = null, text = null, class: className = "", children, ...restProps } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button href={href} class="aya-anim-pop rounded-lg px-2 py-1 text-neutral-500 underline transition duration-200 hover:bg-black/10 hover:text-neutral-600 dark:text-zinc-500 dark:hover:bg-white/10 dark:hover:text-zinc-400 {className}" {...restProps}>
|
||||||
|
{#if !text}
|
||||||
|
{@render children?.()}
|
||||||
|
{:else}
|
||||||
|
{text}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
22
client/app/src/Controls/Checkbox.svelte
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script>
|
||||||
|
let { checked = $bindable(false), onchange = null, label = null, labelClass = "", class: className = "", children, ...restProps } = $props()
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
if (onchange) onchange(checked)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked
|
||||||
|
onchange={handleChange}
|
||||||
|
class="aya-anim-pop cursor-pointer rounded border-gray-300 text-aya-500 shadow-sm transition duration-100 focus:ring-aya-400 dark:border-zinc-700 dark:bg-zinc-900 {className}"
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{#if label}
|
||||||
|
<span class="ml-2 text-gray-700 dark:text-gray-300 {labelClass}">{label}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-2 flex items-center text-gray-700 dark:text-gray-300 {labelClass}">{@render children?.()}</span>
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
48
client/app/src/Controls/Dropdown.svelte
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount, setContext } from "svelte"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
|
||||||
|
import DropdownButton from "@/Controls/DropdownButton.svelte"
|
||||||
|
|
||||||
|
let { button = null, class: className = "", children, ...restProps } = $props();
|
||||||
|
|
||||||
|
let shown = $state(false)
|
||||||
|
let dropdownRef = $state()
|
||||||
|
|
||||||
|
setContext("dropdown", {
|
||||||
|
close: () => (shown = false),
|
||||||
|
isShown: () => shown
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (dropdownRef && !dropdownRef.contains(event.target) && shown) {
|
||||||
|
shown = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("click", handleClickOutside)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("click", handleClickOutside)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleDropdown = () => (shown = !shown)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={dropdownRef} class="relative" {...restProps}>
|
||||||
|
{#if button}
|
||||||
|
{@render button?.({ shown, toggleDropdown })}
|
||||||
|
{:else}
|
||||||
|
<DropdownButton {shown} onclick={toggleDropdown} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if shown}
|
||||||
|
<div class="absolute z-10 mt-2 w-48 rounded-md bg-white dark:bg-zinc-800 shadow-lg ring-1 ring-black ring-opacity-5 {className}" transition:fade={{ duration: 50 }}>
|
||||||
|
<div class="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
9
client/app/src/Controls/DropdownButton.svelte
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
let { shown = false, onclick = () => {}, children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button {onclick} class="aya-anim-pop rounded-lg text-neutral-500 transition duration-200 hover:text-neutral-600 {shown ? 'bg-black/10' : 'hover:bg-black/5'}">
|
||||||
|
{#if children}{@render children({ shown })}{:else}
|
||||||
|
<i class="{shown ? 'fa-solid' : 'fa-regular'} fa-caret-down fa-fw aya-anim-pop"></i>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
1
client/app/src/Controls/DropdownDivider.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<div class="w-100 my-1 border-t dark:border-neutral-700"></div>
|
||||||
27
client/app/src/Controls/DropdownItem.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
let { text = null, icon = null, iconPosition = "left", style = null, selected = false, onclick = null, class: className = "", ...restProps } = $props()
|
||||||
|
|
||||||
|
const { close } = getContext("dropdown")
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (onclick) onclick()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={handleClick}
|
||||||
|
class="aya-anim-pop flex w-full items-center px-3 py-1 text-sm text-neutral-700 transition duration-100 dark:text-neutral-300 {style === 'danger' ? 'hover:bg-red-100 hover:text-red-900 dark:hover:bg-red-950 dark:hover:text-red-100' : 'hover:bg-gray-100 dark:hover:bg-zinc-700'} {icon && iconPosition === 'right' ? 'justify-between' : ''} {selected ? 'bg-gray-200 hover:bg-gray-300' : ''} {className}"
|
||||||
|
role="menuitem"
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#if icon && iconPosition === "left"}
|
||||||
|
<i class="{selected ? 'fa-solid' : 'fa-light'} {icon} fa-fw me-1" />
|
||||||
|
{/if}
|
||||||
|
<span class={selected ? "font-semibold" : "font-normal"}>{text}</span>
|
||||||
|
{#if icon && iconPosition === "right"}
|
||||||
|
<i class="{selected ? 'fa-solid' : 'fa-light'} {icon} fa-fw" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
15
client/app/src/Controls/InputError.svelte
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Props
|
||||||
|
* @property {any} [message]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {Props} */
|
||||||
|
let { message = null } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if message}
|
||||||
|
<p class="mt-2 text-sm text-red-600 dark:text-red-400">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
11
client/app/src/Controls/InputLabel.svelte
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
let { value = null, for: forAttr = null, children, ...restProps } = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label class="select-none dark:text-neutral-100" for={forAttr} {...restProps}>
|
||||||
|
{#if value}
|
||||||
|
<span>{value}</span>
|
||||||
|
{:else}
|
||||||
|
<span>{@render children?.()}</span>
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
25
client/app/src/Controls/List.svelte
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script>
|
||||||
|
let { items = [], activeItem = $bindable(items[0]?.id), onitemchange = null } = $props();
|
||||||
|
|
||||||
|
function handleItemChange(id) {
|
||||||
|
activeItem = id
|
||||||
|
if (onitemchange) onitemchange(id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#each items as item}
|
||||||
|
{#if activeItem === item.id}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button class="aya-games-genre-list w-full !ms-0"><i class={`fa-solid fa-caret-right me-1 text-red-500`}></i> {item.label}</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button class={`aya-games-genre-list ${!item.icon ? 'ml-[10px]' : ''}`} onclick={() => handleItemChange(item.id)}>
|
||||||
|
{#if item.icon}
|
||||||
|
<i class={`fa-light fa-fw me-1 ${item.icon}`}></i>
|
||||||
|
{/if}
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
15
client/app/src/Controls/ListButtons.svelte
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script>
|
||||||
|
let { items = [], activeItem = $bindable(items[0]?.id), onitemchange = null } = $props();
|
||||||
|
|
||||||
|
function handleItemChange(id) {
|
||||||
|
activeItem = id
|
||||||
|
if (onitemchange) onitemchange(id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each items as item}
|
||||||
|
<button class="aya-anim-pop flex flex-nowrap items-center rounded-lg px-2 py-1 text-start transition duration-200 {activeItem === item.id ? 'bg-black/10 dark:bg-white/10 font-semibold text-aya-700 dark:text-aya-500' : 'text-aya-600/85 hover:bg-black/5 dark:hover:bg-white/5 hover:text-aya-600 dark:text-aya-600'}" class:active={activeItem === item.id} onclick={() => handleItemChange(item.id)}>
|
||||||
|
<i class={`${activeItem === item.id ? "fa-solid" : "fa-regular"} ${item.icon} fa-fw me-1`}></i>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
37
client/app/src/Controls/Pagination.svelte
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<script>
|
||||||
|
import Button from "@/Controls/Button.svelte"
|
||||||
|
|
||||||
|
let { currentPage, lastPage, routeName } = $props();
|
||||||
|
|
||||||
|
const pageNumbers = () => {
|
||||||
|
let pages = []
|
||||||
|
const maxPages = 10
|
||||||
|
let start = Math.max(currentPage - Math.floor(maxPages / 2), 1)
|
||||||
|
let end = Math.min(start + maxPages - 1, lastPage)
|
||||||
|
|
||||||
|
if (end - start < maxPages - 1) {
|
||||||
|
start = Math.max(end - maxPages + 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToPage = (pageNumber) => {
|
||||||
|
if (pageNumber < 1 || pageNumber > lastPage) return
|
||||||
|
window.location.href = window.route(routeName, { page: pageNumber })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if lastPage > 1}
|
||||||
|
<div class="mt-4 flex items-center justify-center space-x-0">
|
||||||
|
<Button class="aya-btn-sm rounded-l-md rounded-r-none border hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-700" text="‹" disabled={currentPage === 1} onclick={() => goToPage(currentPage - 1)} />
|
||||||
|
{#each pageNumbers() as pageNum}
|
||||||
|
<Button class={`aya-btn-sm border ${pageNum === currentPage ? "text-gray-700" : "hover:bg-gray-100"} ${pageNum === currentPage ? "cursor-not-allowed" : ""} rounded-none dark:border-gray-600 dark:hover:bg-gray-700`} text={pageNum} disabled={pageNum === currentPage} onclick={() => pageNum !== currentPage && goToPage(pageNum)} />
|
||||||
|
{/each}
|
||||||
|
<Button class="aya-btn-sm rounded-l-none rounded-r-md border hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-700" text="›" disabled={currentPage === lastPage} onclick={() => goToPage(currentPage + 1)} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
17
client/app/src/Controls/PillButtons.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
let { items = [], activeItem = $bindable(items[0]?.id), onitemchange = null } = $props();
|
||||||
|
|
||||||
|
function handleItemChange(id) {
|
||||||
|
activeItem = id
|
||||||
|
if (onitemchange) onitemchange({ id })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
{#each items as item}
|
||||||
|
<button class="aya-anim-pop me-1 rounded-full px-3 py-1 text-sm transition duration-200 {activeItem === item.id ? 'border border-aya-500 bg-white font-medium text-aya-500 hover:bg-black/5 dark:border-aya-400 dark:bg-neutral-800 dark:text-aya-400' : 'text-neutral-600 hover:bg-black/10 dark:text-neutral-400 dark:hover:bg-white/10'}" class:active={activeItem === item.id} onclick={() => handleItemChange(item.id)}>
|
||||||
|
<i class={`${activeItem === item.id ? "fa-solid" : "fa-regular"} ${item.icon} fa-fw`}></i>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
14
client/app/src/Controls/Select.svelte
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Props
|
||||||
|
* @property {string} [value]
|
||||||
|
* @property {import('svelte').Snippet} [children]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {Props} */
|
||||||
|
let { value = $bindable(""), children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select bind:value class="mt-1 w-full rounded-lg dark:bg-zinc-900 border-gray-300 dark:border-neutral-700 text-neutral-700 dark:text-neutral-300 transition duration-100 focus:border-aya-500">
|
||||||
|
{@render children?.()}
|
||||||
|
</select>
|
||||||
71
client/app/src/Controls/Tabs.svelte
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let { tabs = [], activeTab = $bindable(), hideTabs = false, roundedCorners = true, ontabchange = null, class: className = "", ...restProps } = $props()
|
||||||
|
|
||||||
|
let tabRefs = {}
|
||||||
|
let lineStyle = $state("")
|
||||||
|
|
||||||
|
function setActiveTab(id) {
|
||||||
|
activeTab = id
|
||||||
|
if (ontabchange) ontabchange(id)
|
||||||
|
updateLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLine() {
|
||||||
|
const activeTabRef = tabRefs[activeTab]
|
||||||
|
if (activeTabRef) {
|
||||||
|
lineStyle = `left: ${activeTabRef.offsetLeft}px; width: ${activeTabRef.offsetWidth}px;`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
updateLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
let tabClasses = $derived(tabs.map((tab, index) => {
|
||||||
|
let classes = ["aya-anim-pop", "px-6", "py-2", "text-lg", "transition", "duration-200", "hover:bg-black/5", "dark:hover:bg-white/5", className]
|
||||||
|
|
||||||
|
if (activeTab === tab.id) {
|
||||||
|
classes.push("font-medium", "text-aya-500")
|
||||||
|
} else {
|
||||||
|
classes.push("text-neutral-500", "dark:text-neutral-400", "hover:text-neutral-700", "dark:hover:text-neutral-300")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roundedCorners) {
|
||||||
|
classes.push("rounded-t-lg")
|
||||||
|
} else {
|
||||||
|
if (index === 0) classes.push("rounded-tl-lg")
|
||||||
|
if (index === tabs.length - 1) classes.push("rounded-tr-lg")
|
||||||
|
if (index !== 0 && index !== tabs.length - 1) classes.push("border-t-0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(" ")
|
||||||
|
}))
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateLine()
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
activeTab
|
||||||
|
hideTabs
|
||||||
|
updateLine()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onresize={handleResize} />
|
||||||
|
|
||||||
|
<div class="relative mb-2 flex {className}" {...restProps}>
|
||||||
|
{#each tabs as tab, index}
|
||||||
|
<button bind:this={tabRefs[tab.id]} onclick={() => setActiveTab(tab.id)} class={tabClasses[index]}>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{#if !hideTabs}
|
||||||
|
<span class="absolute bottom-0 h-0.5 rounded bg-aya-500 transition-all duration-200 ease-in-out" style={lineStyle}></span>
|
||||||
|
{/if}
|
||||||
|
<slot name="header" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot {activeTab}></slot>
|
||||||
25
client/app/src/Controls/TextArea.svelte
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let { value = $bindable(""), error = null, class: className = "", ...restProps } = $props()
|
||||||
|
|
||||||
|
let textAreaElement
|
||||||
|
let isFocused = $state(false)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (textAreaElement.hasAttribute("autofocus")) {
|
||||||
|
textAreaElement.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let classes = $derived([error ? "pe-7 dark:border-red-400 border-red-600 focus:border-red-500 focus:ring-red-600 hover:border-red-400 dark:hover:border-red-600" : "border-gray-300 dark:border-gray-700 hover:border-aya-300 focus:border-aya-500 dark:border-zinc-700 dark:focus:border-aya-600", className, "focus:ring-aya-500 block w-full rounded-lg border-gray-300 transition duration-100 dark:bg-zinc-900 dark:text-gray-300"].filter(Boolean).join(" "))
|
||||||
|
|
||||||
|
let iconColor = $derived(isFocused ? "text-red-500 dark:text-red-600" : "text-red-600 dark:text-red-400")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative {className && className.includes('w-full') ? 'w-full' : ''}">
|
||||||
|
<textarea class={classes} bind:value bind:this={textAreaElement} onfocus={() => (isFocused = true)} onblur={() => (isFocused = false)} {...restProps} />
|
||||||
|
{#if error}
|
||||||
|
<i class={`fas fa-times pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 transform transition duration-100 ${iconColor}`} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
28
client/app/src/Controls/TextInput.svelte
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let { value = $bindable(""), error = null, icon = null, class: className = "", ...restProps } = $props()
|
||||||
|
|
||||||
|
let inputElement
|
||||||
|
let isFocused = $state(false)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (inputElement.hasAttribute("autofocus")) {
|
||||||
|
inputElement.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let classes = $derived([error ? "pe-7 dark:border-red-400 border-red-600 focus:border-red-500 focus:ring-red-600 hover:border-red-400 dark:hover:border-red-600" : "border-gray-300 hover:border-aya-300 focus:border-aya-500 dark:border-neutral-700 dark:focus:border-aya-600", `focus:ring-aya-500 block w-full rounded border-gray-300 transition duration-100 dark:bg-neutral-900 dark:text-neutral-300 h-[2rem] dark:placeholder-neutral-500 ${className ?? ""} ${icon ? "pl-10" : ""}`].filter(Boolean).join(" "))
|
||||||
|
|
||||||
|
let iconColor = $derived(isFocused ? "text-red-500 dark:text-red-600" : "text-red-600 dark:text-red-400")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative {className && className.includes('w-full') ? 'w-full' : ''}">
|
||||||
|
{#if icon}
|
||||||
|
<i class={`fa-regular fa-fw ${icon} pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400`} />
|
||||||
|
{/if}
|
||||||
|
<input class={classes} bind:value bind:this={inputElement} onfocus={() => (isFocused = true)} onblur={() => (isFocused = false)} {...restProps} />
|
||||||
|
{#if error}
|
||||||
|
<i class={`fa-solid fa-times pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 transform transition duration-100 ${iconColor}`} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
7
client/app/src/Controls/TextLink.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
let { href = null, text = null, class: className = "", ...restProps } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button href={href} class="aya-anim-pop rounded-lg text-aya-500 underline transition duration-200 hover:text-aya-400 {className}" {...restProps}>
|
||||||
|
{text}
|
||||||
|
</button>
|
||||||
17
client/app/src/Enums/ChatStyle.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const ChatStyle = {
|
||||||
|
Classic: 0,
|
||||||
|
Bubble: 1,
|
||||||
|
ClassicAndBubble: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = {
|
||||||
|
[ChatStyle.Classic]: "Classic",
|
||||||
|
[ChatStyle.Bubble]: "Bubble",
|
||||||
|
[ChatStyle.ClassicAndBubble]: "Classic and Bubble"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChatStyleName(style) {
|
||||||
|
return names[style]
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ChatStyle, getChatStyleName }
|
||||||
57
client/app/src/Enums/GearType.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
const GearType = {
|
||||||
|
MeleeWeapons: 0,
|
||||||
|
RangedWeapons: 1,
|
||||||
|
Explosives: 2,
|
||||||
|
PowerUps: 3,
|
||||||
|
NavigationEnhancers: 4,
|
||||||
|
MusicalInstruments: 5,
|
||||||
|
SocialItems: 6,
|
||||||
|
BuildingTools: 7,
|
||||||
|
Transport: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = {
|
||||||
|
[GearType.MeleeWeapons]: "Melee Weapons",
|
||||||
|
[GearType.RangedWeapons]: "Ranged Weapons",
|
||||||
|
[GearType.Explosives]: "Explosives",
|
||||||
|
[GearType.PowerUps]: "Power Ups",
|
||||||
|
[GearType.NavigationEnhancers]: "Navigation Enhancers",
|
||||||
|
[GearType.MusicalInstruments]: "Musical Instruments",
|
||||||
|
[GearType.SocialItems]: "Social Items",
|
||||||
|
[GearType.BuildingTools]: "Building Tools",
|
||||||
|
[GearType.Transport]: "Transport"
|
||||||
|
}
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
[GearType.MeleeWeapons]: "sword",
|
||||||
|
[GearType.RangedWeapons]: "crosshairs",
|
||||||
|
[GearType.Explosives]: "bomb",
|
||||||
|
[GearType.PowerUps]: "bolt",
|
||||||
|
[GearType.NavigationEnhancers]: "compass",
|
||||||
|
[GearType.MusicalInstruments]: "music",
|
||||||
|
[GearType.SocialItems]: "share-nodes",
|
||||||
|
[GearType.BuildingTools]: "screwdriver-wrench",
|
||||||
|
[GearType.Transport]: "car-side"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGearTypeName(type) {
|
||||||
|
return names[type]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGearTypeIcon(type) {
|
||||||
|
return icons[type]
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableGearType(gearAttributes, gearType) {
|
||||||
|
return gearAttributes | (1 << gearType)
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableGearType(gearAttributes, gearType) {
|
||||||
|
return gearAttributes & ~(1 << gearType)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGearTypeEnabled(gearAttributes, gearType) {
|
||||||
|
return (gearAttributes & (1 << gearType)) !== 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export { GearType, getGearTypeName, getGearTypeIcon, enableGearType, disableGearType, isGearTypeEnabled }
|
||||||
8
client/app/src/Enums/MessageType.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
Output: 0,
|
||||||
|
Info: 1,
|
||||||
|
Warning: 2,
|
||||||
|
Error: 3,
|
||||||
|
Sensitive: 4,
|
||||||
|
Max: 5
|
||||||
|
}
|
||||||
170
client/app/src/Pages/Player/About.svelte
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let { transport } = $props()
|
||||||
|
|
||||||
|
let platform = $state(null)
|
||||||
|
let version = $state(null)
|
||||||
|
let compiler = $state(null)
|
||||||
|
let compileTimestamp = $state(null)
|
||||||
|
let isUsingInstance = $state(null)
|
||||||
|
let instanceName = $state(null)
|
||||||
|
let instanceMotd = $state(null)
|
||||||
|
let instanceUrl = $state(null)
|
||||||
|
let instanceUpdateState = $state(null)
|
||||||
|
let totalPlaytime = $state(null)
|
||||||
|
|
||||||
|
let totalItems = $state(0)
|
||||||
|
let totalModels = $state(0)
|
||||||
|
let totalLevels = $state(0)
|
||||||
|
|
||||||
|
const dependencies = [
|
||||||
|
{ name: "assimp", license: "https://github.com/assimp/assimp/blob/master/LICENSE" },
|
||||||
|
{ name: "BGFX", license: "https://github.com/bkaradzic/bgfx/blob/master/LICENSE" },
|
||||||
|
{ name: "Boost", license: "https://www.boost.org/users/license.html" },
|
||||||
|
{ name: "Bullet Physics SDK", license: "https://github.com/bulletphysics/bullet3/blob/master/LICENSE.txt" },
|
||||||
|
{ name: "cURL", license: "https://curl.se/docs/copyright.html" },
|
||||||
|
{ name: "CGAL", license: "https://www.cgal.org/license.html" },
|
||||||
|
{ name: "Discord RPC", license: "https://github.com/discord/discord-rpc/blob/master/LICENSE" },
|
||||||
|
{ name: "GLAD", license: "https://github.com/Dav1dde/glad/blob/glad2/LICENSE" },
|
||||||
|
{ name: "libjpeg-turbo", license: "https://github.com/libjpeg-turbo/libjpeg-turbo/blob/main/LICENSE.md" },
|
||||||
|
{ name: "FreeType", license: "https://freetype.org/license.html" },
|
||||||
|
{ name: "ImGui", license: "https://www.dearimgui.com/licenses/" },
|
||||||
|
{ name: "ImPlot", license: "https://github.com/epezent/implot/blob/master/LICENSE" },
|
||||||
|
{ name: "libarchive", license: "https://raw.githubusercontent.com/libarchive/libarchive/master/COPYING" },
|
||||||
|
{ name: "libjxl", license: "https://github.com/libjxl/libjxl/blob/main/LICENSE" },
|
||||||
|
{ name: "libpng", license: "http://www.libpng.org/pub/png/src/libpng-LICENSE.txt" },
|
||||||
|
{ name: "lz4", license: "https://github.com/lz4/lz4/blob/dev/LICENSE" },
|
||||||
|
{ name: "microprofile", license: "https://github.com/jonasmr/microprofile/blob/master/LICENSE" },
|
||||||
|
{ name: "OpenSSL", license: "https://www.openssl.org/source/apache-license-2.0.txt" },
|
||||||
|
{ name: "opus", license: "https://opus-codec.org/license/" },
|
||||||
|
{ name: "PortAudio", license: "https://files.portaudio.com/docs/v19-doxydocs/License.html" },
|
||||||
|
{ name: "pugixml", license: "https://pugixml.org/license.html" },
|
||||||
|
{ name: "Qt 6", license: "https://doc.qt.io/qt-6/lgpl.html" },
|
||||||
|
{ name: "RakNet", license: "https://github.com/facebookarchive/RakNet/blob/master/LICENSE" },
|
||||||
|
{ name: "RapidJSON", license: "https://github.com/Tencent/rapidjson/blob/master/license.txt" },
|
||||||
|
{ name: "SDL3", license: "https://www.libsdl.org/license.php" },
|
||||||
|
{ name: "xxHash", license: "https://github.com/Cyan4973/xxHash/blob/dev/LICENSE" },
|
||||||
|
{ name: "zlib", license: "https://www.zlib.net/zlib_license.html" },
|
||||||
|
{ name: "zstd", license: "https://github.com/facebook/zstd/blob/dev/LICENSE" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const cardClass = "mx-4 my-2 p-4 rounded rounded-lg border border-slate-100 bg-gray-50 shadow dark:bg-neutral-900 dark:border-neutral-800"
|
||||||
|
const iconClass = "me-1 [image-rendering:pixelated]"
|
||||||
|
const statClass = "flex items-center text-stone-800 dark:text-stone-300 font-medium"
|
||||||
|
|
||||||
|
function humanizeDuration(seconds) {
|
||||||
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
|
||||||
|
return minutes > 0 ? `${hours}h ${minutes}m` : `${minutes}m ${seconds % 60}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
function humanizeTimestamp(timestamp) {
|
||||||
|
const date = new Date(timestamp * 1000)
|
||||||
|
|
||||||
|
return `${date.toDateString()} ${date.toTimeString().split(" ")[0]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!transport) return
|
||||||
|
|
||||||
|
[platform, version, isUsingInstance, totalPlaytime, compiler, compileTimestamp, instanceUpdateState] = await Promise.all([
|
||||||
|
transport.getPlatformName(),
|
||||||
|
transport.getVersion(),
|
||||||
|
transport.isUsingInstance(),
|
||||||
|
transport.getTotalPlaytime().then(humanizeDuration),
|
||||||
|
transport.getCompilerName(),
|
||||||
|
transport.getCompileTime().then(humanizeTimestamp),
|
||||||
|
transport.getInstanceUpdateState()
|
||||||
|
]); // this semicolon is very important... svelte's js parser breaks without it!
|
||||||
|
|
||||||
|
[totalLevels, totalItems, totalModels] = await Promise.all([
|
||||||
|
transport.getTotalLevels(),
|
||||||
|
transport.getTotalItems(),
|
||||||
|
transport.getTotalModels()
|
||||||
|
])
|
||||||
|
|
||||||
|
if (isUsingInstance) {
|
||||||
|
[instanceUrl, instanceName, instanceMotd] = await Promise.all([
|
||||||
|
transport.getInstanceURL(),
|
||||||
|
transport.getInstanceName(),
|
||||||
|
transport.getInstanceMotd()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-full flex flex-col overflow-hidden">
|
||||||
|
<div class="{cardClass} flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<img alt="Aya" class="select-none pointer-events-none" src="./img/aya.png" width="80" />
|
||||||
|
<div class="ms-5 flex flex-col">
|
||||||
|
<span class="text-3xl mb-1.5 select-none"><b>Aya</b> for {platform ?? "Unknown"}</span>
|
||||||
|
<code>{version ? `v${version}` : "N/A"}</code>
|
||||||
|
<span class="text-sm"><span class="select-none">Compiled on</span><code class="ms-1">{compileTimestamp ?? "N/A"} ({compiler ?? "N/A"})</code></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center pointer-events-none select-none space-y-0.5 mx-1">
|
||||||
|
<span class={statClass}><img alt="Levels" class={iconClass} src="./img/icons/level.png">{totalLevels ?? 0} levels</span>
|
||||||
|
<span class={statClass}><img alt="Items" class={iconClass} src="./img/icons/item.png">{totalItems ?? 0} items</span>
|
||||||
|
<span class={statClass}><img alt="Models" class={iconClass} src="./img/icons/model.png">{totalModels ?? 0} models</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isUsingInstance}
|
||||||
|
<div class="{cardClass} mb-2 flex items-center">
|
||||||
|
<img alt="{instanceName}" class="select-none pointer-events-none" src="./img/small.png" width="80" />
|
||||||
|
<div class="ms-5 flex flex-col">
|
||||||
|
<span class="mb-1 text-3xl select-none font-bold">{instanceName ?? "Unknown"}</span>
|
||||||
|
<div class="mb-1 flex items-center">
|
||||||
|
<span class="text-aya-500"><i class="fa-regular fa-globe me-1"></i><a href="{instanceUrl}" target="_blank" class="underline">{instanceUrl ?? "https://unknown.org/"}</a></span>
|
||||||
|
<i class="fa-regular fa-pipe"></i>
|
||||||
|
{#if instanceUpdateState && instanceUpdateState.enabled}
|
||||||
|
<span class="text-green-600 dark:text-green-400 font-semibold flex items-center"><i class="fa-solid fa-cloud fa-fw me-1"></i>Updates enabled <span class="text-xs ms-1">(last updated: {humanizeTimestamp(instanceUpdateState.lastUpdated)})</span></span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-gray-500 dark:text-gray-400 flex items-center"><i class="fa-regular fa-cloud-slash fa-fw me-1"></i>Updates disabled</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if instanceMotd}
|
||||||
|
<span class="text-sm italic">“{instanceMotd}”</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col items-center justify-center w-full select-none">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400 flex items-center mb-1"><i class="fa-regular fa-link-slash fa-fw me-1"></i>Not currently linked to an instance</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mb-2 flex flex-col items-center justify-center w-full select-none">
|
||||||
|
<span class="text-stone-600 dark:text-neutral-200 font-semibold flex items-center"><i class="fa-regular fa-clock fa-fw me-1"></i>{totalPlaytime ?? "0m 0s"} of total playtime</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2 flex items-center w-full justify-center flex-col select-none">
|
||||||
|
<span class="text-3xl font-extrabold">License</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-neutral-200 dark:border-neutral-700 dark:bg-neutral-800/80 m-1.5 mb-4 flex flex-col flex-1 min-h-0">
|
||||||
|
<div class="p-4 font-mono text-[12.5px] overflow-y-auto">
|
||||||
|
<span class="font-semibold">Due to the fact that Aya is a fork of previously leaked proprietary Roblox source code, it is not possible to apply a traditional license to Aya. Therefore, all authors of custom non-proprietary code and related resources used in Aya hereby irrevocably waive all copyright and related rights to their contributions, to the maximum extent allowed by law.</span>
|
||||||
|
<br />
|
||||||
|
<br />Please note that this waiver of copyright applies only to the non proprietary items found in Aya. Any proprietary code that may still exist in Aya remains subject to the copyright and licensing conditions imposed by its original authors or owners; in particular, Roblox Corporation.
|
||||||
|
<br />
|
||||||
|
<br />Aya uses a variety of third party dependencies. A list of all the third party dependencies used in Aya alongside a link to their respective license are available below:
|
||||||
|
<br />
|
||||||
|
{#each dependencies as dep}
|
||||||
|
<br /><i class="fa-sharp fa-dot mx-1"></i>{dep.name}: <a class="text-aya-500 underline" target="_blank" href="{dep.license}">{dep.license}</a>
|
||||||
|
{/each}
|
||||||
|
<br />
|
||||||
|
<br />The authors of Aya have made a concerted effort to rid the original codebase of any and all proprietary or otherwise closed source dependencies, and to replace them with free and open source alternatives. The only proprietary or non-free items that still remain in this repository is code and artwork created by Roblox Corporation, which themselves have undergone substantial modification to the extent that they no longer resemble their original versions.
|
||||||
|
<br />
|
||||||
|
<br /><span class="underline">It is the duty of anyone who uses Aya to be fully aware of the legal circumstances surrounding its use. The original authors of Aya expressly disclaim all liability for any and all uses of Aya, including, without limitation, any direct, indirect, incidental, special, consequential, or exemplary damages, even if advised of the possibility of such damages. The original authors of Aya further disclaim any and all responsibility for any third party's use or misuse of Aya.</span>
|
||||||
|
<br />
|
||||||
|
<br /><b>THE MATERIALS IN THIS REPOSITORY, INCLUDING ALL SOURCE CODE AND OTHER RELATED ITEMS, SUCH AS DOCUMENTATION, ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR ANY OTHER COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.</b>
|
||||||
|
<br />
|
||||||
|
<br /><span class="italic">In addition to the legal responsibilities outlined above, we strongly encourage all users of Aya to use this software in a responsible and ethical manner. Please respect the rights and dignity of others, and use Aya only in a way that contributes positively to the world.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
12
client/app/src/Pages/Player/Avatar.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
import { onDestroy, onMount } from "svelte"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import Button from "@/Controls/Button.svelte"
|
||||||
|
import PillButtons from "@/Controls/PillButtons.svelte"
|
||||||
|
|
||||||
|
let { transport } = $props();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
3
client/app/src/Pages/Player/Favorites.svelte
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold">Favorites</h1>
|
||||||
|
</div>
|
||||||
7
client/app/src/Pages/Player/Levels.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
let { transport } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold">Browse Roblox Levels</h1>
|
||||||
|
</div>
|
||||||
7
client/app/src/Pages/Player/Packages.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
let { transport } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold">Manage Packages</h1>
|
||||||
|
</div>
|
||||||
120
client/app/src/Pages/Player/Play.svelte
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<script>
|
||||||
|
import { preventDefault } from 'svelte/legacy';
|
||||||
|
|
||||||
|
import ServerBrowser from "@/Components/ServerBrowser.svelte"
|
||||||
|
import Button from "@/Controls/Button.svelte"
|
||||||
|
import InputLabel from "@/Controls/InputLabel.svelte"
|
||||||
|
import TextInput from "@/Controls/TextInput.svelte"
|
||||||
|
import InputError from "@/Controls/InputError.svelte"
|
||||||
|
import Card from "@/Components/Card.svelte"
|
||||||
|
|
||||||
|
let { transport, onNavigate } = $props();
|
||||||
|
|
||||||
|
let ipAddress = $state("")
|
||||||
|
let port = $state("")
|
||||||
|
let error = $state("")
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
try {
|
||||||
|
const baseUrl = transport.getMasterServerURL()
|
||||||
|
const userId = Math.floor(Math.random() * 65535)
|
||||||
|
|
||||||
|
let bodyColors = []
|
||||||
|
let charApp = []
|
||||||
|
|
||||||
|
let charAppValue = transport.characterAppearanceJSON()
|
||||||
|
|
||||||
|
console.error("initial: " + charAppValue);
|
||||||
|
|
||||||
|
let selectedAssets = JSON.parse(charAppValue);
|
||||||
|
|
||||||
|
if (selectedAssets.hat) {
|
||||||
|
selectedAssets.hat.forEach(function (assetId) {
|
||||||
|
charApp.push(assetId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedAssets.normal) {
|
||||||
|
for (const subCategory in selectedAssets.normal) {
|
||||||
|
if (selectedAssets.normal.hasOwnProperty(subCategory)) {
|
||||||
|
const assetIds = selectedAssets.normal[subCategory];
|
||||||
|
assetIds.forEach(function (assetId) {
|
||||||
|
charApp.push(assetId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("charapp: " + JSON.stringify(charApp))
|
||||||
|
|
||||||
|
let bodyColorsValue = transport.getBodyColorJson()
|
||||||
|
bodyColors = JSON.parse(bodyColorsValue);
|
||||||
|
|
||||||
|
const finalCharApp = baseUrl + '/avatar-fetch/?userid=' + userId + '&json=' + encodeURIComponent(JSON.stringify(charApp)) + '&body=' + encodeURIComponent(JSON.stringify(bodyColors));
|
||||||
|
|
||||||
|
const connectionObj = {
|
||||||
|
ClientPort: 0,
|
||||||
|
MachineAddress: ipAddress,
|
||||||
|
ServerPort: Number(port),
|
||||||
|
UserName: "Player",
|
||||||
|
DisplayName: "Player",
|
||||||
|
CharacterAppearance: finalCharApp,
|
||||||
|
GameId: 1818,
|
||||||
|
PlaceId: 1818,
|
||||||
|
PingInterval: 20,
|
||||||
|
UserId: userId,
|
||||||
|
CreatorId: 1,
|
||||||
|
MembershipType: "None",
|
||||||
|
SuperSafeChat: false,
|
||||||
|
IsUnknownOrUnder13: false,
|
||||||
|
CreatorTypeEnum: "User",
|
||||||
|
ChatStyle: "ClassicAndBubble",
|
||||||
|
VirtualVersion: 0,
|
||||||
|
IsRobloxPlace: true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionObj.VirtualVersion === 4) {
|
||||||
|
transport.enableChatBarWidget()
|
||||||
|
}
|
||||||
|
|
||||||
|
await transport.launchGame(JSON.stringify(connectionObj), connectionObj.VirtualVersion)
|
||||||
|
} catch (err) {
|
||||||
|
error = "Failed to connect to server: " + err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid h-full grid-rows-[auto,1fr] gap-3">
|
||||||
|
<div class="col-span-1 grid grid-cols-2 gap-3">
|
||||||
|
<Card title="Direct Connect" class="col-span-1" icon="fa-plug">
|
||||||
|
<form onsubmit={preventDefault(submit)}>
|
||||||
|
<div>
|
||||||
|
<InputLabel for="ip" value="IP Address" />
|
||||||
|
<TextInput placeholder="127.0.0.1" class="mt-1" id="ip" type="text" bind:value={ipAddress} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<InputLabel for="port" value="Port" />
|
||||||
|
<TextInput placeholder="53640" class="mt-1" id="port" type="number" bind:value={port} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<InputError message={error} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span class="mt-2 block text-sm italic text-neutral-500 dark:text-neutral-300">
|
||||||
|
You will be joining as <b>Player</b>.
|
||||||
|
<button type="button" onclick={() => onNavigate('avatar')} class="underline">Customize Character</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="mt-2 flex w-full justify-end">
|
||||||
|
<Button class="aya-btn-sm !bg-transparent !text-pink-400 !duration-0 hover:!bg-transparent hover:!text-pink-500" title="Add to Favorites" icon="fa-lg fa-heart" />
|
||||||
|
<Button class="aya-btn-sm" type="submit" text="Join" icon="fa-right-to-bracket" title="Connect to Server" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
<Card title="Play Solo" class="col-span-1" icon="fa-play">placeholder</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ServerBrowser {transport} />
|
||||||
|
</div>
|
||||||
79
client/app/src/Pages/Player/Settings.svelte
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<script>
|
||||||
|
import { preventDefault } from 'svelte/legacy';
|
||||||
|
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
import Button from "@/Controls/Button.svelte"
|
||||||
|
import InputLabel from "@/Controls/InputLabel.svelte"
|
||||||
|
import TextInput from "@/Controls/TextInput.svelte"
|
||||||
|
import InputError from "@/Controls/InputError.svelte"
|
||||||
|
import Card from "@/Components/Card.svelte"
|
||||||
|
|
||||||
|
let { transport } = $props();
|
||||||
|
|
||||||
|
let masterServerURL = $state("")
|
||||||
|
let masterServerKey = $state("")
|
||||||
|
let serverHostPassword = $state("")
|
||||||
|
let robloSecurityCookie = $state("")
|
||||||
|
let error = $state("")
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
getSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getSettings() {
|
||||||
|
masterServerURL = await transport.getMasterServerURL() ?? ""
|
||||||
|
robloSecurityCookie = await transport.getRobloSecurityCookie() ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
let a = '';
|
||||||
|
(Object.getOwnPropertyNames(transport)).forEach(element => {
|
||||||
|
a += (element) + "\n"
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
transport.setMasterServerURL(masterServerURL)
|
||||||
|
transport.setRobloSecurityCookie(robloSecurityCookie)
|
||||||
|
} catch (err) {
|
||||||
|
error = err + "\n" + a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid h-full grid-rows-[auto,1fr] gap-3">
|
||||||
|
<div class="col-span-1 grid grid-cols-3 gap-3">
|
||||||
|
This page is temporary.
|
||||||
|
<Card title="General Settings" class="col-span-1" icon="fa-gear">
|
||||||
|
<form onsubmit={preventDefault(submit)}>
|
||||||
|
<div>
|
||||||
|
<InputLabel for="masterserverurl" value="Master Server URL" />
|
||||||
|
<TextInput placeholder="http://masterserver.com/" class="mt-1" id="masterserverurl" type="text" bind:value={masterServerURL} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<InputLabel for="masterserverkey" value="Master Server Key" />
|
||||||
|
<TextInput placeholder="http://masterserver.com/" class="mt-1" id="masterserverkey" type="password" bind:value={masterServerKey} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<InputLabel for="hostserverpassword" value="Host Server Password" />
|
||||||
|
<TextInput placeholder="" class="mt-1" id="hostserverpassword" type="password" bind:value={serverHostPassword} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<InputLabel for="securitycookie" value="ROBLOSECURITY Cookie" />
|
||||||
|
<TextInput placeholder=".ROBLOSECURITY=_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|" class="mt-1" id="securitycookie" type="password" bind:value={robloSecurityCookie} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<InputError message={error} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-2 flex w-full justify-end">
|
||||||
|
<Button class="aya-btn-sm" type="submit" text="Update Settings" icon="fa-gear" title="Update Settings" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
178
client/app/src/Pages/Server/Host.svelte
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<script>
|
||||||
|
import { ChatStyle, getChatStyleName } from "@/Enums/ChatStyle"
|
||||||
|
import { GearType, getGearTypeName, getGearTypeIcon } from "@/Enums/GearType"
|
||||||
|
|
||||||
|
import Button from "@/Controls/Button.svelte"
|
||||||
|
import TextInput from "@/Controls/TextInput.svelte"
|
||||||
|
import Card from "@/Components/Card.svelte"
|
||||||
|
|
||||||
|
import Select from "@/Controls/Select.svelte"
|
||||||
|
import InputLabel from "@/Controls/InputLabel.svelte"
|
||||||
|
import Checkbox from "@/Controls/Checkbox.svelte"
|
||||||
|
import TextArea from "@/Controls/TextArea.svelte"
|
||||||
|
|
||||||
|
let { transport } = $props();
|
||||||
|
|
||||||
|
let showServerSettings = $state(false)
|
||||||
|
let isTransitioning = $state(false)
|
||||||
|
let portValue = $state("")
|
||||||
|
let fileName = $state(null)
|
||||||
|
let serverPassword = $state(null)
|
||||||
|
let serverName = $state("")
|
||||||
|
let serverHost = $state("")
|
||||||
|
let serverDescription = $state("")
|
||||||
|
let chatStyle = $state(ChatStyle.ClassicAndBubble)
|
||||||
|
let progress = $state(false)
|
||||||
|
let error = $state(false)
|
||||||
|
let broadcast = $state(false)
|
||||||
|
let masterServerUrl = $state("")
|
||||||
|
let masterServerKey = $state("")
|
||||||
|
|
||||||
|
let isPublicDomain = $state(false)
|
||||||
|
|
||||||
|
let maxPlayers = $state(16)
|
||||||
|
|
||||||
|
let enabledGearTypes = $state(Object.values(GearType).map(() => false))
|
||||||
|
|
||||||
|
let gearAttributes = $derived(enabledGearTypes.reduce((acc, isEnabled, index) => {
|
||||||
|
return isEnabled ? acc | (1 << index) : acc
|
||||||
|
}, 0))
|
||||||
|
|
||||||
|
function handleGearChange(index, isChecked) {
|
||||||
|
enabledGearTypes[index] = isChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSettings() {
|
||||||
|
isTransitioning = true
|
||||||
|
setTimeout(() => {
|
||||||
|
showServerSettings = !showServerSettings
|
||||||
|
isTransitioning = false
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileSelect(event) {
|
||||||
|
const file = event.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
fileName = file.name
|
||||||
|
console.log("Selected file:", file.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startServer() {
|
||||||
|
progress = true
|
||||||
|
setTimeout(() => (progress = false), 5000)
|
||||||
|
error = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if showServerSettings}
|
||||||
|
<div class="lex h-full w-full flex-col transition-opacity duration-300 ease-in-out" class:opacity-0={isTransitioning} class:opacity-100={!isTransitioning}>
|
||||||
|
<div class="flex flex-shrink-0 items-center p-1">
|
||||||
|
<button onclick={toggleSettings} class="aya-anim-pop flex h-10 items-center justify-center rounded px-3 font-medium text-neutral-500 transition duration-100 hover:bg-black/10 hover:text-neutral-600 dark:text-neutral-300 dark:hover:bg-white/10">
|
||||||
|
<i class="fa fa-solid fa-fw fa-arrow-left me-2"></i>
|
||||||
|
<span>Back</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 px-3 pb-24 pt-1">
|
||||||
|
<Card class="flex flex-col">
|
||||||
|
<div>
|
||||||
|
<InputLabel for="max_players" value="Maximum amount of players" />
|
||||||
|
<TextInput class="mt-2" id="max_players" type="number" bind:value={maxPlayers} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<InputLabel for="server_password" value="Server password (optional)" />
|
||||||
|
<TextInput class="mt-2" id="server_password" type="password" placeholder="Leave empty for no password" bind:value={serverPassword} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<InputLabel for="chat_style" value="Chat style" />
|
||||||
|
<Select bind:value={chatStyle}>
|
||||||
|
{#each Object.values(ChatStyle) as style}
|
||||||
|
<option value={style} selected={chatStyle === style || null}>{getChatStyleName(style)}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<Checkbox name="is_public_domain" label="Allow connected players to download your place" bind:checked={isPublicDomain} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<Checkbox name="broadcast" label="Broadcast server details to a masterserver" bind:checked={broadcast} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if broadcast}
|
||||||
|
<div class="mt-2 select-none text-sm font-medium italic text-red-500 dark:text-red-400">Details about your server (IP address, port, connected players, etc.) will be publicly visible on the masterserver and to anyone who connects to it. Please make sure you trust the masterserver you are connecting to.</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<InputLabel for="master_server_url" value="Masterserver base URL" />
|
||||||
|
<TextInput class="mt-2" id="master_server_url" type="text" placeholder="e.g. masterserver.example.com" bind:value={masterServerUrl} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<InputLabel for="master_server_key" value="Masterserver authentication key (optional)" />
|
||||||
|
<TextInput class="mt-2" id="master_server_key" type="text" bind:value={masterServerKey} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<InputLabel for="server_name" value="Server Name" />
|
||||||
|
<TextInput class="mt-2" id="server_name" placeholder="Max 50 characters" type="text" bind:value={serverName} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<InputLabel for="server_host" value="Server Host" />
|
||||||
|
<TextInput class="mt-2" id="server_host" placeholder="Typically your username" type="text" bind:value={serverHost} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<InputLabel for="server_description" value="Server Description" />
|
||||||
|
<TextArea class="mt-2" id="server_description" placeholder="Max 1500 characters" bind:value={serverDescription} required />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<InputLabel for="name" value="Allowed gears" />
|
||||||
|
<div class="mt-1 grid grid-cols-2 gap-1 md:grid-cols-3">
|
||||||
|
{#each Object.entries(GearType) as [typeName, typeValue], index}
|
||||||
|
<Checkbox name={getGearTypeName(typeValue).toString().toLowerCase().replace(" ", "_")} bind:checked={enabledGearTypes[index]} onchange={() => handleGearChange(index, enabledGearTypes[index])}>
|
||||||
|
{getGearTypeName(typeValue)}
|
||||||
|
<i class="fa-regular fa-fw fa-{getGearTypeIcon(typeValue)} ms-1"></i>
|
||||||
|
</Checkbox>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Main Page -->
|
||||||
|
<div class="flex h-full w-full flex-col items-center justify-center transition-opacity duration-300 ease-in-out" class:opacity-0={isTransitioning} class:opacity-100={!isTransitioning}>
|
||||||
|
<img src="./img/aya-server{progress ? '-progress' : error ? '-error' : ''}.png" width="150" alt="Aya Server" class="pointer-events-none select-none" />
|
||||||
|
|
||||||
|
<span class="my-5 select-none text-4xl font-extrabold text-stone-900 dark:text-stone-50">Aya Server</span>
|
||||||
|
|
||||||
|
<div class="flex w-full flex-col items-center">
|
||||||
|
<div class="flex w-full max-w-64 flex-col">
|
||||||
|
<label for="place-file" class="aya-anim-pop flex cursor-pointer items-center rounded rounded-b-none border border-neutral-500/20 bg-neutral-500/5 px-3 py-1 text-lg text-neutral-500/85 transition duration-100 hover:bg-neutral-500/10">
|
||||||
|
<div class="select-none">
|
||||||
|
<i class="fa-light fa-folder-open fa-sm fa-fw me-1"></i>
|
||||||
|
{fileName ?? "Select place file …"}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input id="place-file" type="file" accept=".rbxl,.ayal,.rbxl.gz,.ayal.gz" class="hidden" onchange={handleFileSelect} />
|
||||||
|
|
||||||
|
<TextInput bind:value={portValue} min="0" max="65535" type="number" placeholder="Port (e.g. 53640)" class="!h-auto !rounded-t-none !border-t-0 py-1 text-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="aya-anim-pop mt-2 flex h-auto items-center justify-center rounded px-2 py-1 text-center text-neutral-500 transition duration-100 hover:bg-black/10 hover:text-neutral-600 dark:text-neutral-300 dark:hover:bg-white/10" onclick={toggleSettings}>
|
||||||
|
<i class="fa-regular fa-square-sliders me-0.5"></i>
|
||||||
|
Server Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onclick={startServer} disabled={progress} icon="fa-play" class="mt-4 !w-auto !min-w-max !max-w-full !gap-1 !rounded-md !border !border-green-600 !bg-green-500 !shadow-lg dark:!border-green-700 dark:!bg-green-600 {progress ? 'pointer-events-none cursor-default opacity-50' : ''}" text="Start" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
57
client/app/src/Pages/Server/Jobs.svelte
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<script>
|
||||||
|
let { transport } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-full w-full flex-col justify-start gap-3 overflow-scroll p-4 pb-24">
|
||||||
|
<div class="flex w-full items-center"><span class="text-3xl font-extrabold">Running Jobs</span><button class="ml-auto flex items-center rounded border border-neutral-300 bg-white px-3 py-1 text-sm text-neutral-600 shadow-sm hover:bg-gray-100"><i class="fa fa-regular fa-play fa-fw me-2"></i>Start</button></div>
|
||||||
|
|
||||||
|
<div class="flex items-center rounded border border-neutral-400/75 bg-gray-50/90 px-3 py-4">
|
||||||
|
<i class="fa fa-light fa-server fa-fw me-1 text-6xl text-neutral-600"></i>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-xl font-bold text-neutral-900">127.0.0.1:53640</span>
|
||||||
|
<span class="font-mono text-xs text-neutral-500">9f4e11e2-f13b-470f-b6cd-ada7d82cdf2f</span>
|
||||||
|
</div>
|
||||||
|
<!-- these go on the far right -->
|
||||||
|
<div class="ml-auto flex gap-3">
|
||||||
|
<button class="flex items-center rounded border border-neutral-300 bg-white px-3 py-1 text-sm text-neutral-600 shadow-sm hover:bg-gray-100">
|
||||||
|
<i class="fa fa-regular fa-eye fa-fw me-2"></i>Inspect
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center rounded border border-red-400 bg-red-500/10 px-3 py-1 text-sm text-red-600 shadow-sm hover:bg-red-500/20">
|
||||||
|
<i class="fa fa-regular fa-xmark fa-fw me-2"></i>Stop Job
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center rounded border border-neutral-400/75 bg-gray-50/90 px-3 py-4">
|
||||||
|
<i class="fa fa-light fa-server fa-fw me-1 text-6xl text-neutral-600"></i>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-xl font-bold text-neutral-900">Test Job</span>
|
||||||
|
<span class="font-mono text-sm text-neutral-500">a27df45e-670e-4a6b-94fa-d65f9ce844f3</span>
|
||||||
|
</div>
|
||||||
|
<!-- these go on the far right -->
|
||||||
|
<div class="ml-auto flex gap-3">
|
||||||
|
<button class="flex items-center rounded border border-neutral-300 bg-white px-3 py-1 text-sm text-neutral-600 shadow-sm hover:bg-gray-100">
|
||||||
|
<i class="fa fa-regular fa-eye fa-fw me-2"></i>Inspect
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center rounded border border-red-400 bg-red-500/10 px-3 py-1 text-sm text-red-600 shadow-sm hover:bg-red-500/20">
|
||||||
|
<i class="fa fa-regular fa-xmark fa-fw me-2"></i>Stop Job
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center rounded border border-neutral-400/75 bg-gray-50/90 px-3 py-4">
|
||||||
|
<i class="fa fa-light fa-server fa-fw me-1 text-6xl text-neutral-600"></i>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-xl font-bold text-neutral-900">yrdts</span>
|
||||||
|
<span class="font-mono text-sm text-neutral-500">72110ea2-de5e-4c82-b203-05b62cac9aa7</span>
|
||||||
|
</div>
|
||||||
|
<!-- these go on the far right -->
|
||||||
|
<div class="ml-auto flex gap-3">
|
||||||
|
<button class="flex items-center rounded border border-neutral-300 bg-white px-3 py-1 text-sm text-neutral-600 shadow-sm hover:bg-gray-100">
|
||||||
|
<i class="fa fa-regular fa-eye fa-fw me-2"></i>Inspect
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center rounded border border-red-400 bg-red-500/10 px-3 py-1 text-sm text-red-600 shadow-sm hover:bg-red-500/20">
|
||||||
|
<i class="fa fa-regular fa-xmark fa-fw me-2"></i>Stop Job
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
7
client/app/src/Pages/Server/REPL.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
let { transport } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-screen px-4 pb-[4.75rem] pt-3">
|
||||||
|
<div class="h-full w-full rounded bg-black outline outline-2 outline-neutral-600 dark:outline-white">HELLO</div>
|
||||||
|
</div>
|
||||||
40
client/app/src/Pages/Studio/IDE.svelte
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
let { transport } = $props();
|
||||||
|
|
||||||
|
let files = []
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
let parameters = new URLSearchParams(window.location.search)
|
||||||
|
let entries = Array.from(parameters.entries())
|
||||||
|
|
||||||
|
if (entries.length % 2 !== 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < entries.length; i += 2) {
|
||||||
|
let filepath = entries[i][1]
|
||||||
|
let filename = entries[i + 1][1]
|
||||||
|
|
||||||
|
files.push({ path: filepath, name: filename })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="mb-3 flex items-center">
|
||||||
|
<img src="./img/aya-studio.png" width="75" />
|
||||||
|
<div class="ms-3 flex flex-col">
|
||||||
|
<span class="text-2xl font-extrabold">Aya Studio</span>
|
||||||
|
<code>v1.0.0</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="mb-2 text-lg font-bold">Recent Files:</span>
|
||||||
|
<ul>
|
||||||
|
{#each files as file}
|
||||||
|
<li><a href={`#${file.path}`}>{file.name}</a></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
0
client/app/src/Pages/Studio/Toolbox.svelte
Normal file
77
client/app/src/Player.svelte
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<script>
|
||||||
|
import { spring } from "svelte/motion"
|
||||||
|
|
||||||
|
import PageButton from "@/Components/PlayerPageButton.svelte"
|
||||||
|
import PlayPage from "@/Pages/Player/Play.svelte"
|
||||||
|
import AvatarPage from "@/Pages/Player/Avatar.svelte"
|
||||||
|
import LevelsPage from "@/Pages/Player/Levels.svelte"
|
||||||
|
import PackagesPage from "@/Pages/Player/Packages.svelte"
|
||||||
|
import FavoritesPage from "@/Pages/Player/Favorites.svelte"
|
||||||
|
import SettingsPage from "@/Pages/Player/Settings.svelte"
|
||||||
|
import AboutPage from "@/Pages/Player/About.svelte"
|
||||||
|
|
||||||
|
let { transport } = $props();
|
||||||
|
|
||||||
|
let selectedPage = $state("play")
|
||||||
|
|
||||||
|
const pages = [
|
||||||
|
{ id: "play", title: "Play" },
|
||||||
|
{ id: "avatar", title: "Character" },
|
||||||
|
{ id: "levels", title: "Levels" },
|
||||||
|
{ id: "packages", title: "Packages" },
|
||||||
|
{ id: "favorites", title: "Favorites" },
|
||||||
|
{ id: "server", title: "Open Aya Server" },
|
||||||
|
{ id: "studio", title: "Open Aya Studio" },
|
||||||
|
{ id: "settings", title: "Settings", icon: "fa-cog" },
|
||||||
|
{ id: "about", title: "About", icon: "fa-circle-info" }
|
||||||
|
]
|
||||||
|
|
||||||
|
let pageIndicator = spring(9.5, {
|
||||||
|
stiffness: 0.15,
|
||||||
|
damping: 0.7
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (selectedPage) {
|
||||||
|
const activeButton = document.querySelector(`[data-page="${selectedPage}"]`)
|
||||||
|
if (activeButton) {
|
||||||
|
pageIndicator.set(activeButton.offsetTop + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-screen">
|
||||||
|
<div class="flex w-[3.5rem] flex-col items-center border-r border-gray-300 bg-gray-100 pb-0.5 pt-2 text-white dark:border-neutral-950 dark:bg-neutral-900">
|
||||||
|
<div class="absolute left-0 h-6 w-[0.175rem] rounded-r bg-aya-500" style="transform: translateY({$pageIndicator}px); transition: height 0.2s ease" role="presentation"></div>
|
||||||
|
{#each pages as page}
|
||||||
|
{#if page.id == "server"}
|
||||||
|
<div class="flex-grow"></div>
|
||||||
|
<PageButton {page} {selectedPage} onClick={() => (transport.launchStudio())} />
|
||||||
|
{:else if page.id == "studio"}
|
||||||
|
<PageButton {page} {selectedPage} onClick={() => (transport.launchStudio())} />
|
||||||
|
{:else}
|
||||||
|
<PageButton {page} {selectedPage} onClick={() => (selectedPage = page.id)} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="flex-1 p-3 dark:bg-stone-950 dark:text-neutral-100 {selectedPage === 'avatar' ? 'border-r border-gray-300' : ''}">
|
||||||
|
{#if selectedPage === "play"}
|
||||||
|
<PlayPage {transport} onNavigate={(page) => selectedPage = page} />
|
||||||
|
{:else if selectedPage === "avatar"}
|
||||||
|
<AvatarPage {transport} />
|
||||||
|
{:else if selectedPage === "levels"}
|
||||||
|
<LevelsPage {transport} />
|
||||||
|
{:else if selectedPage === "packages"}
|
||||||
|
<PackagesPage {transport} />
|
||||||
|
{:else if selectedPage === "favorites"}
|
||||||
|
<FavoritesPage />
|
||||||
|
{:else if selectedPage === "settings"}
|
||||||
|
<SettingsPage {transport} />
|
||||||
|
{:else if selectedPage === "about"}
|
||||||
|
<AboutPage {transport} />
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
46
client/app/src/Server.svelte
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import PageButton from "@/Components/ServerPageButton.svelte"
|
||||||
|
import HostPage from "@/Pages/Server/Host.svelte"
|
||||||
|
import JobPage from "@/Pages/Server/Jobs.svelte"
|
||||||
|
import REPLPage from "@/Pages/Server/REPL.svelte"
|
||||||
|
let { transport } = $props();
|
||||||
|
|
||||||
|
let selectedPage = $state("host")
|
||||||
|
let isTransitioning = $state(false)
|
||||||
|
|
||||||
|
const pages = [
|
||||||
|
{ icon: "fa-tower-broadcast", id: "host", title: "Host" },
|
||||||
|
{ icon: "fa-server", id: "jobs", title: "Jobs" },
|
||||||
|
{ icon: "fa-rectangle-terminal", id: "repl", title: "REPL" }
|
||||||
|
]
|
||||||
|
|
||||||
|
function changePage(newPage) {
|
||||||
|
if (newPage === selectedPage) return
|
||||||
|
|
||||||
|
isTransitioning = true
|
||||||
|
setTimeout(() => {
|
||||||
|
selectedPage = newPage
|
||||||
|
isTransitioning = false
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-screen w-screen flex-col items-center justify-center dark:bg-stone-950 dark:text-neutral-100">
|
||||||
|
<div class=" contents-center absolute bottom-0 left-0 z-50 flex w-full items-center justify-center gap-2 border-t border-gray-300 bg-gray-100 py-1.5 dark:border-neutral-950 dark:bg-neutral-900">
|
||||||
|
{#each pages as page}
|
||||||
|
<PageButton {page} {selectedPage} onClick={() => changePage(page.id)} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<main class="h-full w-full overflow-scroll">
|
||||||
|
<div class="h-full w-full transition-opacity duration-300 {isTransitioning ? 'opacity-0' : 'opacity-100'}">
|
||||||
|
{#if selectedPage === "host"}
|
||||||
|
<HostPage {transport} />
|
||||||
|
{:else if selectedPage === "jobs"}
|
||||||
|
<JobPage {transport} />
|
||||||
|
{:else if selectedPage === "repl"}
|
||||||
|
<REPLPage {transport} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
15
client/app/src/Studio.svelte
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import IDEPage from "@/Pages/Studio/IDE.svelte"
|
||||||
|
import ToolboxPage from "@/Pages/Studio/Toolbox.svelte"
|
||||||
|
let { transport } = $props();
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
const page = params.get("page") || "ide" // can be of [ide, toolbox] to access two different UIs
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if page === "ide"}
|
||||||
|
<IDEPage {transport} />
|
||||||
|
{:else if page === "toolbox"}
|
||||||
|
<ToolboxPage {transport} />
|
||||||
|
{/if}
|
||||||
29
client/bootstrapper/CMakeLists.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
set(CMAKE_AUTOUIC ON)
|
||||||
|
set(CMAKE_AUTORCC ON)
|
||||||
|
|
||||||
|
add_executable(Bootstrapper
|
||||||
|
src/main.cpp
|
||||||
|
resources/qt.qrc
|
||||||
|
|
||||||
|
src/Bootstrapper.cpp
|
||||||
|
src/Bootstrapper.hpp
|
||||||
|
|
||||||
|
${CLIENT_DIR}/common/AppSettings.cpp
|
||||||
|
${CLIENT_DIR}/common/AppSettings.hpp
|
||||||
|
)
|
||||||
|
|
||||||
|
if(AYA_OS_WINDOWS)
|
||||||
|
target_sources(Bootstrapper PRIVATE
|
||||||
|
resources/winrc.h
|
||||||
|
resources/script.rc
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(Bootstrapper PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||||
|
windeployqt(Bootstrapper)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_compile_definitions(Bootstrapper PRIVATE SKIP_APP_SETTINGS_LOADING)
|
||||||
|
target_link_libraries(Bootstrapper ${OPENSSL_CRYPTO_LIBRARIES})
|
||||||
|
target_include_directories(Bootstrapper PRIVATE src resources)
|
||||||
|
set_target_properties(Bootstrapper PROPERTIES OUTPUT_NAME "Aya")
|
||||||
BIN
client/bootstrapper/resources/icon.ico
Normal file
|
After Width: | Height: | Size: 32 KiB |
5
client/bootstrapper/resources/qt.qrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<RCC version="1.0">
|
||||||
|
<qresource>
|
||||||
|
<file>icon.ico</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
||||||
49
client/bootstrapper/resources/script.rc
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#include "winrc.h"
|
||||||
|
|
||||||
|
#if defined(__MINGW64__) || defined(__MINGW32__)
|
||||||
|
// MinGW-w64, MinGW
|
||||||
|
#if defined(__has_include) && __has_include(<winres.h>)
|
||||||
|
#include <winres.h>
|
||||||
|
#else
|
||||||
|
#include <afxres.h>
|
||||||
|
#include <winresrc.h>
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
// MSVC, Windows SDK
|
||||||
|
#include <winres.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
IDI_ICON1 ICON APP_ICON
|
||||||
|
|
||||||
|
LANGUAGE LANG_ENGLISH, SUBLANG_DEFAULT
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION VERSION_RESOURCE
|
||||||
|
PRODUCTVERSION VERSION_RESOURCE
|
||||||
|
FILEFLAGSMASK 0x3fL
|
||||||
|
#ifdef _DEBUG
|
||||||
|
FILEFLAGS 0x1L
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS 0x4L
|
||||||
|
FILETYPE 0x1L
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904b0"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", APP_ORGANIZATION
|
||||||
|
VALUE "FileDescription", APP_DESCRIPTION
|
||||||
|
VALUE "FileVersion", VERSION_RESOURCE_STR
|
||||||
|
VALUE "LegalCopyright", APP_COPYRIGHT
|
||||||
|
VALUE "ProductName", APP_NAME
|
||||||
|
VALUE "ProductVersion", VERSION_RESOURCE_STR
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", PRODUCT_LANGUAGE, PRODUCT_CHARSET
|
||||||
|
END
|
||||||
|
END
|
||||||
24
client/bootstrapper/resources/winrc.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define VERSION_MAJOR_MINOR_STR AYA_VERSION_MAJOR_STR "." AYA_VERSION_MINOR_STR
|
||||||
|
#define VERSION_MAJOR_MINOR_PATCH_STR VERSION_MAJOR_MINOR_STR "." AYA_VERSION_PATCH_STR
|
||||||
|
#ifdef AYA_VERSION_TYPE
|
||||||
|
#define VERSION_FULL_STR VERSION_MAJOR_MINOR_PATCH_STR "-" AYA_VERSION_TYPE
|
||||||
|
#else
|
||||||
|
#define VERSION_FULL_STR VERSION_MAJOR_MINOR_PATCH_STR
|
||||||
|
#endif
|
||||||
|
#define VERSION_RESOURCE AYA_VERSION_MAJOR, AYA_VERSION_MINOR, AYA_VERSION_PATCH, 0
|
||||||
|
#define VERSION_RESOURCE_STR VERSION_FULL_STR "\0"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These properties are part of VarFileInfo.
|
||||||
|
* For more information, please see: https://learn.microsoft.com/en-us/windows/win32/menurc/varfileinfo-block
|
||||||
|
*/
|
||||||
|
#define PRODUCT_LANGUAGE 0x0409 // en-US
|
||||||
|
#define PRODUCT_CHARSET 1200 // Unicode
|
||||||
|
|
||||||
|
#define APP_ICON "icon.ico"
|
||||||
|
#define APP_NAME AYA_PROJECT_NAME "\0"
|
||||||
|
#define APP_DESCRIPTION AYA_PROJECT_NAME " Bootstrapper\0"
|
||||||
|
#define APP_ORGANIZATION AYA_PROJECT_NAME "\0"
|
||||||
|
#define APP_COPYRIGHT AYA_PROJECT_NAME " License\0"
|
||||||
308
client/bootstrapper/src/Bootstrapper.cpp
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
#ifdef _WIN32
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
#include <Windows.h>
|
||||||
|
#endif
|
||||||
|
#include "Bootstrapper.hpp"
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <vector>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include <archive.h>
|
||||||
|
#include <archive_entry.h>
|
||||||
|
#include <openssl/sha.h>
|
||||||
|
#include <rapidjson/writer.h>
|
||||||
|
#include <rapidjson/stringbuffer.h>
|
||||||
|
|
||||||
|
size_t WriteToString(void* contents, size_t size, size_t nmemb, void* userp)
|
||||||
|
{
|
||||||
|
size_t total = size * nmemb;
|
||||||
|
auto* out = static_cast<std::string*>(userp);
|
||||||
|
out->append(static_cast<const char*>(contents), total);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t WriteToOStream(void* contents, size_t size, size_t nmemb, void* userp)
|
||||||
|
{
|
||||||
|
size_t total = size * nmemb;
|
||||||
|
auto* os = static_cast<std::ostream*>(userp);
|
||||||
|
os->write(static_cast<const char*>(contents), static_cast<std::streamsize>(total));
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bootstrapper::Bootstrapper(const std::string& mode, bool showUI, bool forceSkipUpdates, bool isUsingInstance, const std::string& instanceUrl,
|
||||||
|
const std::string& instanceAccessKey)
|
||||||
|
: mode(mode)
|
||||||
|
, showUI(showUI)
|
||||||
|
, forceSkipUpdates(forceSkipUpdates)
|
||||||
|
, isUsingInstance(isUsingInstance)
|
||||||
|
, instanceUrl(instanceUrl)
|
||||||
|
, instanceAccessKey(instanceAccessKey)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
std::string Bootstrapper::httpGet(const std::string& path)
|
||||||
|
{
|
||||||
|
CURL* curl = curl_easy_init();
|
||||||
|
if (!curl)
|
||||||
|
throw std::runtime_error("CURL init failed for " + path);
|
||||||
|
|
||||||
|
std::string response;
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, (this->instanceUrl + path).c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteToString);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||||
|
|
||||||
|
struct curl_slist* headers = nullptr;
|
||||||
|
if (!this->instanceAccessKey.empty())
|
||||||
|
{
|
||||||
|
std::string authHeader = "Authorization: " + this->instanceAccessKey;
|
||||||
|
headers = curl_slist_append(headers, authHeader.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
|
||||||
|
if (headers)
|
||||||
|
curl_slist_free_all(headers);
|
||||||
|
|
||||||
|
if (res != CURLE_OK)
|
||||||
|
{
|
||||||
|
std::string msg = "CURL GET failed: ";
|
||||||
|
msg += curl_easy_strerror(res);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
throw std::runtime_error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Bootstrapper::downloadFile(const std::string& path, const std::string& outputPath)
|
||||||
|
{
|
||||||
|
CURL* curl = curl_easy_init();
|
||||||
|
if (!curl)
|
||||||
|
throw std::runtime_error("CURL init failed for " + path);
|
||||||
|
|
||||||
|
std::ofstream ofs(outputPath, std::ios::binary);
|
||||||
|
if (!ofs)
|
||||||
|
{
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
throw std::runtime_error("Failed to open output file: " + outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, (this->instanceUrl + path).c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteToOStream);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, static_cast<void*>(&ofs));
|
||||||
|
|
||||||
|
struct curl_slist* headers = nullptr;
|
||||||
|
if (!this->instanceAccessKey.empty())
|
||||||
|
{
|
||||||
|
std::string authHeader = "Authorization: " + this->instanceAccessKey;
|
||||||
|
headers = curl_slist_append(headers, authHeader.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
|
||||||
|
curl_off_t downloadSize = 0;
|
||||||
|
curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD_T, &downloadSize);
|
||||||
|
|
||||||
|
if (headers)
|
||||||
|
curl_slist_free_all(headers);
|
||||||
|
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
|
if (res != CURLE_OK)
|
||||||
|
throw std::runtime_error("CURL download failed: " + std::string(curl_easy_strerror(res)));
|
||||||
|
|
||||||
|
return static_cast<int>(downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Bootstrapper::verifySHA256(const std::string& filePath, const std::string& expectedHex)
|
||||||
|
{
|
||||||
|
std::ifstream file(filePath, std::ios::binary);
|
||||||
|
if (!file)
|
||||||
|
throw std::runtime_error("Failed to open file for SHA256: " + filePath);
|
||||||
|
|
||||||
|
SHA256_CTX ctx;
|
||||||
|
SHA256_Init(&ctx);
|
||||||
|
|
||||||
|
std::vector<char> buffer(1 << 16);
|
||||||
|
while (file.good())
|
||||||
|
{
|
||||||
|
file.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
|
||||||
|
std::streamsize r = file.gcount();
|
||||||
|
if (r > 0)
|
||||||
|
SHA256_Update(&ctx, buffer.data(), static_cast<size_t>(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char hash[SHA256_DIGEST_LENGTH];
|
||||||
|
SHA256_Final(hash, &ctx);
|
||||||
|
|
||||||
|
std::ostringstream oss;
|
||||||
|
for (unsigned char b : hash)
|
||||||
|
{
|
||||||
|
oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(b);
|
||||||
|
}
|
||||||
|
return oss.str() == expectedHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bootstrapper::extractTarZst(const std::string& archivePath, const std::string& outputDir)
|
||||||
|
{
|
||||||
|
struct archive* a = archive_read_new();
|
||||||
|
archive_read_support_format_tar(a);
|
||||||
|
archive_read_support_filter_zstd(a);
|
||||||
|
|
||||||
|
if (archive_read_open_filename(a, archivePath.c_str(), 10240) != ARCHIVE_OK)
|
||||||
|
{
|
||||||
|
std::string err = archive_error_string(a);
|
||||||
|
archive_read_free(a);
|
||||||
|
throw std::runtime_error("Failed to open tar.zst: " + err);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::create_directories(outputDir);
|
||||||
|
|
||||||
|
struct archive_entry* entry;
|
||||||
|
while (archive_read_next_header(a, &entry) == ARCHIVE_OK)
|
||||||
|
{
|
||||||
|
const char* pathname = archive_entry_pathname(entry);
|
||||||
|
std::filesystem::path outPath = std::filesystem::path(outputDir) / pathname;
|
||||||
|
|
||||||
|
if (archive_entry_filetype(entry) == AE_IFDIR)
|
||||||
|
{
|
||||||
|
std::filesystem::create_directories(outPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::filesystem::create_directories(outPath.parent_path());
|
||||||
|
std::ofstream ofs(outPath, std::ios::binary);
|
||||||
|
const void* buff;
|
||||||
|
size_t size;
|
||||||
|
la_int64_t offset;
|
||||||
|
while (archive_read_data_block(a, &buff, &size, &offset) == ARCHIVE_OK)
|
||||||
|
{
|
||||||
|
ofs.write(static_cast<const char*>(buff), static_cast<std::streamsize>(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
archive_read_free(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
rapidjson::Document Bootstrapper::parseJson(const std::string& jsonStr)
|
||||||
|
{
|
||||||
|
rapidjson::Document doc;
|
||||||
|
|
||||||
|
doc.Parse(jsonStr.c_str());
|
||||||
|
if (doc.HasParseError())
|
||||||
|
throw std::runtime_error("Failed to parse JSON");
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
rapidjson::Document Bootstrapper::fetchLatestManifest()
|
||||||
|
{
|
||||||
|
return parseJson(httpGet("api/aya/updater/manifest"));
|
||||||
|
}
|
||||||
|
|
||||||
|
rapidjson::Document Bootstrapper::fetchCachedManifest()
|
||||||
|
{
|
||||||
|
if (std::filesystem::exists("data/manifest.json"))
|
||||||
|
{
|
||||||
|
std::ifstream ifs("data/manifest.json");
|
||||||
|
std::stringstream buffer;
|
||||||
|
buffer << ifs.rdbuf();
|
||||||
|
return parseJson(buffer.str());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// return empty data
|
||||||
|
if (!std::filesystem::exists("data"))
|
||||||
|
{
|
||||||
|
std::filesystem::create_directories("data");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string filePath = "data/manifest.json";
|
||||||
|
std::string emptyData = "{}";
|
||||||
|
std::ofstream ofs(filePath);
|
||||||
|
ofs << emptyData;
|
||||||
|
ofs.close();
|
||||||
|
|
||||||
|
return parseJson(emptyData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bootstrapper::updateCachedManifest(const rapidjson::Document& manifest)
|
||||||
|
{
|
||||||
|
rapidjson::StringBuffer buffer;
|
||||||
|
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
|
||||||
|
manifest.Accept(writer);
|
||||||
|
|
||||||
|
std::ofstream ofs("data/manifest.json");
|
||||||
|
ofs << buffer.GetString();
|
||||||
|
ofs.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bootstrapper::checkForUpdates()
|
||||||
|
{
|
||||||
|
rapidjson::Document latestManifest = fetchLatestManifest();
|
||||||
|
rapidjson::Document cachedManifest = fetchCachedManifest();
|
||||||
|
|
||||||
|
if (!cachedManifest.HasMember("version") || cachedManifest["version"].GetString() != latestManifest["version"].GetString())
|
||||||
|
{
|
||||||
|
std::string downloadUrl = latestManifest["download_url"].GetString();
|
||||||
|
std::string expectedSha256 = latestManifest["sha256"].GetString();
|
||||||
|
|
||||||
|
std::cout << "New version available: " << latestManifest["version"].GetString() << std::endl;
|
||||||
|
std::cout << "Downloading from: " << downloadUrl << std::endl;
|
||||||
|
|
||||||
|
std::string tempFilePath = "data/update_temp.tar.zst";
|
||||||
|
downloadFile(downloadUrl, tempFilePath);
|
||||||
|
|
||||||
|
if (!verifySHA256(tempFilePath, expectedSha256))
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Downloaded file SHA256 does not match expected value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
extractTarZst(tempFilePath, ".");
|
||||||
|
|
||||||
|
std::filesystem::remove(tempFilePath);
|
||||||
|
|
||||||
|
updateCachedManifest(latestManifest);
|
||||||
|
|
||||||
|
std::cout << "Update applied successfully to version " << latestManifest["version"].GetString() << std::endl;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cout << "No updates available." << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void launchProcess(const std::string& appName, const std::string& commandLine)
|
||||||
|
{
|
||||||
|
std::string fullCommand = appName + " " + commandLine;
|
||||||
|
int result = std::system(fullCommand.c_str());
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Failed to launch process: " + fullCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bootstrapper::start(const std::string& commandLine)
|
||||||
|
{
|
||||||
|
if (isUsingInstance)
|
||||||
|
this->checkForUpdates();
|
||||||
|
|
||||||
|
if (mode == "player")
|
||||||
|
launchProcess("Aya.Player", commandLine);
|
||||||
|
else if (mode == "studio")
|
||||||
|
launchProcess("Aya.Studio", commandLine);
|
||||||
|
else if (mode == "server")
|
||||||
|
launchProcess("Aya.Server", commandLine);
|
||||||
|
}
|
||||||
62
client/bootstrapper/src/Bootstrapper.hpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <rapidjson/document.h>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QProgressDialog>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
class Bootstrapper
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::string mode;
|
||||||
|
bool showUI;
|
||||||
|
bool forceSkipUpdates;
|
||||||
|
bool isUsingInstance;
|
||||||
|
std::string instanceUrl;
|
||||||
|
std::string instanceAccessKey;
|
||||||
|
|
||||||
|
std::string httpGet(const std::string& url);
|
||||||
|
int downloadFile(const std::string& url, const std::string& outputPath);
|
||||||
|
|
||||||
|
rapidjson::Document fetchCachedManifest();
|
||||||
|
rapidjson::Document fetchLatestManifest();
|
||||||
|
void updateCachedManifest(const rapidjson::Document& manifest);
|
||||||
|
|
||||||
|
static void extractTarZst(const std::string& archivePath, const std::string& outputDir);
|
||||||
|
static bool verifySHA256(const std::string& filePath, const std::string& expectedHex);
|
||||||
|
static rapidjson::Document parseJson(const std::string& jsonStr);
|
||||||
|
|
||||||
|
public:
|
||||||
|
Bootstrapper(const std::string& mode, bool showUI, bool forceSkipUpdates, bool isUsingInstance, const std::string& instanceUrl,
|
||||||
|
const std::string& instanceAccessKey);
|
||||||
|
|
||||||
|
void update(const rapidjson::Document& manifest);
|
||||||
|
void checkForUpdates();
|
||||||
|
void start(const std::string& commandLine);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
class BootstrapperProgressDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BootstrapperProgressDialog();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||||
|
void onFinished();
|
||||||
|
void onReadyRead();
|
||||||
|
void onError(QNetworkReply::NetworkError code);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString formatSize(qint64 bytes) const;
|
||||||
|
|
||||||
|
QProgressDialog* progressDialog{nullptr};
|
||||||
|
QNetworkAccessManager* manager{nullptr};
|
||||||
|
QNetworkReply* reply{nullptr};
|
||||||
|
};
|
||||||
|
*/
|
||||||
126
client/bootstrapper/src/main.cpp
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <boost/program_options.hpp>
|
||||||
|
#include <boost/algorithm/string/join.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "AppSettings.hpp"
|
||||||
|
#include "Bootstrapper.hpp"
|
||||||
|
#include "winrc.h"
|
||||||
|
|
||||||
|
namespace po = boost::program_options;
|
||||||
|
|
||||||
|
QCoreApplication* createApplication(int &argc, const char *argv[])
|
||||||
|
{
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
if (!qstrcmp(argv[i], "--no-gui"))
|
||||||
|
return new QCoreApplication(argc, (char**)argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new QApplication(argc, (char**)argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, const char* argv[])
|
||||||
|
{
|
||||||
|
QScopedPointer<QCoreApplication> app(createApplication(argc, argv));
|
||||||
|
bool showUI = qobject_cast<QApplication*>(app.data()) != nullptr;
|
||||||
|
|
||||||
|
QCoreApplication::setOrganizationName(AYA_PROJECT_NAME);
|
||||||
|
QCoreApplication::setApplicationName(AYA_PROJECT_NAME);
|
||||||
|
QCoreApplication::setApplicationVersion(VERSION_FULL_STR);
|
||||||
|
|
||||||
|
po::options_description desc(AYA_PROJECT_NAME " options");
|
||||||
|
|
||||||
|
std::string mode = "player";
|
||||||
|
bool isUsingInstance = false; // we will determine this through our bespoke methods
|
||||||
|
bool forceSkipUpdates = false;
|
||||||
|
|
||||||
|
std::string appSettingsPath;
|
||||||
|
std::string instanceUrl, instanceAccessKey;
|
||||||
|
|
||||||
|
desc.add_options()
|
||||||
|
("help,?", "Usage help")
|
||||||
|
("version,V", "Print version and exit")
|
||||||
|
("player", "Launch player (default)")
|
||||||
|
("studio", "Launch studio")
|
||||||
|
("server", "Launch server")
|
||||||
|
("skip-updates,F", "Skip update check")
|
||||||
|
("no-gui", "Run in no-GUI mode (server only)")
|
||||||
|
("instance-url,U", po::value<std::string>(&instanceUrl), "Instance URL override")
|
||||||
|
("instance-access-key,k", po::value<std::string>(&instanceAccessKey), "Instance access key")
|
||||||
|
("app-settings,S", po::value<std::string>(&appSettingsPath)->default_value("AppSettings.ini"), "Path to AppSettings.ini");
|
||||||
|
|
||||||
|
po::variables_map vm;
|
||||||
|
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||||
|
po::notify(vm);
|
||||||
|
|
||||||
|
if (vm.count("help"))
|
||||||
|
{
|
||||||
|
std::cout << desc << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vm.count("version"))
|
||||||
|
{
|
||||||
|
std::cout << VERSION_FULL_STR << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSettings settings(appSettingsPath);
|
||||||
|
if (!settings.load())
|
||||||
|
{
|
||||||
|
std::cout << "Failed to load AppSettings.ini - please make sure it exists with a valid ContentPath under the Aya group, and make sure that it is free of any errors.";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.hasGroup("Instance"))
|
||||||
|
{
|
||||||
|
if (settings.has("Instance", "Domain"))
|
||||||
|
if (instanceUrl.empty())
|
||||||
|
instanceUrl = settings.get("Instance", "Domain").value();
|
||||||
|
|
||||||
|
if (settings.has("Instance", "AccessKey"))
|
||||||
|
if (instanceAccessKey.empty())
|
||||||
|
instanceAccessKey = settings.get("Instance", "AccessKey").value();
|
||||||
|
}
|
||||||
|
|
||||||
|
isUsingInstance = !instanceUrl.empty();
|
||||||
|
|
||||||
|
// legacy holdover
|
||||||
|
if (isUsingInstance)
|
||||||
|
if (instanceUrl.rbegin() != instanceUrl.rend() && *instanceUrl.rbegin() != '/')
|
||||||
|
instanceUrl = instanceUrl + "/";
|
||||||
|
|
||||||
|
if (vm.count("studio"))
|
||||||
|
mode = "studio";
|
||||||
|
else if (vm.count("server"))
|
||||||
|
mode = "server";
|
||||||
|
|
||||||
|
if (vm.count("skip-updates"))
|
||||||
|
forceSkipUpdates = true;
|
||||||
|
|
||||||
|
std::vector<std::string> args;
|
||||||
|
for (int i = 1; i < argc; ++i)
|
||||||
|
{
|
||||||
|
std::string arg = argv[i];
|
||||||
|
|
||||||
|
// Skip bootstrapper-specific arguments
|
||||||
|
if (arg == "--help" || arg == "-?" ||
|
||||||
|
arg == "--version" || arg == "-V" ||
|
||||||
|
arg == "--player" || arg == "--studio" || arg == "--server" ||
|
||||||
|
arg == "--skip-updates" || arg == "-F")
|
||||||
|
{
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push_back(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bootstrapper bootstrapper(mode, showUI, forceSkipUpdates, isUsingInstance, instanceUrl, instanceAccessKey);
|
||||||
|
bootstrapper.start(boost::algorithm::join(args, " "));
|
||||||
|
|
||||||
|
return app->exec();
|
||||||
|
}
|
||||||
136
client/common/AppSettings.cpp
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#include "AppSettings.hpp"
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#ifndef SKIP_APP_SETTINGS_LOADING
|
||||||
|
#include "Utility/StandardOut.hpp"
|
||||||
|
#include "Utility/Statistics.hpp"
|
||||||
|
#endif // SKIP_APP_SETTINGS_LOADING
|
||||||
|
|
||||||
|
#include <boost/algorithm/string.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
static const char* kAppSettingsFileName = "AppSettings.ini";
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
AppSettings::AppSettings(const std::string& appDir)
|
||||||
|
: m_pSettings(nullptr)
|
||||||
|
{
|
||||||
|
this->appDir = appDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSettings::~AppSettings()
|
||||||
|
{
|
||||||
|
delete m_pSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppSettings::load()
|
||||||
|
{
|
||||||
|
if (appDir.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
delete m_pSettings;
|
||||||
|
|
||||||
|
std::string path = appDir + "/" + kAppSettingsFileName;
|
||||||
|
|
||||||
|
#ifndef SKIP_APP_SETTINGS_LOADING
|
||||||
|
Aya::StandardOut::singleton()->printf(Aya::MESSAGE_SYSTEM, "Loading AppSettings from %s", path.c_str());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
m_pSettings = new QSettings(QString::fromStdString(appDir + "/" + kAppSettingsFileName), QSettings::IniFormat);
|
||||||
|
|
||||||
|
bool success = m_pSettings->status() == QSettings::NoError;
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
m_pSettings->sync();
|
||||||
|
m_pSettings->setFallbacksEnabled(true);
|
||||||
|
|
||||||
|
// hack for bootstrapper who can't deal with allat
|
||||||
|
#ifndef SKIP_APP_SETTINGS_LOADING
|
||||||
|
std::string assetFolder = get("Aya", "ContentFolder").value_or(appDir + "/content");
|
||||||
|
SetAssetFolder(assetFolder);
|
||||||
|
SetTrustCheckURL(get("Aya", "TrustCheckUrl").value_or(""));
|
||||||
|
|
||||||
|
if (GetTrustCheckURL().empty())
|
||||||
|
SetUsingTrustCheck(false);
|
||||||
|
|
||||||
|
if (has("Aya", "InsecureMode"))
|
||||||
|
{
|
||||||
|
std::string insecureMode = get("Aya", "InsecureMode").value_or("false");
|
||||||
|
boost::algorithm::to_lower(insecureMode);
|
||||||
|
SetInsecureMode(insecureMode == "true" || insecureMode == "1" || insecureMode == "yes");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has("Aya", "VerboseLogging"))
|
||||||
|
{
|
||||||
|
std::string verboseLogging = get("Aya", "VerboseLogging").value_or("false");
|
||||||
|
boost::algorithm::to_lower(verboseLogging);
|
||||||
|
SetVerboseLogging(verboseLogging == "true" || verboseLogging == "1" || verboseLogging == "yes");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasGroup("Instance"))
|
||||||
|
{
|
||||||
|
SetBaseURL(get("Instance", "BaseUrl").value_or(""));
|
||||||
|
SetInstanceAccessKey(get("Instance", "AccessKey").value_or(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetBaseURL().empty())
|
||||||
|
{
|
||||||
|
SetUsingInstance(false);
|
||||||
|
SetFetchLocalClientSettings(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetUsingInstance(true);
|
||||||
|
SetFetchLocalClientSettings(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasGroup("MasterServer"))
|
||||||
|
{
|
||||||
|
SetMasterServerURL(get("MasterServer", "BaseUrl").value_or(""));
|
||||||
|
SetMasterServerKey(get("MasterServer", "AccessKey").value_or(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
SetUsingMasterServer(!GetMasterServerURL().empty());
|
||||||
|
#endif // SKIP_APP_SETTINGS_LOADING
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> AppSettings::get(const std::string& group, const std::string& key) const
|
||||||
|
{
|
||||||
|
m_pSettings->beginGroup(QString::fromStdString(group));
|
||||||
|
QVariant value = m_pSettings->value(QString::fromStdString(key));
|
||||||
|
m_pSettings->endGroup();
|
||||||
|
|
||||||
|
if (value.isValid() && value.type() == QVariant::String)
|
||||||
|
return value.toString().toStdString();
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppSettings::hasGroup(const std::string& group)
|
||||||
|
{
|
||||||
|
return m_pSettings->childGroups().contains(QString::fromStdString(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppSettings::has(const std::string& group, const std::string& key)
|
||||||
|
{
|
||||||
|
if (!this->hasGroup(group))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
m_pSettings->beginGroup(QString::fromStdString(group));
|
||||||
|
QVariant value = m_pSettings->value(QString::fromStdString(key));
|
||||||
|
m_pSettings->endGroup();
|
||||||
|
|
||||||
|
return value.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppSettings::set(const std::string& group, const std::string& key, const std::string& value)
|
||||||
|
{
|
||||||
|
m_pSettings->beginGroup(QString::fromStdString(group));
|
||||||
|
m_pSettings->setValue(QString::fromStdString(key), QString::fromStdString(value));
|
||||||
|
m_pSettings->endGroup();
|
||||||
|
}
|
||||||
21
client/common/AppSettings.hpp
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QSettings>
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
class AppSettings
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AppSettings(const std::string& appDir);
|
||||||
|
virtual ~AppSettings();
|
||||||
|
|
||||||
|
bool load();
|
||||||
|
std::optional<std::string> get(const std::string& group, const std::string& key) const;
|
||||||
|
bool hasGroup(const std::string& group);
|
||||||
|
bool has(const std::string& group, const std::string& key);
|
||||||
|
void set(const std::string& group, const std::string& key, const std::string& value);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QSettings* m_pSettings;
|
||||||
|
std::string appDir;
|
||||||
|
};
|
||||||
0
client/common/AppView.cpp
Normal file
0
client/common/AppView.hpp
Normal file
635
client/common/CrashManager.cpp
Normal file
@@ -0,0 +1,635 @@
|
|||||||
|
#ifndef AYA_STUDIO
|
||||||
|
|
||||||
|
#undef min
|
||||||
|
#undef max
|
||||||
|
|
||||||
|
#include "format_string.hpp"
|
||||||
|
#include "AyaFormat.hpp"
|
||||||
|
#include "Debug.hpp"
|
||||||
|
#include "boost.hpp"
|
||||||
|
#include "Utility/StandardOut.hpp"
|
||||||
|
#include "Utility/FileSystem.hpp"
|
||||||
|
#include "Utility/Guid.hpp"
|
||||||
|
#include "Utility/Http.hpp"
|
||||||
|
#include "Utility/Statistics.hpp"
|
||||||
|
|
||||||
|
#include "debugAssert.hpp"
|
||||||
|
#include <direct.h>
|
||||||
|
|
||||||
|
#include "atltime.h"
|
||||||
|
#include "atlfile.h"
|
||||||
|
|
||||||
|
#include "TaskScheduler.hpp"
|
||||||
|
#include "DumpErrorUploader.hpp"
|
||||||
|
#include "Log.hpp"
|
||||||
|
#include "FastLog.hpp"
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <DbgHelp.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <atlbase.h>
|
||||||
|
#include <boost/format.hpp>
|
||||||
|
|
||||||
|
LOGGROUP(CrashReporterInit)
|
||||||
|
|
||||||
|
bool LogManager::logsEnabled = false; // to be honest we only really need crash dmps & the logs outputted are not working
|
||||||
|
|
||||||
|
MainLogManager* LogManager::mainLogManager = NULL;
|
||||||
|
|
||||||
|
Aya::mutex MainLogManager::fastLogChannelsLock;
|
||||||
|
|
||||||
|
static const ATL::CPath& DoGetPath()
|
||||||
|
{
|
||||||
|
static ATL::CPath path(CString(Aya::FileSystem::getUserDirectory(true, Aya::DirAppData, "logs").native().c_str()));
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void InitPath()
|
||||||
|
{
|
||||||
|
DoGetPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetAppVersion()
|
||||||
|
{
|
||||||
|
CVersionInfo vi;
|
||||||
|
FASTLOG1(FLog::CrashReporterInit, "Getting app version, module handle: %p", _AtlBaseModule.m_hInst);
|
||||||
|
vi.Load(_AtlBaseModule.m_hInst);
|
||||||
|
return vi.GetFileVersionAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ATL::CPath& LogManager::GetLogPath() const
|
||||||
|
{
|
||||||
|
static boost::once_flag flag = BOOST_ONCE_INIT;
|
||||||
|
boost::call_once(&InitPath, flag);
|
||||||
|
return DoGetPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string LogManager::GetLogPathString() const
|
||||||
|
{
|
||||||
|
CStringA path = (LPCTSTR)GetLogPath();
|
||||||
|
return std::string(path.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainLogManager::fastLogMessage(FLog::Channel id, const char* message)
|
||||||
|
{
|
||||||
|
Aya::mutex::scoped_lock lock(fastLogChannelsLock);
|
||||||
|
|
||||||
|
if (mainLogManager)
|
||||||
|
{
|
||||||
|
if (id >= mainLogManager->fastLogChannels.size())
|
||||||
|
mainLogManager->fastLogChannels.resize(id + 1, NULL);
|
||||||
|
|
||||||
|
if (mainLogManager->fastLogChannels[id] == NULL)
|
||||||
|
{
|
||||||
|
|
||||||
|
mainLogManager->fastLogChannels[id] = new Aya::Log(mainLogManager->getFastLogFileName(id).c_str(), "Log Channel");
|
||||||
|
}
|
||||||
|
|
||||||
|
mainLogManager->fastLogChannels[id]->writeEntry(Aya::Log::Information, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string MainLogManager::getSessionId()
|
||||||
|
{
|
||||||
|
std::string id = guid;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string MainLogManager::getCrashEventName()
|
||||||
|
{
|
||||||
|
#ifdef WIN32
|
||||||
|
FASTLOG(FLog::CrashReporterInit, "Getting crash event name");
|
||||||
|
std::string path = GetLogPathString();
|
||||||
|
|
||||||
|
std::string fileName = "log_";
|
||||||
|
fileName += getSessionId();
|
||||||
|
fileName += " ";
|
||||||
|
|
||||||
|
fileName += GetAppVersion();
|
||||||
|
fileName += crashEventExtention;
|
||||||
|
|
||||||
|
path.append(fileName);
|
||||||
|
|
||||||
|
return path;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string MainLogManager::getLogFileName()
|
||||||
|
{
|
||||||
|
#ifdef WIN32
|
||||||
|
std::string path = GetLogPathString();
|
||||||
|
|
||||||
|
std::string fileName = "log_";
|
||||||
|
fileName += getSessionId();
|
||||||
|
fileName += ".txt";
|
||||||
|
|
||||||
|
path.append(fileName);
|
||||||
|
|
||||||
|
return path;
|
||||||
|
#endif
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string MainLogManager::getFastLogFileName(FLog::Channel channelId)
|
||||||
|
{
|
||||||
|
#ifdef WIN32
|
||||||
|
std::string path = GetLogPathString();
|
||||||
|
std::string filename = Aya::format("log_%s_%d.txt", getSessionId().c_str(), channelId);
|
||||||
|
|
||||||
|
path.append(filename);
|
||||||
|
|
||||||
|
return path;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string MainLogManager::MakeLogFileName(const char* postfix)
|
||||||
|
{
|
||||||
|
#ifdef WIN32
|
||||||
|
std::string path = GetLogPathString();
|
||||||
|
|
||||||
|
std::string fileName = "log_";
|
||||||
|
fileName += getSessionId();
|
||||||
|
fileName += postfix;
|
||||||
|
fileName += ".txt";
|
||||||
|
|
||||||
|
path.append(fileName);
|
||||||
|
|
||||||
|
return path;
|
||||||
|
#endif
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ThreadLogManager::getLogFileName()
|
||||||
|
{
|
||||||
|
#ifdef WIN32
|
||||||
|
std::string fileName = mainLogManager->getLogFileName();
|
||||||
|
std::string id = Aya::format("_%s_%d", name.c_str(), threadID);
|
||||||
|
fileName.insert(fileName.size() - 4, id);
|
||||||
|
return fileName;
|
||||||
|
#endif
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Aya::Log* LogManager::getLog()
|
||||||
|
{
|
||||||
|
if (!logsEnabled)
|
||||||
|
return NULL;
|
||||||
|
if (log == NULL)
|
||||||
|
{
|
||||||
|
log = new Aya::Log(getLogFileName().c_str(), name.c_str());
|
||||||
|
// TODO: delete an old log that isn't in use
|
||||||
|
}
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Aya::Log* MainLogManager::provideLog()
|
||||||
|
{
|
||||||
|
if (GetCurrentThreadId() == threadID)
|
||||||
|
return this->getLog();
|
||||||
|
|
||||||
|
return ThreadLogManager::getCurrent()->getLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#include <process.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <io.h>
|
||||||
|
|
||||||
|
#define MAX_CONSOLE_LINES 250;
|
||||||
|
|
||||||
|
HANDLE g_hConsoleOut; // Handle to debug console
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
RobloxCrashReporter::RobloxCrashReporter(const char* outputPath, const char* appName, const char* crashExtention)
|
||||||
|
{
|
||||||
|
controls.minidumpType = MiniDumpWithDataSegs;
|
||||||
|
|
||||||
|
controls.minidumpType |= MiniDumpWithIndirectlyReferencedMemory;
|
||||||
|
|
||||||
|
// null terminate just in case long paths & make safe
|
||||||
|
strncpy(controls.pathToMinidump, outputPath, sizeof(controls.pathToMinidump) - 1);
|
||||||
|
controls.pathToMinidump[sizeof(controls.pathToMinidump) - 1] = '\0';
|
||||||
|
|
||||||
|
strncpy(controls.appName, appName, sizeof(controls.appName) - 1);
|
||||||
|
controls.appName[sizeof(controls.appName) - 1] = '\0';
|
||||||
|
|
||||||
|
strncpy(controls.appVersion, GetAppVersion().c_str(), sizeof(controls.appVersion) - 1);
|
||||||
|
controls.appVersion[sizeof(controls.appVersion) - 1] = '\0';
|
||||||
|
|
||||||
|
strncpy(controls.crashExtention, crashExtention, sizeof(controls.crashExtention) - 1);
|
||||||
|
controls.crashExtention[sizeof(controls.crashExtention) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RobloxCrashReporter::silent;
|
||||||
|
|
||||||
|
LONG RobloxCrashReporter::ProcessException(struct _EXCEPTION_POINTERS* info, bool noMsg)
|
||||||
|
{
|
||||||
|
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE, "StartProcessException...");
|
||||||
|
|
||||||
|
LONG result = __super::ProcessException(info, noMsg);
|
||||||
|
static bool showedMessage = silent;
|
||||||
|
if (!showedMessage && !noMsg)
|
||||||
|
{
|
||||||
|
showedMessage = true;
|
||||||
|
::MessageBoxA(NULL, "An unexpected error occurred and " AYA_PROJECT_NAME " needs to quit. We're sorry!", AYA_PROJECT_NAME " Crash", MB_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE, "DoneProcessException");
|
||||||
|
|
||||||
|
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE, "Uploading .crashevent...");
|
||||||
|
DumpErrorUploader::UploadCrashEventFile(info);
|
||||||
|
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE, "Done uploading .crashevent...");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RobloxCrashReporter::logEvent(const char* msg)
|
||||||
|
{
|
||||||
|
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainLogManager::WriteCrashDump()
|
||||||
|
{
|
||||||
|
std::string appName = "log_";
|
||||||
|
appName += getSessionId();
|
||||||
|
crashReporter.reset(new RobloxCrashReporter(GetLogPathString().c_str(), appName.c_str(), crashExtention));
|
||||||
|
crashReporter->Start();
|
||||||
|
};
|
||||||
|
|
||||||
|
bool MainLogManager::CreateFakeCrashDump()
|
||||||
|
{
|
||||||
|
if (!crashReporter)
|
||||||
|
{
|
||||||
|
// start the service if not started.
|
||||||
|
WriteCrashDump();
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, write FastLog
|
||||||
|
char dumpFilepath[_MAX_PATH];
|
||||||
|
if (FAILED(crashReporter->GenerateDmpFileName(dumpFilepath, _MAX_PATH, true)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FLog::WriteFastLogDump(dumpFilepath, 2000);
|
||||||
|
|
||||||
|
if (FAILED(crashReporter->GenerateDmpFileName(dumpFilepath, _MAX_PATH)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE hFile = CreateFileA(dumpFilepath, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||||
|
if (hFile == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD cb;
|
||||||
|
WriteFile(hFile, "Fake", 5, &cb, NULL);
|
||||||
|
|
||||||
|
CloseHandle(hFile);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainLogManager::EnableImmediateCrashUpload(bool enabled)
|
||||||
|
{
|
||||||
|
if (crashReporter)
|
||||||
|
{
|
||||||
|
crashReporter->EnableImmediateUpload(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainLogManager::DisableHangReporting()
|
||||||
|
{
|
||||||
|
if (crashReporter)
|
||||||
|
{
|
||||||
|
crashReporter->DisableHangReporting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainLogManager::NotifyFGThreadAlive()
|
||||||
|
{
|
||||||
|
if (crashReporter)
|
||||||
|
{
|
||||||
|
#if 0
|
||||||
|
// for debugging only:
|
||||||
|
static int alivecount = 0;
|
||||||
|
if(alivecount++ % 60 == 0)
|
||||||
|
{
|
||||||
|
CString eventMessage;
|
||||||
|
eventMessage.Format("FGAlive %d", alivecount);
|
||||||
|
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE, eventMessage);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
crashReporter->NotifyAlive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void purecallHandler(void)
|
||||||
|
{
|
||||||
|
#ifdef _DEBUG
|
||||||
|
_CrtDbgBreak();
|
||||||
|
#endif
|
||||||
|
// Cause a crash
|
||||||
|
AYACRASH();
|
||||||
|
}
|
||||||
|
|
||||||
|
MainLogManager::MainLogManager(LPCTSTR productName, const char* crashExtention, const char* crashEventExtention)
|
||||||
|
: LogManager("Aya")
|
||||||
|
, crashExtention(crashExtention)
|
||||||
|
, crashEventExtention(crashEventExtention)
|
||||||
|
, gameState(MainLogManager::GameState::UN_INITIALIZED)
|
||||||
|
{
|
||||||
|
Aya::Guid::generateRBXGUID(guid);
|
||||||
|
|
||||||
|
AYAASSERT(mainLogManager == NULL);
|
||||||
|
mainLogManager = this;
|
||||||
|
|
||||||
|
Aya::Log::setLogProvider(this);
|
||||||
|
|
||||||
|
Aya::setAssertionHook(&MainLogManager::handleDebugAssert);
|
||||||
|
Aya::setFailureHook(&MainLogManager::handleFailure);
|
||||||
|
|
||||||
|
_set_purecall_handler(purecallHandler);
|
||||||
|
|
||||||
|
FLog::SetExternalLogFunc(fastLogMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
MainLogManager* LogManager::getMainLogManager()
|
||||||
|
{
|
||||||
|
return mainLogManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThreadLogManager::ThreadLogManager()
|
||||||
|
: LogManager(Aya::get_thread_name())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadLogManager::~ThreadLogManager() {}
|
||||||
|
|
||||||
|
static float getThisYearTimeInMinutes(SYSTEMTIME time)
|
||||||
|
{
|
||||||
|
return (time.wMonth * 43829.0639f) + (time.wDay * 1440) + (time.wHour * 60) + time.wMinute;
|
||||||
|
}
|
||||||
|
|
||||||
|
MainLogManager::~MainLogManager()
|
||||||
|
{
|
||||||
|
Aya::mutex::scoped_lock lock(fastLogChannelsLock);
|
||||||
|
|
||||||
|
FLog::SetExternalLogFunc(NULL);
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < fastLogChannels.size(); i++)
|
||||||
|
delete fastLogChannels[i];
|
||||||
|
|
||||||
|
mainLogManager = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LogManager::~LogManager()
|
||||||
|
{
|
||||||
|
if (log != NULL)
|
||||||
|
{
|
||||||
|
std::string logFile = log->logFile;
|
||||||
|
delete log; // this will close the file so that we can move it
|
||||||
|
log = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline HRESULT WINAPI RbxReportError(
|
||||||
|
const CLSID& clsid, LPCSTR lpszDesc, DWORD dwHelpID, LPCSTR lpszHelpFile, const IID& iid = GUID_NULL, HRESULT hRes = 0)
|
||||||
|
{
|
||||||
|
ATLASSERT(lpszDesc != NULL);
|
||||||
|
if (lpszDesc == NULL)
|
||||||
|
return E_POINTER;
|
||||||
|
|
||||||
|
USES_CONVERSION_EX;
|
||||||
|
CString strDesc(lpszDesc);
|
||||||
|
CComBSTR desc = strDesc.AllocSysString(); // Convert CString to BSTR
|
||||||
|
if (desc == NULL)
|
||||||
|
return E_OUTOFMEMORY;
|
||||||
|
|
||||||
|
CComBSTR helpFile = NULL;
|
||||||
|
if (lpszHelpFile != NULL)
|
||||||
|
{
|
||||||
|
CString strHelpFile(lpszHelpFile);
|
||||||
|
helpFile = strHelpFile.AllocSysString(); // Convert CString to BSTR
|
||||||
|
if (helpFile == NULL)
|
||||||
|
return E_OUTOFMEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AtlSetErrorInfo(clsid, desc.Detach(), dwHelpID, helpFile.Detach(), iid, hRes, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline HRESULT WINAPI RbxReportError(
|
||||||
|
const CLSID& clsid, UINT nID, const IID& iid = GUID_NULL, HRESULT hRes = 0, HINSTANCE hInst = _AtlBaseModule.GetResourceInstance())
|
||||||
|
{
|
||||||
|
return AtlSetErrorInfo(clsid, (LPCOLESTR)MAKEINTRESOURCE(nID), 0, NULL, iid, hRes, hInst);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline HRESULT WINAPI RbxReportError(const CLSID& clsid, UINT nID, DWORD dwHelpID, LPCOLESTR lpszHelpFile, const IID& iid = GUID_NULL,
|
||||||
|
HRESULT hRes = 0, HINSTANCE hInst = _AtlBaseModule.GetResourceInstance())
|
||||||
|
{
|
||||||
|
return AtlSetErrorInfo(clsid, (LPCOLESTR)MAKEINTRESOURCE(nID), dwHelpID, lpszHelpFile, iid, hRes, hInst);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline HRESULT WINAPI RbxReportError(const CLSID& clsid, LPCSTR lpszDesc, const IID& iid = GUID_NULL, HRESULT hRes = 0)
|
||||||
|
{
|
||||||
|
return RbxReportError(clsid, lpszDesc, 0, NULL, iid, hRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline HRESULT WINAPI RbxReportError(const CLSID& clsid, LPCOLESTR lpszDesc, const IID& iid = GUID_NULL, HRESULT hRes = 0)
|
||||||
|
{
|
||||||
|
return AtlSetErrorInfo(clsid, lpszDesc, 0, NULL, iid, hRes, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline HRESULT WINAPI RbxReportError(
|
||||||
|
const CLSID& clsid, LPCOLESTR lpszDesc, DWORD dwHelpID, LPCOLESTR lpszHelpFile, const IID& iid = GUID_NULL, HRESULT hRes = 0)
|
||||||
|
{
|
||||||
|
return AtlSetErrorInfo(clsid, lpszDesc, dwHelpID, lpszHelpFile, iid, hRes, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT LogManager::ReportCOMError(const CLSID& clsid, LPCOLESTR lpszDesc, HRESULT hRes)
|
||||||
|
{
|
||||||
|
return RbxReportError(clsid, lpszDesc, GUID_NULL, hRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT LogManager::ReportCOMError(const CLSID& clsid, LPCSTR lpszDesc, HRESULT hRes)
|
||||||
|
{
|
||||||
|
return RbxReportError(clsid, lpszDesc, GUID_NULL, hRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT LogManager::ReportCOMError(const CLSID& clsid, HRESULT hRes)
|
||||||
|
{
|
||||||
|
std::string message = Aya::format("HRESULT 0x%X", hRes);
|
||||||
|
LogManager::ReportEvent(EVENTLOG_ERROR_TYPE, message.c_str());
|
||||||
|
return RbxReportError(clsid, message.c_str(), GUID_NULL, hRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _MFC_VER
|
||||||
|
HRESULT LogManager::ReportCOMError(const CLSID& clsid, CException* exception)
|
||||||
|
{
|
||||||
|
CString fullError;
|
||||||
|
HRESULT hr = COleException::Process(exception);
|
||||||
|
CString sError;
|
||||||
|
if (exception->GetErrorMessage(sError.GetBuffer(1024), 1023))
|
||||||
|
{
|
||||||
|
sError.ReleaseBuffer();
|
||||||
|
fullError.Format("%s (0x%X)", sError, hr);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
fullError.Format("Error 0x%X", hr);
|
||||||
|
|
||||||
|
LogManager::ReportEvent(EVENTLOG_ERROR_TYPE, fullError);
|
||||||
|
return RbxReportError(clsid, fullError, GUID_NULL, hr);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool MainLogManager::handleG3DDebugAssert(
|
||||||
|
const char* _expression, const std::string& message, const char* filename, int lineNumber, bool useGuiPrompt)
|
||||||
|
{
|
||||||
|
return handleDebugAssert(_expression, filename, lineNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MainLogManager::handleDebugAssert(const char* expression, const char* filename, int lineNumber)
|
||||||
|
{
|
||||||
|
#ifdef _DEBUG
|
||||||
|
LogManager::ReportEvent(EVENTLOG_WARNING_TYPE,
|
||||||
|
std::string("Assertion failed: " + std::string(expression) + "\n" + std::string(filename) + "(" + std::to_string(lineNumber) + ")").c_str());
|
||||||
|
AYACRASH();
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool MainLogManager::handleG3DFailure(const char* _expression, const std::string& message, const char* filename, int lineNumber, bool useGuiPrompt)
|
||||||
|
{
|
||||||
|
return handleFailure(_expression, filename, lineNumber);
|
||||||
|
}
|
||||||
|
bool MainLogManager::handleFailure(const char* expression, const char* filename, int lineNumber)
|
||||||
|
{
|
||||||
|
#ifdef _DEBUG
|
||||||
|
_CrtDbgBreak();
|
||||||
|
#endif
|
||||||
|
// Cause a crash
|
||||||
|
AYACRASH();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT LogManager::ReportExceptionAsCOMError(const CLSID& clsid, std::exception const& exp)
|
||||||
|
{
|
||||||
|
return ReportCOMError(clsid, exp.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogManager::ReportException(std::exception const& exp)
|
||||||
|
{
|
||||||
|
Aya::StandardOut::singleton()->print(Aya::MESSAGE_ERROR, exp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogManager::ReportLastError(LPCSTR message)
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
Aya::StandardOut::singleton()->printf(Aya::MESSAGE_ERROR, "%s, GetLastError=%d", message, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogManager::ReportEvent(WORD type, LPCSTR message)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case EVENTLOG_SUCCESS:
|
||||||
|
Aya::StandardOut::singleton()->printf(Aya::MESSAGE_INFO, "%s", message);
|
||||||
|
break;
|
||||||
|
case EVENTLOG_ERROR_TYPE:
|
||||||
|
Aya::StandardOut::singleton()->printf(Aya::MESSAGE_ERROR, "%s", message);
|
||||||
|
break;
|
||||||
|
case EVENTLOG_INFORMATION_TYPE:
|
||||||
|
Aya::StandardOut::singleton()->printf(Aya::MESSAGE_INFO, "%s", message);
|
||||||
|
break;
|
||||||
|
case EVENTLOG_AUDIT_SUCCESS:
|
||||||
|
Aya::StandardOut::singleton()->printf(Aya::MESSAGE_INFO, "%s", message);
|
||||||
|
break;
|
||||||
|
case EVENTLOG_AUDIT_FAILURE:
|
||||||
|
Aya::StandardOut::singleton()->printf(Aya::MESSAGE_ERROR, "%s", message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#ifdef _DEBUG
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case EVENTLOG_SUCCESS:
|
||||||
|
ATLTRACE("EVENTLOG_SUCCESS %s\n", message);
|
||||||
|
break;
|
||||||
|
case EVENTLOG_ERROR_TYPE:
|
||||||
|
ATLTRACE("EVENTLOG_ERROR_TYPE %s\n", message);
|
||||||
|
break;
|
||||||
|
case EVENTLOG_INFORMATION_TYPE:
|
||||||
|
ATLTRACE("EVENTLOG_INFORMATION_TYPE %s\n", message);
|
||||||
|
break;
|
||||||
|
case EVENTLOG_AUDIT_SUCCESS:
|
||||||
|
ATLTRACE("EVENTLOG_AUDIT_SUCCESS %s\n", message);
|
||||||
|
break;
|
||||||
|
case EVENTLOG_AUDIT_FAILURE:
|
||||||
|
ATLTRACE("EVENTLOG_AUDIT_FAILURE %s\n", message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogManager::ReportEvent(WORD type, LPCSTR message, LPCSTR fileName, int lineNumber)
|
||||||
|
{
|
||||||
|
// CString m;
|
||||||
|
// m.Format(convert_s2w("%s\n%s(%d)"), message, fileName, lineNumber);
|
||||||
|
// LogManager::ReportEvent(type, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _MFC_VER
|
||||||
|
void LogManager::ReportEvent(WORD type, HRESULT hr, LPCSTR fileName, int lineNumber)
|
||||||
|
{
|
||||||
|
COleException e;
|
||||||
|
e.m_sc = hr;
|
||||||
|
TCHAR s[1024];
|
||||||
|
e.GetErrorMessage(s, 1024);
|
||||||
|
|
||||||
|
CString m;
|
||||||
|
m.Format("HRESULT = %d: %s\n%s(%d)", hr, s, fileName, lineNumber);
|
||||||
|
LogManager::ReportEvent(type, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace log_detail
|
||||||
|
{
|
||||||
|
boost::once_flag once_init = BOOST_ONCE_INIT;
|
||||||
|
static boost::thread_specific_ptr<ThreadLogManager>* ts;
|
||||||
|
void init(void)
|
||||||
|
{
|
||||||
|
static boost::thread_specific_ptr<ThreadLogManager> value;
|
||||||
|
ts = &value;
|
||||||
|
}
|
||||||
|
} // namespace log_detail
|
||||||
|
|
||||||
|
ThreadLogManager* ThreadLogManager::getCurrent()
|
||||||
|
{
|
||||||
|
boost::call_once(log_detail::init, log_detail::once_init);
|
||||||
|
ThreadLogManager* logManager = log_detail::ts->get();
|
||||||
|
if (!logManager)
|
||||||
|
{
|
||||||
|
logManager = new ThreadLogManager();
|
||||||
|
log_detail::ts->reset(logManager);
|
||||||
|
}
|
||||||
|
return logManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
157
client/common/CrashManager.hpp
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#if defined(_WIN32) || defined(_WIN64)
|
||||||
|
|
||||||
|
#include "intrusive_ptr_target.hpp"
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <atlpath.h>
|
||||||
|
#include "Log.hpp"
|
||||||
|
#include "boost.hpp"
|
||||||
|
#include "Utility/Exception.hpp"
|
||||||
|
#include "CrashReporter.hpp"
|
||||||
|
#include "boost/scoped_ptr.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include "threadsafe.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LogManager
|
||||||
|
{
|
||||||
|
Aya::Log* log;
|
||||||
|
static bool logsEnabled;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
const DWORD threadID;
|
||||||
|
std::string name;
|
||||||
|
static class MainLogManager* mainLogManager;
|
||||||
|
|
||||||
|
public:
|
||||||
|
Aya::Log* getLog();
|
||||||
|
|
||||||
|
static MainLogManager* getMainLogManager();
|
||||||
|
#ifdef _MFC_VER
|
||||||
|
static HRESULT ReportCOMError(const CLSID& clsid, CException* exception);
|
||||||
|
#endif
|
||||||
|
static HRESULT ReportCOMError(const CLSID& clsid, HRESULT hRes);
|
||||||
|
static HRESULT ReportCOMError(const CLSID& clsid, LPCOLESTR lpszDesc, HRESULT hRes = 0);
|
||||||
|
static HRESULT ReportCOMError(const CLSID& clsid, LPCSTR lpszDesc, HRESULT hRes = 0);
|
||||||
|
static HRESULT ReportExceptionAsCOMError(const CLSID& clsid, std::exception const& exp);
|
||||||
|
static void ReportException(std::exception const& exp);
|
||||||
|
static void ReportLastError(LPCSTR message);
|
||||||
|
static void ReportEvent(WORD type, LPCSTR message);
|
||||||
|
static void ReportEvent(WORD type, LPCSTR message, LPCSTR fileName, int lineNumber);
|
||||||
|
static void ReportEvent(WORD type, HRESULT hr, LPCSTR fileName, int lineNumber);
|
||||||
|
|
||||||
|
const ATL::CPath& GetLogPath() const;
|
||||||
|
const std::string GetLogPathString() const;
|
||||||
|
|
||||||
|
virtual ~LogManager();
|
||||||
|
|
||||||
|
virtual std::string getLogFileName() = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
LogManager(const char* name)
|
||||||
|
: log(NULL)
|
||||||
|
, name(name)
|
||||||
|
, threadID(GetCurrentThreadId()) {};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class RobloxCrashReporter : public CrashReporter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static bool silent;
|
||||||
|
RobloxCrashReporter(const char* outputPath, const char* appName, const char* crashExtention);
|
||||||
|
LONG ProcessException(struct _EXCEPTION_POINTERS* info, bool noMsg);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/*override*/ void logEvent(const char* msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
class MainLogManager
|
||||||
|
: public Aya::ILogProvider
|
||||||
|
, public LogManager
|
||||||
|
{
|
||||||
|
boost::scoped_ptr<RobloxCrashReporter> crashReporter;
|
||||||
|
std::vector<Aya::Log*> fastLogChannels;
|
||||||
|
static Aya::mutex fastLogChannelsLock;
|
||||||
|
const char* crashExtention;
|
||||||
|
const char* crashEventExtention;
|
||||||
|
|
||||||
|
public:
|
||||||
|
MainLogManager(LPCTSTR productName, const char* crashExtention, const char* crashEventExtention); // used for main thread
|
||||||
|
~MainLogManager();
|
||||||
|
|
||||||
|
Aya::Log* provideLog();
|
||||||
|
virtual std::string getLogFileName();
|
||||||
|
std::string getFastLogFileName(FLog::Channel channelId);
|
||||||
|
std::string MakeLogFileName(const char* postfix);
|
||||||
|
|
||||||
|
bool hasErrorLogs() const;
|
||||||
|
|
||||||
|
std::vector<std::string> gatherScriptCrashLogs();
|
||||||
|
|
||||||
|
void WriteCrashDump();
|
||||||
|
|
||||||
|
// triggers upload of log files on next start.
|
||||||
|
bool CreateFakeCrashDump();
|
||||||
|
|
||||||
|
void NotifyFGThreadAlive(); // for deadlock reporting. call every second.
|
||||||
|
void DisableHangReporting();
|
||||||
|
|
||||||
|
void EnableImmediateCrashUpload(bool enabled);
|
||||||
|
|
||||||
|
// returns HEX string that will be part of all the log/dumps output for this session.
|
||||||
|
std::string getSessionId();
|
||||||
|
|
||||||
|
std::string getCrashEventName();
|
||||||
|
|
||||||
|
static void fastLogMessage(FLog::Channel id, const char* message);
|
||||||
|
|
||||||
|
enum GameState
|
||||||
|
{
|
||||||
|
UN_INITIALIZED = 0,
|
||||||
|
IN_GAME,
|
||||||
|
LEAVE_GAME
|
||||||
|
};
|
||||||
|
GameState getGameState()
|
||||||
|
{
|
||||||
|
return gameState;
|
||||||
|
}
|
||||||
|
void setGameLoaded()
|
||||||
|
{
|
||||||
|
gameState = GameState::IN_GAME;
|
||||||
|
}
|
||||||
|
void setLeaveGame()
|
||||||
|
{
|
||||||
|
gameState = GameState::LEAVE_GAME;
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
GameState gameState;
|
||||||
|
std::string guid;
|
||||||
|
static bool handleDebugAssert(const char* expression, const char* filename, int lineNumber);
|
||||||
|
static bool handleFailure(const char* expression, const char* filename, int lineNumber);
|
||||||
|
|
||||||
|
static bool handleG3DFailure(const char* _expression, const std::string& message, const char* filename, int lineNumber,
|
||||||
|
/*bool& ignoreAlways,*/
|
||||||
|
bool useGuiPrompt);
|
||||||
|
|
||||||
|
static bool handleG3DDebugAssert(const char* _expression, const std::string& message, const char* filename, int lineNumber,
|
||||||
|
/*bool& ignoreAlways,*/
|
||||||
|
bool useGuiPrompt);
|
||||||
|
};
|
||||||
|
|
||||||
|
class ThreadLogManager : public LogManager
|
||||||
|
{
|
||||||
|
ThreadLogManager();
|
||||||
|
|
||||||
|
public:
|
||||||
|
static ThreadLogManager* getCurrent();
|
||||||
|
virtual ~ThreadLogManager();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual std::string getLogFileName();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
0
client/common/GfxView.cpp
Normal file
12
client/common/GfxView.hpp
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWindow>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
class GfxView : public QWindow
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
QPoint lastMousePosition;
|
||||||
|
QPoint lastMousePositionScaled;
|
||||||
|
};
|
||||||
87
client/common/GrayChatBar.cpp
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#include "GrayChatBar.hpp"
|
||||||
|
|
||||||
|
#define PLACEHOLDER_TEXT "To chat click here or press the \"/\" key"
|
||||||
|
|
||||||
|
GrayChatBar::GrayChatBar(QWidget* parent) : QLineEdit(parent)
|
||||||
|
{
|
||||||
|
setText(PLACEHOLDER_TEXT);
|
||||||
|
|
||||||
|
// note: this uses segoe ui, may not work on linux?
|
||||||
|
|
||||||
|
// styles:
|
||||||
|
// padding-left: 1px
|
||||||
|
// border-bottom: 5px solid #404040
|
||||||
|
// background-color: #404040 (#e6e6fa on active)
|
||||||
|
// color: #ffffc8 (white on active)
|
||||||
|
// font-weight: bold
|
||||||
|
// font-family: 'Segoe UI'
|
||||||
|
// fixed height: 21px
|
||||||
|
|
||||||
|
setStyleSheet("QLineEdit { background-color: #404040; color: #ffffc8; font-weight: bold; border-radius: 0 !important; padding: 0 !important; border: none; border-bottom: 5px solid #404040; margin: 0 !important; font-family: 'Segoe UI'; font-size: 12px; padding-left: 1px; }");
|
||||||
|
setFixedHeight(21);
|
||||||
|
setMinimumSize(QSize(0, 21));
|
||||||
|
setVisible(false);
|
||||||
|
|
||||||
|
QFont font = this->font();
|
||||||
|
font.setHintingPreference(QFont::PreferFullHinting); // for crappy aa
|
||||||
|
setFont(font);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GrayChatBar::focus()
|
||||||
|
{
|
||||||
|
setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GrayChatBar::focusInEvent(QFocusEvent* e)
|
||||||
|
{
|
||||||
|
QLineEdit::focusInEvent(e);
|
||||||
|
setStyleSheet("QLineEdit { background-color: #e6e6fa; color: black; font-weight: bold; border-radius: 0 !important; padding: 0 !important; border: none; border-bottom: 5px solid #404040; margin: 0 !important; font-family: 'Segoe UI'; font-size: 12px; padding-left: 1px; }");
|
||||||
|
|
||||||
|
if (text() == PLACEHOLDER_TEXT)
|
||||||
|
{
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GrayChatBar::focusOutEvent(QFocusEvent* e)
|
||||||
|
{
|
||||||
|
QLineEdit::focusOutEvent(e);
|
||||||
|
setStyleSheet("QLineEdit { background-color: #404040; color: #ffffc8; font-weight: bold; border-radius: 0 !important; padding: 0 !important; border: none; border-bottom: 5px solid #404040; margin: 0 !important; font-family: 'Segoe UI'; font-size: 12px; padding-left: 1px; }");
|
||||||
|
|
||||||
|
if (text() == "")
|
||||||
|
{
|
||||||
|
setText(PLACEHOLDER_TEXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GrayChatBar::keyPressEvent(QKeyEvent* e)
|
||||||
|
{
|
||||||
|
if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter)
|
||||||
|
{
|
||||||
|
Q_EMIT returnPressed();
|
||||||
|
handleEnteredText(text());
|
||||||
|
setText(PLACEHOLDER_TEXT);
|
||||||
|
clearFocus();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QLineEdit::keyPressEvent(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GrayChatBar::handleEnteredText(const QString& txt) {
|
||||||
|
Q_EMIT enteredText(txt);
|
||||||
|
if (text() == "")
|
||||||
|
{
|
||||||
|
setText(PLACEHOLDER_TEXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GrayChatBar::setVisibility(bool visible)
|
||||||
|
{
|
||||||
|
setVisible(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GrayChatBar::mousePressEvent(QMouseEvent* e) {
|
||||||
|
focus();
|
||||||
|
}
|
||||||
25
client/common/GrayChatBar.hpp
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
|
||||||
|
class GrayChatBar : public QLineEdit
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
GrayChatBar(QWidget* parent = nullptr);
|
||||||
|
void focus();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void focusInEvent(QFocusEvent* e) override;
|
||||||
|
void focusOutEvent(QFocusEvent* e) override;
|
||||||
|
void keyPressEvent(QKeyEvent* e) override;
|
||||||
|
void mousePressEvent(QMouseEvent* e) override;
|
||||||
|
void handleEnteredText(const QString& text);
|
||||||
|
void setVisibility(bool visible);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void returnPressed();
|
||||||
|
void enteredText(const QString& text);
|
||||||
|
};
|
||||||
668
client/common/SDLGameController.cpp
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
|
||||||
|
#include "SDLGameController.hpp"
|
||||||
|
|
||||||
|
#include "DataModel/DataModel.hpp"
|
||||||
|
#include "DataModel/GamepadService.hpp"
|
||||||
|
#include "DataModel/UserInputService.hpp"
|
||||||
|
#include "DataModel/ContentProvider.hpp"
|
||||||
|
|
||||||
|
#define MAX_AXIS_VALUE 32767.0f
|
||||||
|
|
||||||
|
SDLGameController::SDLGameController(shared_ptr<Aya::DataModel> newDM)
|
||||||
|
{
|
||||||
|
dataModel = newDM;
|
||||||
|
initSDL();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::initSDL()
|
||||||
|
{
|
||||||
|
if (SDL_Init(SDL_INIT_GAMEPAD | SDL_INIT_HAPTIC) != 0)
|
||||||
|
{
|
||||||
|
std::string error = SDL_GetError();
|
||||||
|
fprintf(stderr, "\nUnable to initialize SDL: %s\n", error.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aya::ContentId gameControllerDb = Aya::ContentId::fromAssets("fonts/gamecontrollerdb.txt");
|
||||||
|
std::string filePath = Aya::ContentProvider::findAsset(gameControllerDb);
|
||||||
|
|
||||||
|
if (SDL_AddGamepadMappingsFromFile(filePath.c_str()) == -1)
|
||||||
|
{
|
||||||
|
std::string error = SDL_GetError();
|
||||||
|
Aya::StandardOut::singleton()->printf(Aya::MESSAGE_ERROR, "Unable to add SDL controller mappings because %s", error.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shared_ptr<Aya::DataModel> sharedDM = dataModel.lock())
|
||||||
|
{
|
||||||
|
sharedDM->submitTask(boost::bind(&SDLGameController::bindToDataModel, this), Aya::DataModelJob::Write);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::bindToDataModel()
|
||||||
|
{
|
||||||
|
if (Aya::UserInputService* inputService = getUserInputService())
|
||||||
|
{
|
||||||
|
renderSteppedConnection = inputService->updateInputSignal.connect(boost::bind(&SDLGameController::updateControllers, this));
|
||||||
|
getSupportedGamepadKeyCodesConnection =
|
||||||
|
inputService->getSupportedGamepadKeyCodesSignal.connect(boost::bind(&SDLGameController::findAvailableGamepadKeyCodesAndSet, this, _1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Aya::HapticService* hapticService = getHapticService())
|
||||||
|
{
|
||||||
|
setEnabledVibrationMotorsConnection =
|
||||||
|
hapticService->setEnabledVibrationMotorsSignal.connect(boost::bind(&SDLGameController::setVibrationMotorsEnabled, this, _1));
|
||||||
|
setVibrationMotorConnection =
|
||||||
|
hapticService->setVibrationMotorSignal.connect(boost::bind(&SDLGameController::setVibrationMotor, this, _1, _2, _3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDLGameController::~SDLGameController()
|
||||||
|
{
|
||||||
|
renderSteppedConnection.disconnect();
|
||||||
|
getSupportedGamepadKeyCodesConnection.disconnect();
|
||||||
|
|
||||||
|
setEnabledVibrationMotorsConnection.disconnect();
|
||||||
|
setVibrationMotorConnection.disconnect();
|
||||||
|
|
||||||
|
for (boost::unordered_map<int, HapticData>::iterator iter = hapticsFromGamepadId.begin(); iter != hapticsFromGamepadId.end(); ++iter)
|
||||||
|
{
|
||||||
|
SDL_Haptic* haptic = iter->second.hapticDevice;
|
||||||
|
int hapticEffectId = iter->second.hapticEffectId;
|
||||||
|
|
||||||
|
SDL_HapticDestroyEffect(haptic, hapticEffectId);
|
||||||
|
SDL_HapticClose(haptic);
|
||||||
|
}
|
||||||
|
hapticsFromGamepadId.clear();
|
||||||
|
|
||||||
|
SDL_Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Aya::UserInputService* SDLGameController::getUserInputService()
|
||||||
|
{
|
||||||
|
if (shared_ptr<Aya::DataModel> sharedDM = dataModel.lock())
|
||||||
|
{
|
||||||
|
if (Aya::UserInputService* inputService = Aya::ServiceProvider::create<Aya::UserInputService>(sharedDM.get()))
|
||||||
|
{
|
||||||
|
return inputService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aya::HapticService* SDLGameController::getHapticService()
|
||||||
|
{
|
||||||
|
if (shared_ptr<Aya::DataModel> sharedDM = dataModel.lock())
|
||||||
|
{
|
||||||
|
if (Aya::HapticService* hapticService = Aya::ServiceProvider::create<Aya::HapticService>(sharedDM.get()))
|
||||||
|
{
|
||||||
|
return hapticService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aya::GamepadService* SDLGameController::getGamepadService()
|
||||||
|
{
|
||||||
|
if (shared_ptr<Aya::DataModel> sharedDM = dataModel.lock())
|
||||||
|
{
|
||||||
|
if (Aya::GamepadService* gamepadService = Aya::ServiceProvider::create<Aya::GamepadService>(sharedDM.get()))
|
||||||
|
{
|
||||||
|
return gamepadService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Gamepad* SDLGameController::removeControllerMapping(int joystickId)
|
||||||
|
{
|
||||||
|
SDL_Gamepad* gameController = NULL;
|
||||||
|
Aya::UserInputService* inputService = getUserInputService();
|
||||||
|
|
||||||
|
if (joystickIdToGamepadId.find(joystickId) != joystickIdToGamepadId.end())
|
||||||
|
{
|
||||||
|
int gamepadId = joystickIdToGamepadId[joystickId];
|
||||||
|
if (gamepadIdToGameController.find(gamepadId) != gamepadIdToGameController.end())
|
||||||
|
{
|
||||||
|
gameController = gamepadIdToGameController[gamepadId].second;
|
||||||
|
gamepadIdToGameController.erase(gamepadId);
|
||||||
|
|
||||||
|
if (inputService)
|
||||||
|
{
|
||||||
|
inputService->safeFireGamepadDisconnected(Aya::GamepadService::getGamepadEnumForInt(gamepadId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hapticsFromGamepadId.find(gamepadId) != hapticsFromGamepadId.end())
|
||||||
|
{
|
||||||
|
SDL_Haptic* haptic = hapticsFromGamepadId[gamepadId].hapticDevice;
|
||||||
|
int hapticEffectId = hapticsFromGamepadId[gamepadId].hapticEffectId;
|
||||||
|
|
||||||
|
SDL_HapticDestroyEffect(haptic, hapticEffectId);
|
||||||
|
SDL_HapticClose(haptic);
|
||||||
|
|
||||||
|
hapticsFromGamepadId.erase(gamepadId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gameController;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::setupControllerId(int joystickId, int gamepadId, SDL_Gamepad* pad)
|
||||||
|
{
|
||||||
|
gamepadIdToGameController[gamepadId] = std::pair<int, SDL_Gamepad*>(joystickId, pad);
|
||||||
|
joystickIdToGamepadId[joystickId] = gamepadId;
|
||||||
|
|
||||||
|
if (Aya::UserInputService* inputService = getUserInputService())
|
||||||
|
{
|
||||||
|
inputService->safeFireGamepadConnected(Aya::GamepadService::getGamepadEnumForInt(gamepadId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::addController(int gamepadId)
|
||||||
|
{
|
||||||
|
if (SDL_IsGamepad(gamepadId))
|
||||||
|
{
|
||||||
|
SDL_Gamepad* pad = SDL_OpenGamepad(gamepadId);
|
||||||
|
|
||||||
|
if (pad)
|
||||||
|
{
|
||||||
|
SDL_Joystick* joy = SDL_GetGamepadJoystick(pad);
|
||||||
|
int joystickId = SDL_GetJoystickID(joy);
|
||||||
|
|
||||||
|
setupControllerId(joystickId, gamepadId, pad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::removeController(int joystickId)
|
||||||
|
{
|
||||||
|
if (SDL_Gamepad* pad = removeControllerMapping(joystickId))
|
||||||
|
{
|
||||||
|
SDL_CloseGamepad(pad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Aya::Gamepad SDLGameController::getRbxGamepadFromJoystickId(int joystickId)
|
||||||
|
{
|
||||||
|
if (joystickIdToGamepadId.find(joystickId) != joystickIdToGamepadId.end())
|
||||||
|
{
|
||||||
|
if (Aya::GamepadService* gamepadService = getGamepadService())
|
||||||
|
{
|
||||||
|
int gamepadId = joystickIdToGamepadId[joystickId];
|
||||||
|
return gamepadService->getGamepadState(gamepadId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Aya::Gamepad();
|
||||||
|
}
|
||||||
|
|
||||||
|
Aya::KeyCode getKeyCodeFromSDLAxis(SDL_GamepadAxis sdlAxis, int& axisValueChanged)
|
||||||
|
{
|
||||||
|
switch (sdlAxis)
|
||||||
|
{
|
||||||
|
case SDL_GAMEPAD_AXIS_LEFTX:
|
||||||
|
axisValueChanged = 0;
|
||||||
|
return Aya::SDLK_GAMEPAD_THUMBSTICK1;
|
||||||
|
case SDL_GAMEPAD_AXIS_LEFTY:
|
||||||
|
axisValueChanged = 1;
|
||||||
|
return Aya::SDLK_GAMEPAD_THUMBSTICK1;
|
||||||
|
case SDL_GAMEPAD_AXIS_RIGHTX:
|
||||||
|
axisValueChanged = 0;
|
||||||
|
return Aya::SDLK_GAMEPAD_THUMBSTICK2;
|
||||||
|
case SDL_GAMEPAD_AXIS_RIGHTY:
|
||||||
|
axisValueChanged = 1;
|
||||||
|
return Aya::SDLK_GAMEPAD_THUMBSTICK2;
|
||||||
|
case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
|
||||||
|
axisValueChanged = 2;
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONL2;
|
||||||
|
case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
|
||||||
|
axisValueChanged = 2;
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONR2;
|
||||||
|
case SDL_GAMEPAD_AXIS_INVALID:
|
||||||
|
case SDL_GAMEPAD_AXIS_MAX:
|
||||||
|
return Aya::SDLK_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Aya::SDLK_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aya::KeyCode getKeyCodeFromSDLButton(SDL_GamepadButton sdlButton)
|
||||||
|
{
|
||||||
|
switch (sdlButton)
|
||||||
|
{
|
||||||
|
case SDL_GAMEPAD_BUTTON_SOUTH:
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONA;
|
||||||
|
case SDL_GAMEPAD_BUTTON_EAST:
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONB;
|
||||||
|
case SDL_GAMEPAD_BUTTON_WEST:
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONX;
|
||||||
|
case SDL_GAMEPAD_BUTTON_NORTH:
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONY;
|
||||||
|
|
||||||
|
case SDL_GAMEPAD_BUTTON_START:
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONSTART;
|
||||||
|
case SDL_GAMEPAD_BUTTON_BACK:
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONSELECT;
|
||||||
|
|
||||||
|
case SDL_GAMEPAD_BUTTON_DPAD_UP:
|
||||||
|
return Aya::SDLK_GAMEPAD_DPADUP;
|
||||||
|
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
|
||||||
|
return Aya::SDLK_GAMEPAD_DPADDOWN;
|
||||||
|
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
|
||||||
|
return Aya::SDLK_GAMEPAD_DPADLEFT;
|
||||||
|
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
|
||||||
|
return Aya::SDLK_GAMEPAD_DPADRIGHT;
|
||||||
|
|
||||||
|
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONL1;
|
||||||
|
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONR1;
|
||||||
|
|
||||||
|
case SDL_GAMEPAD_BUTTON_LEFT_STICK:
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONL3;
|
||||||
|
case SDL_GAMEPAD_BUTTON_RIGHT_STICK:
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONR3;
|
||||||
|
|
||||||
|
case SDL_GAMEPAD_BUTTON_INVALID:
|
||||||
|
case SDL_GAMEPAD_BUTTON_GUIDE:
|
||||||
|
case SDL_GAMEPAD_BUTTON_MAX:
|
||||||
|
return Aya::SDLK_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Aya::SDLK_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SDLGameController::setupHapticsForDevice(int id)
|
||||||
|
{
|
||||||
|
// already set up
|
||||||
|
if (hapticsFromGamepadId.find(id) != hapticsFromGamepadId.end())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Haptic* haptic = NULL;
|
||||||
|
|
||||||
|
// Open the device
|
||||||
|
haptic = SDL_HapticOpen(id);
|
||||||
|
if (haptic)
|
||||||
|
{
|
||||||
|
HapticData hapticData;
|
||||||
|
hapticData.hapticDevice = haptic;
|
||||||
|
hapticData.hapticEffectId = -1;
|
||||||
|
hapticData.currentLeftMotorValue = 0.0f;
|
||||||
|
hapticData.currentRightMotorValue = 0.0f;
|
||||||
|
|
||||||
|
hapticsFromGamepadId[id] = hapticData;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::setVibrationMotorsEnabled(Aya::InputObject::UserInputType gamepadType)
|
||||||
|
{
|
||||||
|
int gamepadId = getGamepadIntForEnum(gamepadType);
|
||||||
|
if (!setupHapticsForDevice(gamepadId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Haptic* haptic = hapticsFromGamepadId[gamepadId].hapticDevice;
|
||||||
|
if (haptic)
|
||||||
|
{
|
||||||
|
if (Aya::HapticService* hapticService = getHapticService())
|
||||||
|
{
|
||||||
|
hapticService->setEnabledVibrationMotors(gamepadType, Aya::HapticService::MOTOR_LARGE, true);
|
||||||
|
hapticService->setEnabledVibrationMotors(gamepadType, Aya::HapticService::MOTOR_SMALL, true);
|
||||||
|
hapticService->setEnabledVibrationMotors(gamepadType, Aya::HapticService::MOTOR_LEFTTRIGGER, false);
|
||||||
|
hapticService->setEnabledVibrationMotors(gamepadType, Aya::HapticService::MOTOR_RIGHTTRIGGER, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::setVibrationMotor(
|
||||||
|
Aya::InputObject::UserInputType gamepadType, Aya::HapticService::VibrationMotor vibrationMotor, shared_ptr<const Aya::Reflection::Tuple> args)
|
||||||
|
{
|
||||||
|
int gamepadId = getGamepadIntForEnum(gamepadType);
|
||||||
|
if (!setupHapticsForDevice(gamepadId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float newMotorValue = 0.0f;
|
||||||
|
Aya::Reflection::Variant newValue = args->values[0];
|
||||||
|
if (newValue.isFloat())
|
||||||
|
{
|
||||||
|
newMotorValue = newValue.get<float>();
|
||||||
|
newMotorValue = G3D::clamp(newMotorValue, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
else // no valid number in first position, lets bail
|
||||||
|
{
|
||||||
|
Aya::StandardOut::singleton()->printf(
|
||||||
|
Aya::MESSAGE_ERROR, "First value to HapticService:SetMotor is not a valid number (must be a number between 0-1)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::unordered_map<int, HapticData>::iterator iter = hapticsFromGamepadId.find(gamepadId);
|
||||||
|
|
||||||
|
// make sure we grab old data so we set the motors that haven't changed value
|
||||||
|
float leftMotorValue = iter->second.currentLeftMotorValue;
|
||||||
|
float rightMotorValue = iter->second.currentRightMotorValue;
|
||||||
|
|
||||||
|
if (vibrationMotor == Aya::HapticService::MOTOR_LARGE)
|
||||||
|
{
|
||||||
|
leftMotorValue = newMotorValue;
|
||||||
|
}
|
||||||
|
else if (vibrationMotor == Aya::HapticService::MOTOR_SMALL)
|
||||||
|
{
|
||||||
|
rightMotorValue = newMotorValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Haptic* haptic = iter->second.hapticDevice;
|
||||||
|
int oldEffectId = iter->second.hapticEffectId;
|
||||||
|
if (oldEffectId >= 0)
|
||||||
|
{
|
||||||
|
SDL_HapticDestroyEffect(haptic, oldEffectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftMotorValue <= 0.0f && rightMotorValue <= 0.0f)
|
||||||
|
{
|
||||||
|
HapticData hapticData;
|
||||||
|
hapticData.hapticDevice = haptic;
|
||||||
|
hapticData.hapticEffectId = -1;
|
||||||
|
hapticData.currentLeftMotorValue = 0.0f;
|
||||||
|
hapticData.currentRightMotorValue = 0.0f;
|
||||||
|
|
||||||
|
hapticsFromGamepadId[gamepadId] = hapticData;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the left/right effect
|
||||||
|
SDL_HapticEffect effect;
|
||||||
|
memset(&effect, 0, sizeof(SDL_HapticEffect)); // 0 is safe default
|
||||||
|
effect.type = SDL_HAPTIC_LEFTRIGHT;
|
||||||
|
effect.leftright.large_magnitude = 65535.0f * leftMotorValue;
|
||||||
|
effect.leftright.small_magnitude = 65535.0f * rightMotorValue;
|
||||||
|
effect.leftright.length = SDL_HAPTIC_INFINITY;
|
||||||
|
|
||||||
|
// Upload the effect
|
||||||
|
int hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
|
||||||
|
|
||||||
|
HapticData hapticData;
|
||||||
|
hapticData.hapticDevice = haptic;
|
||||||
|
hapticData.hapticEffectId = hapticEffectId;
|
||||||
|
hapticData.currentLeftMotorValue = leftMotorValue;
|
||||||
|
hapticData.currentRightMotorValue = rightMotorValue;
|
||||||
|
|
||||||
|
hapticsFromGamepadId[gamepadId] = hapticData;
|
||||||
|
|
||||||
|
if (haptic && hapticEffectId >= 0)
|
||||||
|
{
|
||||||
|
SDL_HapticRunEffect(haptic, hapticEffectId, SDL_HAPTIC_INFINITY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::refreshHapticEffects()
|
||||||
|
{
|
||||||
|
for (boost::unordered_map<int, HapticData>::iterator iter = hapticsFromGamepadId.begin(); iter != hapticsFromGamepadId.end(); ++iter)
|
||||||
|
{
|
||||||
|
SDL_Haptic* haptic = iter->second.hapticDevice;
|
||||||
|
int hapticEffectId = iter->second.hapticEffectId;
|
||||||
|
|
||||||
|
if (haptic && hapticEffectId >= 0)
|
||||||
|
{
|
||||||
|
SDL_HapticRunEffect(haptic, hapticEffectId, SDL_HAPTIC_INFINITY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::onControllerButton(const SDL_GamepadButtonEvent sdlEvent)
|
||||||
|
{
|
||||||
|
const Aya::KeyCode buttonCode = getKeyCodeFromSDLButton((SDL_GamepadButton)sdlEvent.button);
|
||||||
|
|
||||||
|
if (buttonCode == Aya::SDLK_UNKNOWN)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aya::Gamepad gamepad = getRbxGamepadFromJoystickId(sdlEvent.which);
|
||||||
|
const int buttonState = (sdlEvent.state == SDL_PRESSED) ? 1 : 0;
|
||||||
|
Aya::InputObject::UserInputState newState = (buttonState == 1) ? Aya::InputObject::INPUT_STATE_BEGIN : Aya::InputObject::INPUT_STATE_END;
|
||||||
|
|
||||||
|
if (newState == gamepad[buttonCode]->getUserInputState())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const G3D::Vector3 lastPos = gamepad[buttonCode]->getPosition();
|
||||||
|
|
||||||
|
gamepad[buttonCode]->setPosition(G3D::Vector3(0, 0, buttonState));
|
||||||
|
gamepad[buttonCode]->setDelta(gamepad[buttonCode]->getPosition() - lastPos);
|
||||||
|
gamepad[buttonCode]->setInputState(newState);
|
||||||
|
|
||||||
|
if (Aya::UserInputService* inputService = getUserInputService())
|
||||||
|
{
|
||||||
|
inputService->dangerousFireInputEvent(gamepad[buttonCode], NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::onControllerAxis(const SDL_GamepadAxisEvent sdlEvent)
|
||||||
|
{
|
||||||
|
int axisValueChanged = -1;
|
||||||
|
const Aya::KeyCode axisCode = getKeyCodeFromSDLAxis((SDL_GamepadAxis)sdlEvent.axis, axisValueChanged);
|
||||||
|
|
||||||
|
if (axisCode == Aya::SDLK_UNKNOWN)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float axisValue = sdlEvent.value;
|
||||||
|
axisValue /= MAX_AXIS_VALUE;
|
||||||
|
axisValue = G3D::clamp(axisValue, -1.0f, 1.0f);
|
||||||
|
|
||||||
|
Aya::Gamepad gamepad = getRbxGamepadFromJoystickId(sdlEvent.which);
|
||||||
|
G3D::Vector3 currentPosition = gamepad[axisCode]->getPosition();
|
||||||
|
|
||||||
|
switch (axisValueChanged)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
currentPosition.x = axisValue;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
currentPosition.y = -axisValue;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
currentPosition.z = axisValue;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
G3D::Vector3 lastPos = gamepad[axisCode]->getPosition();
|
||||||
|
if (lastPos != currentPosition)
|
||||||
|
{
|
||||||
|
gamepad[axisCode]->setPosition(currentPosition);
|
||||||
|
|
||||||
|
Aya::InputObject::UserInputState currentState = Aya::InputObject::INPUT_STATE_CHANGE;
|
||||||
|
if (currentPosition == G3D::Vector3::zero())
|
||||||
|
{
|
||||||
|
currentState = Aya::InputObject::INPUT_STATE_END;
|
||||||
|
}
|
||||||
|
else if (currentPosition.z >= 1.0f)
|
||||||
|
{
|
||||||
|
currentState = Aya::InputObject::INPUT_STATE_BEGIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
gamepad[axisCode]->setDelta(currentPosition - lastPos);
|
||||||
|
gamepad[axisCode]->setInputState(currentState);
|
||||||
|
|
||||||
|
if (Aya::UserInputService* inputService = getUserInputService())
|
||||||
|
{
|
||||||
|
inputService->dangerousFireInputEvent(gamepad[axisCode], NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::updateControllers()
|
||||||
|
{
|
||||||
|
SDL_Event sdlEvent;
|
||||||
|
|
||||||
|
while (SDL_PollEvent(&sdlEvent))
|
||||||
|
{
|
||||||
|
switch (sdlEvent.type)
|
||||||
|
{
|
||||||
|
case SDL_EVENT_GAMEPAD_ADDED:
|
||||||
|
addController(sdlEvent.gdevice.which);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||||
|
removeController(sdlEvent.gdevice.which);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
||||||
|
case SDL_EVENT_GAMEPAD_BUTTON_UP:
|
||||||
|
onControllerButton(sdlEvent.gbutton);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
|
||||||
|
onControllerAxis(sdlEvent.gaxis);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshHapticEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
Aya::KeyCode getKeyCodeFromSDLName(std::string sdlName)
|
||||||
|
{
|
||||||
|
if (sdlName.compare("a") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONA;
|
||||||
|
if (sdlName.compare("b") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONB;
|
||||||
|
if (sdlName.compare("x") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONX;
|
||||||
|
if (sdlName.compare("y") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONY;
|
||||||
|
|
||||||
|
if (sdlName.compare("back") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONSELECT;
|
||||||
|
if (sdlName.compare("start") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONSTART;
|
||||||
|
|
||||||
|
if (sdlName.compare("dpdown") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_DPADDOWN;
|
||||||
|
if (sdlName.compare("dpleft") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_DPADLEFT;
|
||||||
|
if (sdlName.compare("dpright") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_DPADRIGHT;
|
||||||
|
if (sdlName.compare("dpup") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_DPADUP;
|
||||||
|
|
||||||
|
if (sdlName.compare("leftshoulder") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONL1;
|
||||||
|
if (sdlName.compare("lefttrigger") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONL2;
|
||||||
|
if (sdlName.compare("leftstick") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONL3;
|
||||||
|
|
||||||
|
if (sdlName.compare("rightshoulder") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONR1;
|
||||||
|
if (sdlName.compare("righttrigger") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONR2;
|
||||||
|
if (sdlName.compare("rightstick") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_BUTTONR3;
|
||||||
|
|
||||||
|
if (sdlName.compare("leftx") == 0 || sdlName.compare("lefty") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_THUMBSTICK1;
|
||||||
|
|
||||||
|
if (sdlName.compare("rightx") == 0 || sdlName.compare("righty") == 0)
|
||||||
|
return Aya::SDLK_GAMEPAD_THUMBSTICK2;
|
||||||
|
|
||||||
|
return Aya::SDLK_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SDLGameController::getGamepadIntForEnum(Aya::InputObject::UserInputType gamepadType)
|
||||||
|
{
|
||||||
|
switch (gamepadType)
|
||||||
|
{
|
||||||
|
case Aya::InputObject::TYPE_GAMEPAD1:
|
||||||
|
return 0;
|
||||||
|
case Aya::InputObject::TYPE_GAMEPAD2:
|
||||||
|
return 1;
|
||||||
|
case Aya::InputObject::TYPE_GAMEPAD3:
|
||||||
|
return 2;
|
||||||
|
case Aya::InputObject::TYPE_GAMEPAD4:
|
||||||
|
return 3;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLGameController::findAvailableGamepadKeyCodesAndSet(Aya::InputObject::UserInputType gamepadType)
|
||||||
|
{
|
||||||
|
shared_ptr<const Aya::Reflection::ValueArray> availableGamepadKeyCodes = getAvailableGamepadKeyCodes(gamepadType);
|
||||||
|
if (Aya::UserInputService* inputService = getUserInputService())
|
||||||
|
{
|
||||||
|
inputService->setSupportedGamepadKeyCodes(gamepadType, availableGamepadKeyCodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shared_ptr<const Aya::Reflection::ValueArray> SDLGameController::getAvailableGamepadKeyCodes(Aya::InputObject::UserInputType gamepadType)
|
||||||
|
{
|
||||||
|
int gamepadId = getGamepadIntForEnum(gamepadType);
|
||||||
|
|
||||||
|
if (gamepadId < 0 || (gamepadIdToGameController.find(gamepadId) == gamepadIdToGameController.end()))
|
||||||
|
{
|
||||||
|
return shared_ptr<const Aya::Reflection::ValueArray>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SDL_Gamepad* gameController = gamepadIdToGameController[gamepadId].second)
|
||||||
|
{
|
||||||
|
char* mappingStr = SDL_GetGamepadMapping(gameController);
|
||||||
|
std::string gameControllerMapping(mappingStr ? mappingStr : "");
|
||||||
|
if (mappingStr) SDL_free(mappingStr);
|
||||||
|
|
||||||
|
std::istringstream controllerMappingStream(gameControllerMapping);
|
||||||
|
std::string mappingItem;
|
||||||
|
|
||||||
|
shared_ptr<Aya::Reflection::ValueArray> supportedGamepadFunctions(new Aya::Reflection::ValueArray());
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
while (std::getline(controllerMappingStream, mappingItem, ','))
|
||||||
|
{
|
||||||
|
// first two settings in mapping are hardware id and device name, don't need those
|
||||||
|
if (count > 1)
|
||||||
|
{
|
||||||
|
std::istringstream mappingStream(mappingItem);
|
||||||
|
std::string sdlName;
|
||||||
|
std::getline(mappingStream, sdlName, ':');
|
||||||
|
|
||||||
|
// platform is always last thing defined in mappings, don't need it so we are done
|
||||||
|
if (sdlName.compare("platform") == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aya::KeyCode gamepadCode = getKeyCodeFromSDLName(sdlName);
|
||||||
|
if (gamepadCode != Aya::SDLK_UNKNOWN)
|
||||||
|
{
|
||||||
|
supportedGamepadFunctions->push_back(gamepadCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return supportedGamepadFunctions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shared_ptr<const Aya::Reflection::ValueArray>();
|
||||||
|
}
|
||||||
82
client/common/SDLGameController.hpp
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <boost/shared_ptr.hpp>
|
||||||
|
#include <boost/unordered_map.hpp>
|
||||||
|
#include <boost/weak_ptr.hpp>
|
||||||
|
|
||||||
|
#include "SDL3/SDL.h"
|
||||||
|
#include "SDL3/SDL_gamepad.h"
|
||||||
|
|
||||||
|
#include "Utility/KeyCode.hpp"
|
||||||
|
#include "DataModel/InputObject.hpp"
|
||||||
|
#include "DataModel/HapticService.hpp"
|
||||||
|
|
||||||
|
namespace Aya
|
||||||
|
{
|
||||||
|
class DataModel;
|
||||||
|
class UserInputService;
|
||||||
|
|
||||||
|
class GamepadService;
|
||||||
|
|
||||||
|
typedef boost::unordered_map<Aya::KeyCode, boost::shared_ptr<Aya::InputObject>> Gamepad;
|
||||||
|
} // namespace Aya
|
||||||
|
|
||||||
|
struct HapticData
|
||||||
|
{
|
||||||
|
int hapticEffectId;
|
||||||
|
float currentLeftMotorValue;
|
||||||
|
float currentRightMotorValue;
|
||||||
|
SDL_Haptic* hapticDevice;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SDLGameController
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
boost::weak_ptr<Aya::DataModel> dataModel;
|
||||||
|
boost::unordered_map<int, std::pair<int, SDL_Gamepad*>> gamepadIdToGameController;
|
||||||
|
boost::unordered_map<int, HapticData> hapticsFromGamepadId;
|
||||||
|
boost::unordered_map<int, int> joystickIdToGamepadId;
|
||||||
|
|
||||||
|
Aya::signals::scoped_connection renderSteppedConnection;
|
||||||
|
Aya::signals::scoped_connection getSupportedGamepadKeyCodesConnection;
|
||||||
|
|
||||||
|
Aya::signals::scoped_connection setEnabledVibrationMotorsConnection;
|
||||||
|
Aya::signals::scoped_connection setVibrationMotorConnection;
|
||||||
|
|
||||||
|
void initSDL();
|
||||||
|
|
||||||
|
Aya::UserInputService* getUserInputService();
|
||||||
|
Aya::HapticService* getHapticService();
|
||||||
|
|
||||||
|
Aya::GamepadService* getGamepadService();
|
||||||
|
Aya::Gamepad getRbxGamepadFromJoystickId(int joystickId);
|
||||||
|
|
||||||
|
void setupControllerId(int joystickId, int gamepadId, SDL_Gamepad* pad);
|
||||||
|
SDL_Gamepad* removeControllerMapping(int joystickId);
|
||||||
|
|
||||||
|
int getGamepadIntForEnum(Aya::InputObject::UserInputType gamepadType);
|
||||||
|
|
||||||
|
void findAvailableGamepadKeyCodesAndSet(Aya::InputObject::UserInputType gamepadType);
|
||||||
|
boost::shared_ptr<const Aya::Reflection::ValueArray> getAvailableGamepadKeyCodes(Aya::InputObject::UserInputType gamepadType);
|
||||||
|
|
||||||
|
void bindToDataModel();
|
||||||
|
|
||||||
|
// Haptic Functions
|
||||||
|
void refreshHapticEffects();
|
||||||
|
bool setupHapticsForDevice(int id);
|
||||||
|
|
||||||
|
void setVibrationMotorsEnabled(Aya::InputObject::UserInputType gamepadType);
|
||||||
|
void setVibrationMotor(Aya::InputObject::UserInputType gamepadType, Aya::HapticService::VibrationMotor vibrationMotor,
|
||||||
|
shared_ptr<const Aya::Reflection::Tuple> args);
|
||||||
|
|
||||||
|
public:
|
||||||
|
SDLGameController(boost::shared_ptr<Aya::DataModel> newDM);
|
||||||
|
~SDLGameController();
|
||||||
|
|
||||||
|
void updateControllers();
|
||||||
|
|
||||||
|
void onControllerAxis(const SDL_GamepadAxisEvent sdlEvent);
|
||||||
|
void onControllerButton(const SDL_GamepadButtonEvent sdlEvent);
|
||||||
|
void removeController(int joystickId);
|
||||||
|
void addController(int gamepadId);
|
||||||
|
};
|
||||||