early-access version 2064
This commit is contained in:
parent
5c707603d4
commit
4d09dce011
17 changed files with 177 additions and 33 deletions
|
@ -1,7 +1,7 @@
|
||||||
yuzu emulator early access
|
yuzu emulator early access
|
||||||
=============
|
=============
|
||||||
|
|
||||||
This is the source code for early-access 2062.
|
This is the source code for early-access 2064.
|
||||||
|
|
||||||
## Legal Notice
|
## Legal Notice
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,6 @@ else()
|
||||||
|
|
||||||
if (ARCHITECTURE_x86_64)
|
if (ARCHITECTURE_x86_64)
|
||||||
add_compile_options("-mcx16")
|
add_compile_options("-mcx16")
|
||||||
add_compile_options("-mbranches-within-32B-boundaries")
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
|
if (APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
|
||||||
|
|
|
@ -84,9 +84,7 @@ FileSys::StorageId GetStorageIdForFrontendSlot(
|
||||||
}
|
}
|
||||||
|
|
||||||
void KProcessDeleter(Kernel::KProcess* process) {
|
void KProcessDeleter(Kernel::KProcess* process) {
|
||||||
if (process) {
|
|
||||||
process->Destroy();
|
process->Destroy();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using KProcessPtr = std::unique_ptr<Kernel::KProcess, decltype(&KProcessDeleter)>;
|
using KProcessPtr = std::unique_ptr<Kernel::KProcess, decltype(&KProcessDeleter)>;
|
||||||
|
@ -324,6 +322,7 @@ struct System::Impl {
|
||||||
kernel.Shutdown();
|
kernel.Shutdown();
|
||||||
memory.Reset();
|
memory.Reset();
|
||||||
applet_manager.ClearAll();
|
applet_manager.ClearAll();
|
||||||
|
// TODO: The main process should be freed based on KAutoObject ref counting.
|
||||||
main_process.reset();
|
main_process.reset();
|
||||||
|
|
||||||
LOG_DEBUG(Core, "Shutdown OK");
|
LOG_DEBUG(Core, "Shutdown OK");
|
||||||
|
|
|
@ -273,6 +273,10 @@ VirtualFile VfsDirectory::GetFile(std::string_view name) const {
|
||||||
return iter == files.end() ? nullptr : *iter;
|
return iter == files.end() ? nullptr : *iter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileTimeStampRaw VfsDirectory::GetFileTimeStamp([[maybe_unused]] std::string_view path) const {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
VirtualDir VfsDirectory::GetSubdirectory(std::string_view name) const {
|
VirtualDir VfsDirectory::GetSubdirectory(std::string_view name) const {
|
||||||
const auto& subs = GetSubdirectories();
|
const auto& subs = GetSubdirectories();
|
||||||
const auto iter = std::find_if(subs.begin(), subs.end(),
|
const auto iter = std::find_if(subs.begin(), subs.end(),
|
||||||
|
|
|
@ -199,6 +199,9 @@ public:
|
||||||
// file with name.
|
// file with name.
|
||||||
virtual VirtualFile GetFile(std::string_view name) const;
|
virtual VirtualFile GetFile(std::string_view name) const;
|
||||||
|
|
||||||
|
// Returns a struct containing the file's timestamp.
|
||||||
|
virtual FileTimeStampRaw GetFileTimeStamp(std::string_view path) const;
|
||||||
|
|
||||||
// Returns a vector containing all of the subdirectories in this directory.
|
// Returns a vector containing all of the subdirectories in this directory.
|
||||||
virtual std::vector<VirtualDir> GetSubdirectories() const = 0;
|
virtual std::vector<VirtualDir> GetSubdirectories() const = 0;
|
||||||
// Returns the directory with name matching name. Returns nullptr if directory dosen't have a
|
// Returns the directory with name matching name. Returns nullptr if directory dosen't have a
|
||||||
|
|
|
@ -13,6 +13,13 @@
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "core/file_sys/vfs_real.h"
|
#include "core/file_sys/vfs_real.h"
|
||||||
|
|
||||||
|
// For FileTimeStampRaw
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#define stat _stat64
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace FileSys {
|
namespace FileSys {
|
||||||
|
|
||||||
namespace FS = Common::FS;
|
namespace FS = Common::FS;
|
||||||
|
@ -392,6 +399,28 @@ std::vector<VirtualFile> RealVfsDirectory::GetFiles() const {
|
||||||
return IterateEntries<RealVfsFile, VfsFile>();
|
return IterateEntries<RealVfsFile, VfsFile>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileTimeStampRaw RealVfsDirectory::GetFileTimeStamp(std::string_view path_) const {
|
||||||
|
const auto full_path = FS::SanitizePath(path + '/' + std::string(path_));
|
||||||
|
const auto fs_path = std::filesystem::path{FS::ToU8String(full_path)};
|
||||||
|
struct stat file_status;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
const auto stat_result = _wstat64(fs_path.c_str(), &file_status);
|
||||||
|
#else
|
||||||
|
const auto stat_result = stat(fs_path.c_str(), &file_status);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (stat_result != 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
.created{static_cast<u64>(file_status.st_ctime)},
|
||||||
|
.accessed{static_cast<u64>(file_status.st_atime)},
|
||||||
|
.modified{static_cast<u64>(file_status.st_mtime)},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<VirtualDir> RealVfsDirectory::GetSubdirectories() const {
|
std::vector<VirtualDir> RealVfsDirectory::GetSubdirectories() const {
|
||||||
return IterateEntries<RealVfsDirectory, VfsDirectory>();
|
return IterateEntries<RealVfsDirectory, VfsDirectory>();
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,7 @@ public:
|
||||||
VirtualDir CreateDirectoryRelative(std::string_view relative_path) override;
|
VirtualDir CreateDirectoryRelative(std::string_view relative_path) override;
|
||||||
bool DeleteSubdirectoryRecursive(std::string_view name) override;
|
bool DeleteSubdirectoryRecursive(std::string_view name) override;
|
||||||
std::vector<VirtualFile> GetFiles() const override;
|
std::vector<VirtualFile> GetFiles() const override;
|
||||||
|
FileTimeStampRaw GetFileTimeStamp(std::string_view path) const override;
|
||||||
std::vector<VirtualDir> GetSubdirectories() const override;
|
std::vector<VirtualDir> GetSubdirectories() const override;
|
||||||
bool IsWritable() const override;
|
bool IsWritable() const override;
|
||||||
bool IsReadable() const override;
|
bool IsReadable() const override;
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
namespace FileSys {
|
namespace FileSys {
|
||||||
|
|
||||||
class VfsDirectory;
|
class VfsDirectory;
|
||||||
|
@ -18,4 +20,11 @@ using VirtualDir = std::shared_ptr<VfsDirectory>;
|
||||||
using VirtualFile = std::shared_ptr<VfsFile>;
|
using VirtualFile = std::shared_ptr<VfsFile>;
|
||||||
using VirtualFilesystem = std::shared_ptr<VfsFilesystem>;
|
using VirtualFilesystem = std::shared_ptr<VfsFilesystem>;
|
||||||
|
|
||||||
|
struct FileTimeStampRaw {
|
||||||
|
u64 created{};
|
||||||
|
u64 accessed{};
|
||||||
|
u64 modified{};
|
||||||
|
u64 padding{};
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace FileSys
|
} // namespace FileSys
|
||||||
|
|
|
@ -3,26 +3,27 @@
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
#include "core/core.h"
|
||||||
#include "core/hle/ipc_helpers.h"
|
#include "core/hle/ipc_helpers.h"
|
||||||
#include "core/hle/kernel/hle_ipc.h"
|
#include "core/hle/kernel/hle_ipc.h"
|
||||||
|
#include "core/hle/kernel/k_event.h"
|
||||||
#include "core/hle/service/audio/audin_u.h"
|
#include "core/hle/service/audio/audin_u.h"
|
||||||
|
|
||||||
namespace Service::Audio {
|
namespace Service::Audio {
|
||||||
|
|
||||||
class IAudioIn final : public ServiceFramework<IAudioIn> {
|
IAudioIn::IAudioIn(Core::System& system_)
|
||||||
public:
|
: ServiceFramework{system_, "IAudioIn"}, buffer_event{system_.Kernel()} {
|
||||||
explicit IAudioIn(Core::System& system_) : ServiceFramework{system_, "IAudioIn"} {
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
static const FunctionInfo functions[] = {
|
static const FunctionInfo functions[] = {
|
||||||
{0, nullptr, "GetAudioInState"},
|
{0, nullptr, "GetAudioInState"},
|
||||||
{1, nullptr, "Start"},
|
{1, &IAudioIn::Start, "Start"},
|
||||||
{2, nullptr, "Stop"},
|
{2, nullptr, "Stop"},
|
||||||
{3, nullptr, "AppendAudioInBuffer"},
|
{3, nullptr, "AppendAudioInBuffer"},
|
||||||
{4, nullptr, "RegisterBufferEvent"},
|
{4, &IAudioIn::RegisterBufferEvent, "RegisterBufferEvent"},
|
||||||
{5, nullptr, "GetReleasedAudioInBuffer"},
|
{5, nullptr, "GetReleasedAudioInBuffer"},
|
||||||
{6, nullptr, "ContainsAudioInBuffer"},
|
{6, nullptr, "ContainsAudioInBuffer"},
|
||||||
{7, nullptr, "AppendUacInBuffer"},
|
{7, nullptr, "AppendUacInBuffer"},
|
||||||
{8, nullptr, "AppendAudioInBufferAuto"},
|
{8, &IAudioIn::AppendAudioInBufferAuto, "AppendAudioInBufferAuto"},
|
||||||
{9, nullptr, "GetReleasedAudioInBuffersAuto"},
|
{9, nullptr, "GetReleasedAudioInBuffersAuto"},
|
||||||
{10, nullptr, "AppendUacInBufferAuto"},
|
{10, nullptr, "AppendUacInBufferAuto"},
|
||||||
{11, nullptr, "GetAudioInBufferCount"},
|
{11, nullptr, "GetAudioInBufferCount"},
|
||||||
|
@ -33,8 +34,34 @@ public:
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
RegisterHandlers(functions);
|
RegisterHandlers(functions);
|
||||||
}
|
|
||||||
};
|
Kernel::KAutoObject::Create(std::addressof(buffer_event));
|
||||||
|
buffer_event.Initialize("IAudioIn:BufferEvent");
|
||||||
|
}
|
||||||
|
|
||||||
|
IAudioIn::~IAudioIn() = default;
|
||||||
|
|
||||||
|
void IAudioIn::Start(Kernel::HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service_Audio, "(STUBBED) called");
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IAudioIn::RegisterBufferEvent(Kernel::HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service_Audio, "(STUBBED) called");
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2, 1};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushCopyObjects(buffer_event.GetReadableEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
void IAudioIn::AppendAudioInBufferAuto(Kernel::HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service_Audio, "(STUBBED) called");
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
AudInU::AudInU(Core::System& system_) : ServiceFramework{system_, "audin:u"} {
|
AudInU::AudInU(Core::System& system_) : ServiceFramework{system_, "audin:u"} {
|
||||||
// clang-format off
|
// clang-format off
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/hle/kernel/k_event.h"
|
||||||
#include "core/hle/service/service.h"
|
#include "core/hle/service/service.h"
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
|
@ -16,6 +17,19 @@ class HLERequestContext;
|
||||||
|
|
||||||
namespace Service::Audio {
|
namespace Service::Audio {
|
||||||
|
|
||||||
|
class IAudioIn final : public ServiceFramework<IAudioIn> {
|
||||||
|
public:
|
||||||
|
explicit IAudioIn(Core::System& system_);
|
||||||
|
~IAudioIn() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Start(Kernel::HLERequestContext& ctx);
|
||||||
|
void RegisterBufferEvent(Kernel::HLERequestContext& ctx);
|
||||||
|
void AppendAudioInBufferAuto(Kernel::HLERequestContext& ctx);
|
||||||
|
|
||||||
|
Kernel::KEvent buffer_event;
|
||||||
|
};
|
||||||
|
|
||||||
class AudInU final : public ServiceFramework<AudInU> {
|
class AudInU final : public ServiceFramework<AudInU> {
|
||||||
public:
|
public:
|
||||||
explicit AudInU(Core::System& system_);
|
explicit AudInU(Core::System& system_);
|
||||||
|
|
|
@ -261,6 +261,18 @@ ResultVal<FileSys::EntryType> VfsDirectoryServiceWrapper::GetEntryType(
|
||||||
return FileSys::ERROR_PATH_NOT_FOUND;
|
return FileSys::ERROR_PATH_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResultVal<FileSys::FileTimeStampRaw> VfsDirectoryServiceWrapper::GetFileTimeStampRaw(
|
||||||
|
const std::string& path) const {
|
||||||
|
auto dir = GetDirectoryRelativeWrapped(backing, Common::FS::GetParentPath(path));
|
||||||
|
if (dir == nullptr) {
|
||||||
|
return FileSys::ERROR_PATH_NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (GetEntryType(path).Failed()) {
|
||||||
|
return FileSys::ERROR_PATH_NOT_FOUND;
|
||||||
|
}
|
||||||
|
return MakeResult(dir->GetFileTimeStamp(Common::FS::GetFilename(path)));
|
||||||
|
}
|
||||||
|
|
||||||
FileSystemController::FileSystemController(Core::System& system_) : system{system_} {}
|
FileSystemController::FileSystemController(Core::System& system_) : system{system_} {}
|
||||||
|
|
||||||
FileSystemController::~FileSystemController() = default;
|
FileSystemController::~FileSystemController() = default;
|
||||||
|
|
|
@ -240,6 +240,12 @@ public:
|
||||||
*/
|
*/
|
||||||
ResultVal<FileSys::EntryType> GetEntryType(const std::string& path) const;
|
ResultVal<FileSys::EntryType> GetEntryType(const std::string& path) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timestamp of the specified path
|
||||||
|
* @return The timestamp of the specified path or error code
|
||||||
|
*/
|
||||||
|
ResultVal<FileSys::FileTimeStampRaw> GetFileTimeStampRaw(const std::string& path) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
FileSys::VirtualDir backing;
|
FileSys::VirtualDir backing;
|
||||||
};
|
};
|
||||||
|
|
|
@ -326,7 +326,7 @@ public:
|
||||||
{11, &IFileSystem::GetFreeSpaceSize, "GetFreeSpaceSize"},
|
{11, &IFileSystem::GetFreeSpaceSize, "GetFreeSpaceSize"},
|
||||||
{12, &IFileSystem::GetTotalSpaceSize, "GetTotalSpaceSize"},
|
{12, &IFileSystem::GetTotalSpaceSize, "GetTotalSpaceSize"},
|
||||||
{13, &IFileSystem::CleanDirectoryRecursively, "CleanDirectoryRecursively"},
|
{13, &IFileSystem::CleanDirectoryRecursively, "CleanDirectoryRecursively"},
|
||||||
{14, nullptr, "GetFileTimeStampRaw"},
|
{14, &IFileSystem::GetFileTimeStampRaw, "GetFileTimeStampRaw"},
|
||||||
{15, nullptr, "QueryEntry"},
|
{15, nullptr, "QueryEntry"},
|
||||||
};
|
};
|
||||||
RegisterHandlers(functions);
|
RegisterHandlers(functions);
|
||||||
|
@ -501,6 +501,24 @@ public:
|
||||||
rb.Push(size.get_total_size());
|
rb.Push(size.get_total_size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GetFileTimeStampRaw(Kernel::HLERequestContext& ctx) {
|
||||||
|
const auto file_buffer = ctx.ReadBuffer();
|
||||||
|
const std::string name = Common::StringFromBuffer(file_buffer);
|
||||||
|
|
||||||
|
LOG_WARNING(Service_FS, "(Partial Implementation) called. file={}", name);
|
||||||
|
|
||||||
|
auto result = backend.GetFileTimeStampRaw(name);
|
||||||
|
if (result.Failed()) {
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(result.Code());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 10};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushRaw(*result);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
VfsDirectoryServiceWrapper backend;
|
VfsDirectoryServiceWrapper backend;
|
||||||
SizeGetter size;
|
SizeGetter size;
|
||||||
|
|
|
@ -15,7 +15,7 @@ public:
|
||||||
explicit IService(Core::System& system_) : ServiceFramework{system_, "ngct:u"} {
|
explicit IService(Core::System& system_) : ServiceFramework{system_, "ngct:u"} {
|
||||||
// clang-format off
|
// clang-format off
|
||||||
static const FunctionInfo functions[] = {
|
static const FunctionInfo functions[] = {
|
||||||
{0, nullptr, "Match"},
|
{0, &IService::Match, "Match"},
|
||||||
{1, &IService::Filter, "Filter"},
|
{1, &IService::Filter, "Filter"},
|
||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
@ -24,6 +24,19 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void Match(Kernel::HLERequestContext& ctx) {
|
||||||
|
const auto buffer = ctx.ReadBuffer();
|
||||||
|
const auto text = Common::StringFromFixedZeroTerminatedBuffer(
|
||||||
|
reinterpret_cast<const char*>(buffer.data()), buffer.size());
|
||||||
|
|
||||||
|
LOG_WARNING(Service_NGCT, "(STUBBED) called, text={}", text);
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
// Return false since we don't censor anything
|
||||||
|
rb.Push(false);
|
||||||
|
}
|
||||||
|
|
||||||
void Filter(Kernel::HLERequestContext& ctx) {
|
void Filter(Kernel::HLERequestContext& ctx) {
|
||||||
const auto buffer = ctx.ReadBuffer();
|
const auto buffer = ctx.ReadBuffer();
|
||||||
const auto text = Common::StringFromFixedZeroTerminatedBuffer(
|
const auto text = Common::StringFromFixedZeroTerminatedBuffer(
|
||||||
|
|
|
@ -477,7 +477,13 @@ void EmitSetSampleMask(EmitContext& ctx, Id value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmitSetFragDepth(EmitContext& ctx, Id value) {
|
void EmitSetFragDepth(EmitContext& ctx, Id value) {
|
||||||
|
if (!ctx.runtime_info.convert_depth_mode) {
|
||||||
ctx.OpStore(ctx.frag_depth, value);
|
ctx.OpStore(ctx.frag_depth, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Id unit{ctx.Const(0.5f)};
|
||||||
|
const Id new_depth{ctx.OpFma(ctx.F32[1], value, unit, unit)};
|
||||||
|
ctx.OpStore(ctx.frag_depth, new_depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmitGetZFlag(EmitContext&) {
|
void EmitGetZFlag(EmitContext&) {
|
||||||
|
|
|
@ -293,6 +293,8 @@ void ShaderCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading,
|
||||||
}};
|
}};
|
||||||
LoadPipelines(stop_loading, shader_cache_filename, CACHE_VERSION, load_compute, load_graphics);
|
LoadPipelines(stop_loading, shader_cache_filename, CACHE_VERSION, load_compute, load_graphics);
|
||||||
|
|
||||||
|
LOG_INFO(Render_OpenGL, "Total Pipeline Count: {}", state.total);
|
||||||
|
|
||||||
std::unique_lock lock{state.mutex};
|
std::unique_lock lock{state.mutex};
|
||||||
callback(VideoCore::LoadCallbackStage::Build, 0, state.total);
|
callback(VideoCore::LoadCallbackStage::Build, 0, state.total);
|
||||||
state.has_loaded = true;
|
state.has_loaded = true;
|
||||||
|
|
|
@ -447,6 +447,8 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
|
||||||
VideoCommon::LoadPipelines(stop_loading, pipeline_cache_filename, CACHE_VERSION, load_compute,
|
VideoCommon::LoadPipelines(stop_loading, pipeline_cache_filename, CACHE_VERSION, load_compute,
|
||||||
load_graphics);
|
load_graphics);
|
||||||
|
|
||||||
|
LOG_INFO(Render_Vulkan, "Total Pipeline Count: {}", state.total);
|
||||||
|
|
||||||
std::unique_lock lock{state.mutex};
|
std::unique_lock lock{state.mutex};
|
||||||
callback(VideoCore::LoadCallbackStage::Build, 0, state.total);
|
callback(VideoCore::LoadCallbackStage::Build, 0, state.total);
|
||||||
state.has_loaded = true;
|
state.has_loaded = true;
|
||||||
|
|
Loading…
Reference in a new issue