Initial commit

This commit is contained in:
2025-12-17 16:47:48 +00:00
commit 13813f3363
4964 changed files with 1079753 additions and 0 deletions

75
client/app/config.js Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

71
client/app/css/icons.css Normal file

File diff suppressed because one or more lines are too long

25
client/app/index.html Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

24
client/app/src/App.svelte Normal file
View 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}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1 @@
<div class="w-100 my-1 border-t dark:border-neutral-700"></div>

View 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>

View 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}

View 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>

View 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>

View 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}

View 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}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 }

View 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 }

View File

@@ -0,0 +1,8 @@
export default {
Output: 0,
Info: 1,
Warning: 2,
Error: 3,
Sensitive: 4,
Max: 5
}

View 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">&ldquo;{instanceMotd}&rdquo;</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>

View 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>

View File

@@ -0,0 +1,3 @@
<div>
<h1 class="text-3xl font-bold">Favorites</h1>
</div>

View File

@@ -0,0 +1,7 @@
<script>
let { transport } = $props();
</script>
<div>
<h1 class="text-3xl font-bold">Browse Roblox Levels</h1>
</div>

View File

@@ -0,0 +1,7 @@
<script>
let { transport } = $props();
</script>
<div>
<h1 class="text-3xl font-bold">Manage Packages</h1>
</div>

View 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>

View 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>

View 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}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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}

View 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")

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,5 @@
<RCC version="1.0">
<qresource>
<file>icon.ico</file>
</qresource>
</RCC>

View 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

View 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"

View 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);
}

View 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};
};
*/

View 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();
}

View 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();
}

View 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;
};

View File

View File

View 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

View 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

View File

12
client/common/GfxView.hpp Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
#include <QWindow>
#include <QEvent>
#include <QPoint>
class GfxView : public QWindow
{
private:
QPoint lastMousePosition;
QPoint lastMousePositionScaled;
};

View 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();
}

View 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);
};

View 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>();
}

View 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);
};

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChromiumFrame Initialized</title>
<style>
@font-face {
font-family: "Source Sans Pro";
font-style: normal;
font-weight: 200 900;
src: url("./fonts/SourceSansPro-Regular.ttf") format("ttf");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #111;
color: #fff;
font-family: "Source Sans Pro", sans-serif;
}
.container {
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<p><img src="./textures/Aya.png" width="150" /></p>
<h1>ChromiumFrame Initialized</h1>
<p>Reference the Lua API for instructions on how to use ChromiumFrames.</p>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More