| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| #include <StdInc.h> | ||
|
|
||
| #if __has_include(<ConsoleHost.h>) && __has_include(<imgui.h>) && __has_include(<grcTexture.h>) | ||
| #include <ConsoleHost.h> | ||
| #include <CefOverlay.h> | ||
| #include <grcTexture.h> | ||
| #include <DrawCommands.h> | ||
| #include <nutsnbolts.h> | ||
|
|
||
| #include <json.hpp> | ||
|
|
||
| using json = nlohmann::json; | ||
|
|
||
| #define STB_IMAGE_IMPLEMENTATION | ||
| #include "imfd/ImFileDialog.h" | ||
|
|
||
| static std::mutex g_pfdMutex; | ||
| static std::map<std::string, std::shared_ptr<ifd::FileDialog>> g_pendingFileDialogs; | ||
| static std::set<void*> g_deleteTexQueue; | ||
|
|
||
| static InitFunction initFunction([]() | ||
| { | ||
| nui::OnInvokeNative.Connect([](const wchar_t* type, const wchar_t* arg) | ||
| { | ||
| if (wcscmp(type, L"openFileDialog") == 0) | ||
| { | ||
| if (!nui::HasMainUI()) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| std::unique_lock _(g_pfdMutex); | ||
| if (g_pendingFileDialogs.empty()) | ||
| { | ||
| ConHost::SetCursorMode(true); | ||
| } | ||
|
|
||
| auto id = std::make_shared<ifd::FileDialog>(); | ||
| id->CreateTexture = [](uint8_t* data, int width, int height, char fmt) -> void* | ||
| { | ||
| if (width > 2048 || height > 2048) | ||
| { | ||
| return nullptr; | ||
| } | ||
|
|
||
| rage::grcTextureReference reference; | ||
| memset(&reference, 0, sizeof(reference)); | ||
| reference.width = width; | ||
| reference.height = height; | ||
| reference.depth = 1; | ||
| reference.stride = width * 4; | ||
| reference.format = (fmt) ? 12 : 11; | ||
| reference.pixelData = (uint8_t*)data; | ||
|
|
||
| auto iconPtr = new void*[2]; | ||
| iconPtr[0] = rage::grcTextureFactory::getInstance()->createImage(&reference, nullptr); | ||
| iconPtr[1] = nullptr; | ||
| return iconPtr; | ||
| }; | ||
|
|
||
| id->DeleteTexture = [](void* tex) | ||
| { | ||
| g_deleteTexQueue.insert(tex); | ||
| }; | ||
|
|
||
| id->Open(ToNarrow(arg), "Select a file", "Image files (*.png;*.jpg;*.jpeg;*.bmp){.png,.jpg,.jpeg,.bmp}"); | ||
|
|
||
| g_pendingFileDialogs.insert({ ToNarrow(arg), id }); | ||
| } | ||
| }); | ||
|
|
||
| OnPostFrontendRender.Connect([]() | ||
| { | ||
| auto deleteThat = [](uintptr_t, uintptr_t) | ||
| { | ||
| std::unique_lock _(g_pfdMutex); | ||
|
|
||
| for (auto& entry : g_deleteTexQueue) | ||
| { | ||
| if (entry) | ||
| { | ||
| void** td = (void**)entry; | ||
| delete (rage::grcTexture*)td[0]; | ||
| delete[] td; | ||
| } | ||
| } | ||
|
|
||
| g_deleteTexQueue.clear(); | ||
| }; | ||
|
|
||
| if (IsOnRenderThread()) | ||
| { | ||
| deleteThat(0, 0); | ||
| } | ||
| }); | ||
|
|
||
| ConHost::OnShouldDrawGui.Connect([](bool* should) | ||
| { | ||
| *should = *should || !g_pendingFileDialogs.empty(); | ||
| }); | ||
|
|
||
| ConHost::OnDrawGui.Connect([]() | ||
| { | ||
| std::unique_lock _(g_pfdMutex); | ||
|
|
||
| if (!g_pendingFileDialogs.empty()) | ||
| { | ||
| auto nowPending = g_pendingFileDialogs; | ||
|
|
||
| for (auto& [ entryName, id ] : nowPending) | ||
| { | ||
| if (id->IsDone(entryName)) | ||
| { | ||
| // has result? | ||
| json result = nullptr; | ||
|
|
||
| if (id->HasResult()) | ||
| { | ||
| result = id->GetResult().u8string(); | ||
| } | ||
|
|
||
| id->Close(); | ||
|
|
||
| nui::PostFrameMessage("mpMenu", json::object({ | ||
| { "type", "fileDialogResult" }, | ||
| { "dialogKey", entryName }, | ||
| { "result", result } | ||
| }).dump()); | ||
|
|
||
| // remove dialog | ||
| g_pendingFileDialogs.erase(entryName); | ||
|
|
||
| if (g_pendingFileDialogs.empty()) | ||
| { | ||
| ConHost::SetCursorMode(false); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| #include "imfd/ImFileDialog-inl.h" | ||
| #endif |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| #pragma once | ||
| #include <ctime> | ||
| #include <stack> | ||
| #include <string> | ||
| #include <thread> | ||
| #include <vector> | ||
| #include <functional> | ||
| #include <filesystem> | ||
| #include <unordered_map> | ||
| #include <algorithm> // std::min, std::max | ||
|
|
||
| #define IFD_DIALOG_FILE 0 | ||
| #define IFD_DIALOG_DIRECTORY 1 | ||
| #define IFD_DIALOG_SAVE 2 | ||
|
|
||
| namespace ifd { | ||
| class FileDialog { | ||
| public: | ||
| static inline FileDialog& Instance() | ||
| { | ||
| static FileDialog ret; | ||
| return ret; | ||
| } | ||
|
|
||
| FileDialog(); | ||
| ~FileDialog(); | ||
|
|
||
| bool Save(const std::string& key, const std::string& title, const std::string& filter, const std::string& startingDir = ""); | ||
|
|
||
| bool Open(const std::string& key, const std::string& title, const std::string& filter, bool isMultiselect = false, const std::string& startingDir = ""); | ||
|
|
||
| bool IsDone(const std::string& key); | ||
|
|
||
| inline bool HasResult() { return m_result.size(); } | ||
| inline const std::filesystem::path& GetResult() { return m_result[0]; } | ||
| inline const std::vector<std::filesystem::path>& GetResults() { return m_result; } | ||
|
|
||
| void Close(); | ||
|
|
||
| void RemoveFavorite(const std::string& path); | ||
| void AddFavorite(const std::string& path); | ||
| inline const std::vector<std::string>& GetFavorites() { return m_favorites; } | ||
|
|
||
| inline void SetZoom(float z) { | ||
| m_zoom = std::min<float>(25.0f, std::max<float>(1.0f, z)); | ||
| m_refreshIconPreview(); | ||
| } | ||
| inline float GetZoom() { return m_zoom; } | ||
|
|
||
| std::function<void*(uint8_t*, int, int, char)> CreateTexture; // char -> fmt -> { 0 = BGRA, 1 = RGBA } | ||
| std::function<void(void*)> DeleteTexture; | ||
|
|
||
| class FileTreeNode { | ||
| public: | ||
| FileTreeNode(const std::string& path) { | ||
| Path = std::filesystem::u8path(path); | ||
| Read = false; | ||
| } | ||
|
|
||
| std::filesystem::path Path; | ||
| bool Read; | ||
| std::vector<FileTreeNode*> Children; | ||
| }; | ||
| class FileData { | ||
| public: | ||
| FileData(const std::filesystem::path& path); | ||
|
|
||
| std::filesystem::path Path; | ||
| bool IsDirectory; | ||
| size_t Size; | ||
| time_t DateModified; | ||
|
|
||
| bool HasIconPreview; | ||
| void* IconPreview; | ||
| uint8_t* IconPreviewData; | ||
| int IconPreviewWidth, IconPreviewHeight; | ||
| }; | ||
|
|
||
| private: | ||
| std::string m_currentKey; | ||
| std::string m_currentTitle; | ||
| std::filesystem::path m_currentDirectory; | ||
| bool m_isMultiselect; | ||
| bool m_isOpen; | ||
| uint8_t m_type; | ||
| char m_inputTextbox[1024]; | ||
| char m_pathBuffer[1024]; | ||
| char m_newEntryBuffer[1024]; | ||
| char m_searchBuffer[128]; | ||
| std::vector<std::string> m_favorites; | ||
| bool m_calledOpenPopup; | ||
| std::stack<std::filesystem::path> m_backHistory, m_forwardHistory; | ||
| float m_zoom; | ||
|
|
||
| std::vector<std::filesystem::path> m_selections; | ||
| int m_selectedFileItem; | ||
| void m_select(const std::filesystem::path& path, bool isCtrlDown = false); | ||
|
|
||
| std::vector<std::filesystem::path> m_result; | ||
| bool m_finalize(const std::string& filename = ""); | ||
|
|
||
| std::string m_filter; | ||
| std::vector<std::vector<std::string>> m_filterExtensions; | ||
| int m_filterSelection; | ||
| void m_parseFilter(const std::string& filter); | ||
|
|
||
| std::vector<int> m_iconIndices; | ||
| std::vector<std::string> m_iconFilepaths; // m_iconIndices[x] <-> m_iconFilepaths[x] | ||
| std::unordered_map<std::string, void*> m_icons; | ||
| void* m_getIcon(const std::filesystem::path& path); | ||
| void m_clearIcons(); | ||
| void m_refreshIconPreview(); | ||
| void m_clearIconPreview(); | ||
|
|
||
| std::thread* m_previewLoader; | ||
| bool m_previewLoaderRunning; | ||
| void m_stopPreviewLoader(); | ||
| void m_loadPreview(); | ||
|
|
||
| std::vector<FileTreeNode*> m_treeCache; | ||
| void m_clearTree(FileTreeNode* node); | ||
| void m_renderTree(FileTreeNode* node); | ||
|
|
||
| unsigned int m_sortColumn; | ||
| unsigned int m_sortDirection; | ||
| std::vector<FileData> m_content; | ||
| void m_setDirectory(const std::filesystem::path& p, bool addHistory = true); | ||
| void m_sortContent(unsigned int column, unsigned int sortDirection); | ||
| void m_renderContent(); | ||
|
|
||
| void m_renderPopups(); | ||
| void m_renderFileDialog(); | ||
| }; | ||
|
|
||
| static const char* GetDefaultFolderIcon(); | ||
| static const char* GetDefaultFileIcon(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| MIT License | ||
|
|
||
| Copyright (c) 2021 dfranx | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS 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 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,14 @@ | ||
| import { bindApiContribution } from "backend/api/api-contribution"; | ||
| import { bindAppContribution } from "backend/app/app-contribution"; | ||
| import { interfaces } from "inversify"; | ||
| import { GameConnectionService } from "./game-connection-service"; | ||
| import { GameService } from "./game-service"; | ||
|
|
||
| export const bindGame = (container: interfaces.Container) => { | ||
| container.bind(GameService).toSelf().inSingletonScope(); | ||
| bindAppContribution(container, GameService); | ||
| bindApiContribution(container, GameService); | ||
|
|
||
| container.bind(GameConnectionService).toSelf().inSingletonScope(); | ||
| bindAppContribution(container, GameConnectionService); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { AppContribution } from "backend/app/app-contribution"; | ||
| import { GameServerService } from "backend/game-server/game-server-service"; | ||
| import { LogService } from "backend/logger/log-service"; | ||
| import { inject, injectable } from "inversify"; | ||
| import { ServerStates } from "shared/api.types"; | ||
| import { GameStates } from "./game-contants"; | ||
| import { GameService } from "./game-service"; | ||
|
|
||
| @injectable() | ||
| export class GameConnectionService implements AppContribution { | ||
| @inject(LogService) | ||
| protected readonly logService: LogService; | ||
|
|
||
| @inject(GameService) | ||
| protected readonly gameService: GameService; | ||
|
|
||
| @inject(GameServerService) | ||
| protected readonly gameServerService: GameServerService; | ||
|
|
||
| private gameFPSLimited = false; | ||
|
|
||
| boot() { | ||
| this.gameService.onGameStateChange((gameState) => { | ||
| this.processState(gameState, this.gameServerService.getState()); | ||
| }); | ||
|
|
||
| this.gameServerService.onServerStateChange((serverState) => { | ||
| this.processState(this.gameService.getGameState(), serverState); | ||
| }); | ||
| } | ||
|
|
||
| private processState(gameState: GameStates, serverState: ServerStates) { | ||
| switch (true) { | ||
| case gameState === GameStates.READY && serverState === ServerStates.up: { | ||
| return emit('sdk:connectClientTo', '127.0.0.1:30120'); | ||
| } | ||
|
|
||
| case gameState === GameStates.CONNECTED && serverState === ServerStates.up: { | ||
| return this.unlimitFPS(); | ||
| } | ||
|
|
||
| case gameState === GameStates.UNLOADING && serverState === ServerStates.down: { | ||
| return emit('sdk:disconnectClient'); | ||
| } | ||
|
|
||
| default: { | ||
| return this.limitFPS(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private limitFPS() { | ||
| if (this.gameFPSLimited) { | ||
| return; | ||
| } | ||
|
|
||
| this.gameFPSLimited = true; | ||
| emit('sdk:setFPSLimit', 60); | ||
| } | ||
|
|
||
| private unlimitFPS() { | ||
| if (!this.gameFPSLimited) { | ||
| return; | ||
| } | ||
|
|
||
| this.gameFPSLimited = false; | ||
| emit('sdk:setFPSLimit', 0); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| // If modifying, don't forget to change it at sdk/resources/sdk-root/personality-theia/fxdk-services/src/browser/fxdk-data-service.ts as well | ||
| export enum GameStates { | ||
| NOT_RUNNING, | ||
| READY, | ||
| LOADING, | ||
| CONNECTED, | ||
| UNLOADING, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,9 +34,6 @@ | |
| 100% { | ||
| background-position-x: 100%; | ||
| } | ||
| } | ||
|
|
||
| animation: slidingStripe .5s linear infinite; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| @import "../vars.scss"; | ||
|
|
||
| .root { | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
|
|
||
| position: fixed; | ||
|
|
||
| top: 0; | ||
| left: 0; | ||
| right: 0; | ||
| bottom: 0; | ||
|
|
||
| background-color: $bgColor; | ||
|
|
||
| .text { | ||
| margin-top: $q*4; | ||
|
|
||
| @include fontSecondary; | ||
| font-size: $fs3; | ||
|
|
||
| user-select: none; | ||
| } | ||
|
|
||
| .progress { | ||
| width: 25vw; | ||
| height: $q*2; | ||
|
|
||
| margin-top: $q*4; | ||
|
|
||
| background-color: rgba($fgColor, .25); | ||
|
|
||
| overflow: hidden; | ||
|
|
||
| &.indeterminate { | ||
| background-image: linear-gradient(90deg, transparent, #{$acColor}, transparent); | ||
| background-size: 50%; | ||
|
|
||
| @keyframes slidingStripe { | ||
| 0% { | ||
| background-position-x: 0%; | ||
| } | ||
| 100% { | ||
| background-position-x: 100%; | ||
| } | ||
| } | ||
|
|
||
| animation: slidingStripe .5s linear infinite; | ||
| } | ||
|
|
||
| .bar { | ||
| height: 100%; | ||
|
|
||
| background-color: $acColor; | ||
|
|
||
| transition: width .2s ease; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import React from 'react'; | ||
| import { observer } from 'mobx-react-lite'; | ||
| import classnames from 'classnames'; | ||
| import { GameState } from 'store/GameState'; | ||
| import { GameLoadingState } from 'store/GameLoadingState'; | ||
| import { GameStates } from 'backend/game/game-contants'; | ||
| import s from './LoadScreen.module.scss'; | ||
|
|
||
| function getLoadingText() { | ||
| if (GameState.state === GameStates.READY) { | ||
| return 'The World Editor is preparing to load...'; | ||
| } | ||
|
|
||
| if (GameState.state === GameStates.UNLOADING) { | ||
| return 'The World Editor is getting ready...'; | ||
| } | ||
|
|
||
| return 'Loading World Editor...'; | ||
| } | ||
|
|
||
| export const LoadScreen = observer(function LoadScreen() { | ||
| const text = getLoadingText(); | ||
|
|
||
| const progress = GameState.state !== GameStates.LOADING | ||
| ? '0%' | ||
| : `${GameLoadingState.loadingProgress * 100}%`; | ||
| const progressClassName = classnames(s.progress, { | ||
| [s.indeterminate]: GameLoadingState.loadingProgress === 0 || GameState.state !== GameStates.LOADING, | ||
| }); | ||
|
|
||
| return ( | ||
| <div className={classnames(s.root, 'animated-background')}> | ||
| <div className={s.text}> | ||
| {text} | ||
| </div> | ||
|
|
||
| <div className={progressClassName}> | ||
| <div className={s.bar} style={{ width: progress }} /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,7 @@ | |
|
|
||
| display: flex; | ||
|
|
||
| top: 0; | ||
| left: 0; | ||
| right: 0; | ||
| bottom: 0; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| @import "../../vars.scss"; | ||
|
|
||
| .root { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
|
|
||
| height: 100%; | ||
|
|
||
| gap: $q; | ||
|
|
||
| .mode { | ||
| flex-grow: 0; | ||
|
|
||
| position: relative; | ||
|
|
||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
|
|
||
| gap: $q*2; | ||
|
|
||
| user-select: none; | ||
|
|
||
| height: $weToolbarHeight; | ||
|
|
||
| padding: 0 $q*5; | ||
|
|
||
| @include backdrop-blur; | ||
|
|
||
| font-weight: 100; | ||
| letter-spacing: 1px; | ||
|
|
||
| svg { | ||
| font-size: $fs2; | ||
| } | ||
|
|
||
| span { | ||
| position: absolute; | ||
|
|
||
| top: $weToolbarHeight + $q; | ||
|
|
||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
|
|
||
| padding: $q $q*2; | ||
|
|
||
| color: $fgColor; | ||
| background-color: rgba($bgColor, .75); | ||
| backdrop-filter: blur(20px); | ||
| border-radius: 2px; | ||
|
|
||
| @include fontPrimary; | ||
| font-size: $fs08; | ||
| font-weight: 100; | ||
| text-transform: lowercase; | ||
|
|
||
| opacity: 0; | ||
| pointer-events: none; | ||
|
|
||
| z-index: 1; | ||
|
|
||
| :global(.shortcut) { | ||
| margin-left: $q; | ||
| } | ||
| } | ||
|
|
||
| @include interactiveTransition; | ||
|
|
||
| &:hover { | ||
| background-color: $acColor; | ||
|
|
||
| span { | ||
| opacity: 1; | ||
| } | ||
| } | ||
|
|
||
| &.active { | ||
| background-color: mix(rgba($acColor, .6), rgba($fgColor, .2)); | ||
| box-shadow: 0 -2px 0 0 $acColor inset; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import React from 'react'; | ||
| import classnames from 'classnames'; | ||
| import { observer } from 'mobx-react-lite'; | ||
| import { CgArrowsExpandUpRight } from 'react-icons/cg'; | ||
| import { EditorMode, WorldEditorState } from 'personalities/WorldEditorPersonality/WorldEditorState'; | ||
| import { AiOutlineRotateRight } from 'react-icons/ai'; | ||
| import { GiResize } from 'react-icons/gi'; | ||
| import { BsBoundingBoxCircles } from 'react-icons/bs'; | ||
| import s from './ModeSelector.module.scss'; | ||
|
|
||
| export const ModeSelector = observer(function ModeSelector() { | ||
| return ( | ||
| <div className={s.root}> | ||
| <div | ||
| onClick={WorldEditorState.enableTranslation} | ||
| className={classnames(s.mode, { [s.active]: WorldEditorState.editorMode === EditorMode.TRANSLATE })} | ||
| > | ||
| <CgArrowsExpandUpRight /> | ||
| <span> | ||
| Translate | ||
| <div className="shortcut">1</div> | ||
| </span> | ||
| </div> | ||
| <div | ||
| onClick={WorldEditorState.enableRotation} | ||
| className={classnames(s.mode, { [s.active]: WorldEditorState.editorMode === EditorMode.ROTATE })} | ||
| > | ||
| <AiOutlineRotateRight /> | ||
| <span> | ||
| Rotate | ||
| <div className="shortcut">2</div> | ||
| </span> | ||
| </div> | ||
| <div | ||
| onClick={WorldEditorState.enableScaling} | ||
| className={classnames(s.mode, { [s.active]: WorldEditorState.editorMode === EditorMode.SCALE })} | ||
| > | ||
| <GiResize /> | ||
| <span> | ||
| Scale | ||
| <div className="shortcut">3</div> | ||
| </span> | ||
| </div> | ||
|
|
||
| <div className={classnames(s.mode, { [s.active]: WorldEditorState.editorLocal })} onClick={WorldEditorState.toggleEditorLocal}> | ||
| <BsBoundingBoxCircles /> | ||
| <span> | ||
| Local coords | ||
| <div className="shortcut">~</div> | ||
| </span> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }); |