mirror of
https://github.com/YimMenu/YimMenuV2.git
synced 2026-10-10 03:43:15 +08:00
Legacy yim outfit editor (#865)
* add legacy yim outfit editor * refractor code
This commit is contained in:
@@ -25,6 +25,12 @@ namespace YimMenu
|
||||
return std::string(start, end);
|
||||
}
|
||||
|
||||
inline std::string TrimString(char* str)
|
||||
{
|
||||
std::string t = str;
|
||||
return TrimString(t);
|
||||
}
|
||||
|
||||
inline void StrCpySafe(char* dest, const char* src, int dest_size)
|
||||
{
|
||||
auto len = dest_size - 1;
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#include "Outfit.hpp"
|
||||
#include "game/backend/Self.hpp"
|
||||
#include "game/gta/Natives.hpp"
|
||||
#include "core/frontend/Notifications.hpp"
|
||||
#include <fstream>
|
||||
#include "core/util/Strings.hpp"
|
||||
|
||||
namespace YimMenu
|
||||
{
|
||||
namespace Outfit
|
||||
{
|
||||
void OutfitEditor::CheckBoundsDrawable(ComponentData& item, const int lower)
|
||||
{
|
||||
if (item.drawable_id > item.drawable_id_max)
|
||||
item.drawable_id = item.drawable_id_max;
|
||||
if (item.drawable_id < lower)
|
||||
item.drawable_id = lower;
|
||||
}
|
||||
|
||||
void OutfitEditor::CheckBoundsTexture(ComponentData& item, const int lower)
|
||||
{
|
||||
if (item.texture_id > item.texture_id_max)
|
||||
item.texture_id = item.texture_id_max;
|
||||
if (item.texture_id < lower)
|
||||
item.texture_id = lower;
|
||||
}
|
||||
|
||||
void OutfitEditor::SetSelfOutfit(OutfitComponents components, OutfitProps props, bool applyHair)
|
||||
{
|
||||
for (auto t : components.items)
|
||||
{
|
||||
auto item = t.second;
|
||||
|
||||
auto palette_var = item.palette_var;
|
||||
if (item.palette_var == -1)
|
||||
palette_var = PED::GET_PED_PALETTE_VARIATION(Self::GetPed().GetHandle(), t.first);
|
||||
|
||||
if (!applyHair && t.first == 2)
|
||||
continue;
|
||||
|
||||
PED::SET_PED_COMPONENT_VARIATION(Self::GetPed().GetHandle(), t.first, item.drawable_id, item.texture_id, palette_var);
|
||||
}
|
||||
|
||||
PED::CLEAR_ALL_PED_PROPS(Self::GetPed().GetHandle(), 0);
|
||||
|
||||
for (auto& t : props.items)
|
||||
{
|
||||
auto item = t.second;
|
||||
PED::SET_PED_PROP_INDEX(Self::GetPed().GetHandle(), t.first, item.drawable_id, item.texture_id, TRUE, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Folder OutfitEditor::CheckFolder(std::string folderName)
|
||||
{
|
||||
return FileMgr::GetProjectFolder("./saved_outfits/" + folderName);
|
||||
}
|
||||
|
||||
void OutfitEditor::ApplyOutfitFromJson(std::string folderName, std::string fileName, bool applyHair)
|
||||
{
|
||||
const auto file = CheckFolder(folderName).GetFile(fileName).Path();
|
||||
|
||||
if (!std::filesystem::exists(file))
|
||||
{
|
||||
Notifications::Show("Outfit", "File does not exist.", NotificationType::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
std::ifstream file_stream(file);
|
||||
nlohmann::json j;
|
||||
|
||||
try
|
||||
{
|
||||
file_stream >> j;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
LOG(WARNING) << e.what();
|
||||
}
|
||||
file_stream.close();
|
||||
|
||||
OutfitComponents components;
|
||||
OutfitProps props;
|
||||
|
||||
if (j["model"] != Self::GetPed().GetModel())
|
||||
{
|
||||
Notifications::Show("Oufit", "Ped models different.", NotificationType::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& item : j["components"].items())
|
||||
{
|
||||
std::stringstream ss(item.key());
|
||||
int id = 0;
|
||||
ss >> id;
|
||||
components.items[id].drawable_id = item.value()["drawable_id"];
|
||||
components.items[id].texture_id = item.value()["texture_id"];
|
||||
if (auto palette_var = item.value()["palette_var"]; !palette_var.is_null())
|
||||
components.items[id].palette_var = palette_var;
|
||||
}
|
||||
for (auto& item : j["props"].items())
|
||||
{
|
||||
std::stringstream ss(item.key());
|
||||
int id = 0;
|
||||
ss >> id;
|
||||
props.items[id].drawable_id = item.value()["drawable_id"];
|
||||
props.items[id].texture_id = item.value()["texture_id"];
|
||||
}
|
||||
|
||||
SetSelfOutfit(components, props, applyHair);
|
||||
}
|
||||
|
||||
void OutfitEditor::SaveOutfit(std::string fileName, std::string folder)
|
||||
{
|
||||
OutfitComponents components;
|
||||
OutfitProps props;
|
||||
|
||||
nlohmann::json j;
|
||||
nlohmann::json j_components;
|
||||
nlohmann::json j_props;
|
||||
|
||||
ReplaceString(fileName, ".", ""); // filename say "bob.." will throw relative path error from Folder::GetFile
|
||||
fileName += ".json";
|
||||
|
||||
auto model = Self::GetPed().GetModel();
|
||||
auto ped = Self::GetPed().GetHandle();
|
||||
|
||||
for (auto& item : components.items)
|
||||
{
|
||||
nlohmann::json tmp;
|
||||
tmp["drawable_id"] = PED::GET_PED_DRAWABLE_VARIATION(ped, item.first);
|
||||
tmp["texture_id"] = PED::GET_PED_TEXTURE_VARIATION(ped, item.first);
|
||||
tmp["palette_var"] = PED::GET_PED_PALETTE_VARIATION(ped, item.first);
|
||||
j_components[std::to_string(item.first)] = tmp;
|
||||
}
|
||||
|
||||
for (auto& item : props.items)
|
||||
{
|
||||
nlohmann::json tmp;
|
||||
tmp["drawable_id"] = PED::GET_PED_PROP_INDEX(ped, item.first, 0);
|
||||
tmp["texture_id"] = PED::GET_PED_PROP_TEXTURE_INDEX(ped, item.first);
|
||||
j_props[std::to_string(item.first)] = tmp;
|
||||
}
|
||||
|
||||
j["components"] = j_components;
|
||||
j["props"] = j_props;
|
||||
j["model"] = model;
|
||||
|
||||
auto path = CheckFolder(folder).GetFile(fileName).Path();
|
||||
std::ofstream o(path);
|
||||
o << std::setw(4) << j << std::endl;
|
||||
o.close();
|
||||
}
|
||||
|
||||
void OutfitEditor::RefreshList(std::string folderName, std::vector<std::string>& folders, std::vector<std::string>& files)
|
||||
{
|
||||
folders.clear();
|
||||
|
||||
const auto file_path = CheckFolder();
|
||||
for (const auto& directory_entry : std::filesystem::directory_iterator(file_path.Path()))
|
||||
if (directory_entry.is_directory())
|
||||
folders.push_back(directory_entry.path().filename().generic_string());
|
||||
|
||||
files.clear();
|
||||
|
||||
const auto file_path2 = CheckFolder(folderName);
|
||||
for (const auto& directory_entry : std::filesystem::directory_iterator(file_path2.Path()))
|
||||
if (directory_entry.path().extension() == ".json")
|
||||
files.push_back(directory_entry.path().filename().generic_string());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
#include "core/filemgr/FileMgr.hpp"
|
||||
#include <unordered_map>
|
||||
|
||||
namespace YimMenu
|
||||
{
|
||||
namespace Outfit
|
||||
{
|
||||
struct ComponentData
|
||||
{
|
||||
std::string label;
|
||||
int drawable_id = 0;
|
||||
int texture_id = 0;
|
||||
int palette_var = -1;
|
||||
int drawable_id_max = 0;
|
||||
int texture_id_max = 0;
|
||||
};
|
||||
|
||||
struct OutfitComponents
|
||||
{
|
||||
std::unordered_map<int, ComponentData> items = {
|
||||
{1, {"Mask"}},
|
||||
{2, {"Hair"}},
|
||||
{3, {"Torso"}},
|
||||
{4, {"Leg"}},
|
||||
{5, {"Bag"}},
|
||||
{6, {"Shoe"}},
|
||||
{7, {"Accessory"}},
|
||||
{8, {"Undershirt"}},
|
||||
{9, {"Kevlar/Armor"}},
|
||||
{10, {"Decals"}},
|
||||
{11, {"Tops"}}};
|
||||
};
|
||||
|
||||
struct OutfitProps
|
||||
{
|
||||
std::unordered_map<int, ComponentData> items = {
|
||||
{0, {"Hats"}},
|
||||
{1, {"Glasses"}},
|
||||
{2, {"Ears"}},
|
||||
{6, {"Watches"}},
|
||||
{7, {"Bracelets"}}};
|
||||
};
|
||||
|
||||
class OutfitEditor {
|
||||
|
||||
public:
|
||||
|
||||
static void SetSelfOutfit(OutfitComponents components, OutfitProps props, bool applyHair);
|
||||
|
||||
// json
|
||||
static Folder CheckFolder(std::string folderName = "");
|
||||
static void ApplyOutfitFromJson(std::string folderName, std::string fileName, bool applyHair);
|
||||
static void SaveOutfit(std::string fileName, std::string folder);
|
||||
static void RefreshList(std::string folderName, std::vector<std::string>& folders, std::vector<std::string>& files);
|
||||
|
||||
// check & fix bounds
|
||||
static void CheckBoundsDrawable(ComponentData& item, const int lower);
|
||||
static void CheckBoundsTexture(ComponentData& item, const int lower);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "game/gta/VehicleModel.hpp"
|
||||
#include "game/backend/Self.hpp"
|
||||
#include "game/gta/Natives.hpp"
|
||||
#include "core/util/Strings.hpp"
|
||||
#include "game/gta/Vehicle.hpp"
|
||||
#include "game/gta/data/VehicleValues.hpp"
|
||||
|
||||
@@ -131,6 +132,9 @@ namespace YimMenu
|
||||
{
|
||||
if (auto veh = Self::GetVehicle(); veh && veh.IsValid())
|
||||
{
|
||||
ReplaceString(fileName, ".", ""); // filename say "bob.." will throw relative path error from Folder::GetFile
|
||||
fileName += ".json";
|
||||
|
||||
const auto file = SavedVehicles::CheckFolder(folderName).GetFile(fileName);
|
||||
std::ofstream file_stream(file.Path(), std::ios::out | std::ios::trunc);
|
||||
file_stream << SavedVehicles::GetJson(veh).dump(4);
|
||||
|
||||
@@ -1,235 +1,274 @@
|
||||
#include "OutfitEditor.hpp"
|
||||
|
||||
#include "core/frontend/manager/UIManager.hpp"
|
||||
#include "game/backend/Self.hpp"
|
||||
#include "game/frontend/items/Items.hpp"
|
||||
#include "core/backend/FiberPool.hpp"
|
||||
#include "game/backend/Outfit.hpp"
|
||||
#include "core/frontend/Notifications.hpp"
|
||||
#include "game/backend/Self.hpp"
|
||||
#include "game/gta/Natives.hpp"
|
||||
|
||||
#include <random>
|
||||
|
||||
// TODO: clean up more AI generated junk from this file
|
||||
#include "core/util/Strings.hpp"
|
||||
#include "misc/cpp/imgui_stdlib.h"
|
||||
|
||||
namespace YimMenu
|
||||
{
|
||||
int GetMaxDrawable(int slot)
|
||||
class OutfitEditorMenu
|
||||
{
|
||||
auto ped = Self::GetPed();
|
||||
return PED::GET_NUMBER_OF_PED_DRAWABLE_VARIATIONS(static_cast<int>(ped.GetHandle()), slot);
|
||||
}
|
||||
Outfit::OutfitComponents components{};
|
||||
Outfit::OutfitProps props{};
|
||||
std::vector<std::string> folders{}, files{};
|
||||
std::string folder{}, file{};
|
||||
char outfitName[64]{}, newFolder[50]{};
|
||||
|
||||
int GetMaxTexture(int slot, int drawable)
|
||||
{
|
||||
auto ped = Self::GetPed();
|
||||
return PED::GET_NUMBER_OF_PED_TEXTURE_VARIATIONS(static_cast<int>(ped.GetHandle()), slot, drawable);
|
||||
}
|
||||
|
||||
int GetMaxPropDrawable(int slot)
|
||||
{
|
||||
auto ped = Self::GetPed();
|
||||
return PED::GET_NUMBER_OF_PED_PROP_DRAWABLE_VARIATIONS(static_cast<int>(ped.GetHandle()), slot);
|
||||
}
|
||||
|
||||
int GetMaxPropTexture(int slot, int drawable)
|
||||
{
|
||||
auto ped = Self::GetPed();
|
||||
return PED::GET_NUMBER_OF_PED_PROP_TEXTURE_VARIATIONS(static_cast<int>(ped.GetHandle()), slot, drawable);
|
||||
}
|
||||
|
||||
void GetOutfitSlot(int slot, int& drawable, int& texture)
|
||||
{
|
||||
auto ped = Self::GetPed();
|
||||
drawable = PED::GET_PED_DRAWABLE_VARIATION(static_cast<int>(ped.GetHandle()), slot);
|
||||
texture = PED::GET_PED_TEXTURE_VARIATION(static_cast<int>(ped.GetHandle()), slot);
|
||||
}
|
||||
|
||||
void GetPropSlot(int slot, int& drawable, int& texture)
|
||||
{
|
||||
auto ped = Self::GetPed();
|
||||
drawable = PED::GET_PED_PROP_INDEX(static_cast<int>(ped.GetHandle()), slot, 0);
|
||||
if (drawable == -1)
|
||||
public:
|
||||
// refreshes the outfit editor data to current ped outfit
|
||||
void RefreshStats()
|
||||
{
|
||||
drawable = 0;
|
||||
texture = 0;
|
||||
return;
|
||||
auto ped = Self::GetPed().GetHandle();
|
||||
for (auto& t : components.items)
|
||||
{
|
||||
auto& item = t.second;
|
||||
item.drawable_id = PED::GET_PED_DRAWABLE_VARIATION(ped, t.first);
|
||||
item.drawable_id_max = PED::GET_NUMBER_OF_PED_DRAWABLE_VARIATIONS(ped, t.first) - 1;
|
||||
item.texture_id = PED::GET_PED_TEXTURE_VARIATION(ped, t.first);
|
||||
item.texture_id_max = PED::GET_NUMBER_OF_PED_TEXTURE_VARIATIONS(ped, t.first, item.drawable_id) - 1;
|
||||
}
|
||||
|
||||
for (auto& t : props.items)
|
||||
{
|
||||
auto& item = t.second;
|
||||
item.drawable_id = PED::GET_PED_PROP_INDEX(ped, t.first, 0);
|
||||
item.drawable_id_max = PED::GET_NUMBER_OF_PED_PROP_DRAWABLE_VARIATIONS(ped, t.first) - 1;
|
||||
item.texture_id = PED::GET_PED_PROP_TEXTURE_INDEX(ped, t.first);
|
||||
item.texture_id_max = PED::GET_NUMBER_OF_PED_PROP_TEXTURE_VARIATIONS(ped, t.first, item.drawable_id) - 1;
|
||||
}
|
||||
}
|
||||
texture = PED::GET_PED_PROP_TEXTURE_INDEX(static_cast<int>(ped.GetHandle()), slot);
|
||||
}
|
||||
|
||||
void SetOutfitSlot(int slot, int drawable, int texture)
|
||||
{
|
||||
auto ped = Self::GetPed();
|
||||
PED::SET_PED_COMPONENT_VARIATION(static_cast<int>(ped.GetHandle()), slot, drawable, texture, 0);
|
||||
}
|
||||
void RenderComponents()
|
||||
{
|
||||
ImGui::BeginGroup();
|
||||
for (auto& t : components.items)
|
||||
{
|
||||
auto& item = t.second;
|
||||
ImGui::SetNextItemWidth(120);
|
||||
if (ImGui::InputInt(std::format("{} [0,{}]##1", item.label, item.drawable_id_max).c_str(), &item.drawable_id))
|
||||
{
|
||||
Outfit::OutfitEditor::CheckBoundsDrawable(item, 0);
|
||||
FiberPool::Push([id = t.first, item, this] {
|
||||
PED::SET_PED_COMPONENT_VARIATION(Self::GetPed().GetHandle(), id, item.drawable_id, 0, PED::GET_PED_PALETTE_VARIATION(Self::GetPed().GetHandle(), id));
|
||||
RefreshStats();
|
||||
});
|
||||
}
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
|
||||
void SetPropSlot(int slot, int drawable, int texture)
|
||||
{
|
||||
auto ped = Self::GetPed();
|
||||
PED::SET_PED_PROP_INDEX(static_cast<int>(ped.GetHandle()), slot, drawable, texture, true, 0);
|
||||
}
|
||||
void RenderComponentsTextures()
|
||||
{
|
||||
ImGui::BeginGroup();
|
||||
for (auto& t : components.items)
|
||||
{
|
||||
auto& item = t.second;
|
||||
ImGui::SetNextItemWidth(120);
|
||||
if (ImGui::InputInt(std::format("{} TEX [0,{}]##2", item.label, item.texture_id_max).c_str(), &item.texture_id))
|
||||
{
|
||||
Outfit::OutfitEditor::CheckBoundsTexture(item, 0);
|
||||
FiberPool::Push([id = t.first, item, this] {
|
||||
PED::SET_PED_COMPONENT_VARIATION(Self::GetPed().GetHandle(), id, item.drawable_id, item.texture_id, PED::GET_PED_PALETTE_VARIATION(Self::GetPed().GetHandle(), id));
|
||||
RefreshStats();
|
||||
});
|
||||
}
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
|
||||
// Helper function for underlined text
|
||||
static void TextUnderlined(const char* text)
|
||||
{
|
||||
ImGui::Text("%s", text);
|
||||
ImVec2 min = ImGui::GetItemRectMin();
|
||||
ImVec2 max = ImGui::GetItemRectMax();
|
||||
min.y = max.y;
|
||||
ImGui::GetWindowDrawList()->AddLine(min, max, ImGui::GetColorU32(ImGui::GetStyle().Colors[ImGuiCol_Text]));
|
||||
}
|
||||
void RenderProps()
|
||||
{
|
||||
for (auto& t : props.items)
|
||||
{
|
||||
auto& item = t.second;
|
||||
ImGui::SetNextItemWidth(120);
|
||||
if (ImGui::InputInt(std::format("{} [0,{}]##3", item.label, item.drawable_id_max).c_str(), &item.drawable_id))
|
||||
{
|
||||
Outfit::OutfitEditor::CheckBoundsDrawable(item, -1);
|
||||
FiberPool::Push([id = t.first, item, this] {
|
||||
if (item.drawable_id == -1)
|
||||
PED::CLEAR_PED_PROP(Self::GetPed().GetHandle(), id, 1);
|
||||
else
|
||||
PED::SET_PED_PROP_INDEX(Self::GetPed().GetHandle(), id, item.drawable_id, 0, TRUE, 0);
|
||||
RefreshStats();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void TextUnderlinedAt(const char* text, float y)
|
||||
{
|
||||
auto old_cursor = ImGui::GetCursorPos();
|
||||
ImGui::SetCursorPosY(y);
|
||||
TextUnderlined(text);
|
||||
ImGui::SetCursorPos(old_cursor);
|
||||
}
|
||||
void RenderPropsTextures()
|
||||
{
|
||||
for (auto& t : props.items)
|
||||
{
|
||||
auto& item = t.second;
|
||||
ImGui::SetNextItemWidth(120);
|
||||
if (ImGui::InputInt(std::format("{} TEX [0,{}]##4", item.label, item.texture_id_max).c_str(), &item.texture_id))
|
||||
{
|
||||
Outfit::OutfitEditor::CheckBoundsTexture(item, -1);
|
||||
FiberPool::Push([id = t.first, item, this] {
|
||||
PED::SET_PED_PROP_INDEX(Self::GetPed().GetHandle(), id, item.drawable_id, item.texture_id, TRUE, 0);
|
||||
RefreshStats();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderOutfitList()
|
||||
{
|
||||
ImGui::BeginGroup();
|
||||
{
|
||||
// folders
|
||||
ImGui::SetNextItemWidth(300.f);
|
||||
if (ImGui::BeginCombo("", folder.empty() ? "Root" : folder.c_str()))
|
||||
{
|
||||
if (ImGui::Selectable("Root", folder == ""))
|
||||
{
|
||||
folder.clear();
|
||||
FiberPool::Push([this] {
|
||||
Outfit::OutfitEditor::RefreshList(folder, folders, files);
|
||||
});
|
||||
}
|
||||
|
||||
for (std::string folderName : folders)
|
||||
if (ImGui::Selectable(folderName.c_str(), folder == folderName))
|
||||
{
|
||||
folder = folderName;
|
||||
FiberPool::Push([this] {
|
||||
Outfit::OutfitEditor::RefreshList(folder, folders, files);
|
||||
});
|
||||
}
|
||||
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
// files
|
||||
static std::string search;
|
||||
ImGui::SetNextItemWidth(300);
|
||||
if (ImGui::InputTextWithHint("###outfitname", "Search", &search))
|
||||
std::transform(search.begin(), search.end(), search.begin(), tolower);
|
||||
if (ImGui::BeginListBox("##saved_outfits", ImVec2(300, 300)))
|
||||
{
|
||||
for (const auto& pair : files)
|
||||
{
|
||||
std::string pair_lower = pair;
|
||||
std::transform(pair_lower.begin(), pair_lower.end(), pair_lower.begin(), tolower);
|
||||
if (pair_lower.contains(search))
|
||||
{
|
||||
auto fileName = pair.c_str();
|
||||
if (ImGui::Selectable(fileName, file == pair, ImGuiSelectableFlags_AllowItemOverlap))
|
||||
file = pair;
|
||||
}
|
||||
}
|
||||
ImGui::EndListBox();
|
||||
}
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
|
||||
void RenderSaveButton(bool saveToNewFolder)
|
||||
{
|
||||
if (ImGui::Button("Save Outfit"))
|
||||
FiberPool::Push([saveToNewFolder, this] {
|
||||
std::string fileName = TrimString(outfitName);
|
||||
strcpy(outfitName, "");
|
||||
|
||||
if (!fileName.size())
|
||||
{
|
||||
Notifications::Show("Outfit", "Filename empty!", NotificationType::Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
Outfit::OutfitEditor::SaveOutfit(fileName, folder);
|
||||
|
||||
if (saveToNewFolder)
|
||||
{
|
||||
folder = newFolder; // set current folder to newly created folder
|
||||
strcpy(newFolder, "");
|
||||
}
|
||||
|
||||
Outfit::OutfitEditor::RefreshList(folder, folders, files);
|
||||
});
|
||||
};
|
||||
|
||||
void RenderOutfitListControls()
|
||||
{
|
||||
ImGui::BeginGroup();
|
||||
{
|
||||
if (ImGui::Button("Refresh list"))
|
||||
FiberPool::Push([this] {
|
||||
Outfit::OutfitEditor::RefreshList(folder, folders, files);
|
||||
});
|
||||
ImGui::Spacing();
|
||||
static bool applyHair = false;
|
||||
ImGui::Checkbox("Apply hair", &applyHair);
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Apply Selected Outfit"))
|
||||
FiberPool::Push([this] {
|
||||
Outfit::OutfitEditor::ApplyOutfitFromJson(folder, file, applyHair);
|
||||
applyHair = false; // reset everytime
|
||||
RefreshStats();
|
||||
});
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// save outfit
|
||||
ImGui::Text("Outfit Name");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(250);
|
||||
ImGui::InputText("##filename", outfitName, IM_ARRAYSIZE(outfitName));
|
||||
|
||||
if (folder.empty())
|
||||
{
|
||||
ImGui::Text("Folder Name");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(250);
|
||||
ImGui::InputText("##foldername", newFolder, IM_ARRAYSIZE(newFolder));
|
||||
RenderSaveButton(true);
|
||||
}
|
||||
else
|
||||
RenderSaveButton(false);
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
};
|
||||
|
||||
std::shared_ptr<Category> CreateOutfitsMenu()
|
||||
{
|
||||
static OutfitEditorMenu editor{};
|
||||
auto category = std::make_shared<Category>("Outfit Editor");
|
||||
|
||||
category->AddItem(std::make_shared<ImGuiItem>([] {
|
||||
if (!NativeInvoker::AreHandlersCached())
|
||||
return ImGui::TextDisabled("Natives not cached yet");
|
||||
|
||||
auto ped = Self::GetPed();
|
||||
|
||||
if (!ped)
|
||||
return ImGui::TextDisabled("Player ped not found");
|
||||
|
||||
// Create two columns layout
|
||||
const float windowWidth = ImGui::GetContentRegionAvail().x;
|
||||
const float columnWidth = windowWidth * 0.5f;
|
||||
const float inputWidth = 120.0f; // Minimal width for number input
|
||||
|
||||
ImGui::Columns(2, "OutfitColumns", false);
|
||||
ImGui::SetColumnWidth(0, columnWidth);
|
||||
|
||||
// Components section (Left column)
|
||||
|
||||
float header_y = ImGui::GetCursorPosY();
|
||||
|
||||
TextUnderlined("Components");
|
||||
const struct
|
||||
{
|
||||
const char* name;
|
||||
int slot;
|
||||
} componentSlots[] = {{"Top", 11}, {"Undershirt", 8}, {"Legs", 4}, {"Feet", 6}, {"Accessories", 7}, {"Bags", 5}, {"Mask", 1}, {"Gloves", 3}, {"Decals", 10}, {"Armor", 9}};
|
||||
|
||||
bool first_iter = true;
|
||||
for (const auto& component : componentSlots)
|
||||
{
|
||||
ImGui::PushID(component.slot);
|
||||
|
||||
int drawable, texture;
|
||||
GetOutfitSlot(component.slot, drawable, texture);
|
||||
|
||||
ImGui::Text("%s", component.name);
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::SetCursorPosX(columnWidth - inputWidth * 2 - 10);
|
||||
|
||||
ImGui::PushItemWidth(inputWidth);
|
||||
if (first_iter)
|
||||
TextUnderlinedAt("Drawable", header_y);
|
||||
if (ImGui::InputInt("##{}drawable", &drawable))
|
||||
{
|
||||
drawable = std::clamp(drawable, 0, GetMaxDrawable(component.slot) - 1);
|
||||
SetOutfitSlot(component.slot, drawable, texture);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (first_iter)
|
||||
TextUnderlinedAt("Texture", header_y); // TODO: this heading is slightly misaligned and I'm not sure why (caused by the above SameLine?)
|
||||
if (ImGui::InputInt("##{}texture", &texture))
|
||||
{
|
||||
texture = std::clamp(texture, 0, GetMaxTexture(component.slot, drawable) - 1);
|
||||
SetOutfitSlot(component.slot, drawable, texture);
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::PopID();
|
||||
|
||||
first_iter = false;
|
||||
}
|
||||
|
||||
// Props section (Right column)
|
||||
ImGui::NextColumn();
|
||||
TextUnderlined("Props");
|
||||
|
||||
const struct
|
||||
{
|
||||
const char* name;
|
||||
int slot;
|
||||
} propSlots[] = {{"Hats", 0}, {"Glasses", 1}, {"Ears", 2}, {"Watches", 6}};
|
||||
|
||||
first_iter = true;
|
||||
for (const auto& prop : propSlots)
|
||||
{
|
||||
ImGui::PushID(prop.slot);
|
||||
|
||||
int drawable, texture;
|
||||
GetPropSlot(prop.slot, drawable, texture);
|
||||
|
||||
ImGui::Text("%s", prop.name);
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::SetCursorPosX(columnWidth + (columnWidth - inputWidth * 2 - 10));
|
||||
|
||||
ImGui::PushItemWidth(inputWidth);
|
||||
if (first_iter)
|
||||
TextUnderlinedAt("Drawable", header_y);
|
||||
if (ImGui::InputInt("##pdrawable", &drawable))
|
||||
{
|
||||
drawable = std::clamp(drawable, 0, GetMaxPropDrawable(prop.slot) - 1);
|
||||
SetPropSlot(prop.slot, drawable, texture);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (first_iter)
|
||||
TextUnderlinedAt("Texture", header_y);
|
||||
if (ImGui::InputInt("##ptexture", &texture))
|
||||
{
|
||||
texture = std::clamp(texture, 0, GetMaxPropTexture(prop.slot, drawable) - 1);
|
||||
SetPropSlot(prop.slot, drawable, texture);
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::PopID();
|
||||
|
||||
first_iter = false;
|
||||
}
|
||||
|
||||
ImGui::Columns(1);
|
||||
|
||||
if (ImGui::Button("Refresh Stats"))
|
||||
FiberPool::Push([] {
|
||||
editor.RefreshStats();
|
||||
});
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Randomize Outfit"))
|
||||
FiberPool::Push([] {
|
||||
Self::GetPed().RandomizeOutfit2();
|
||||
});
|
||||
|
||||
editor.RenderComponents();
|
||||
ImGui::SameLine();
|
||||
editor.RenderComponentsTextures();
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
{
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
|
||||
// Randomize components
|
||||
for (int i = 0; i < 12; ++i)
|
||||
{
|
||||
int maxDrawable = GetMaxDrawable(i);
|
||||
if (maxDrawable > 0)
|
||||
{
|
||||
int drawable = std::uniform_int_distribution<>(0, maxDrawable - 1)(gen);
|
||||
int maxTexture = GetMaxTexture(i, drawable);
|
||||
int texture = maxTexture > 0 ? std::uniform_int_distribution<>(0, maxTexture - 1)(gen) : 0;
|
||||
SetOutfitSlot(i, drawable, texture);
|
||||
}
|
||||
}
|
||||
|
||||
// Randomize props
|
||||
for (int i : {0, 1, 2, 6, 7})
|
||||
{
|
||||
int maxDrawable = GetMaxPropDrawable(i);
|
||||
if (maxDrawable > 0)
|
||||
{
|
||||
int drawable = std::uniform_int_distribution<>(0, maxDrawable - 1)(gen);
|
||||
int maxTexture = GetMaxPropTexture(i, drawable);
|
||||
int texture = maxTexture > 0 ? std::uniform_int_distribution<>(0, maxTexture - 1)(gen) : 0;
|
||||
SetPropSlot(i, drawable, texture);
|
||||
}
|
||||
}
|
||||
editor.RenderProps();
|
||||
ImGui::Spacing();
|
||||
editor.RenderPropsTextures();
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
editor.RenderOutfitList();
|
||||
ImGui::SameLine();
|
||||
editor.RenderOutfitListControls();
|
||||
}));
|
||||
|
||||
return category;
|
||||
|
||||
@@ -31,18 +31,15 @@ namespace YimMenu::Submenus
|
||||
|
||||
if (ImGui::Button("Save"))
|
||||
FiberPool::Push([saveToNewFolder] {
|
||||
std::string fileName = vehicle_file_name_input;
|
||||
std::string fileName = TrimString(vehicle_file_name_input);
|
||||
strcpy(vehicle_file_name_input, "");
|
||||
|
||||
if (!TrimString(fileName).size())
|
||||
if (!fileName.size())
|
||||
{
|
||||
Notifications::Show("Saved Vehicles", "Filename empty!", NotificationType::Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
ReplaceString(fileName, ".", ""); // filename say "bob.." will throw relative path error from Folder::GetFile
|
||||
fileName += ".json";
|
||||
|
||||
SavedVehicles::Save(saveToNewFolder ? newFolder : folder, fileName);
|
||||
|
||||
if (saveToNewFolder)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "Ped.hpp"
|
||||
|
||||
#include "Natives.hpp"
|
||||
#include "game/backend/Outfit.hpp"
|
||||
#include "core/backend/ScriptMgr.hpp"
|
||||
#include "game/pointers/Pointers.hpp"
|
||||
#include <random>
|
||||
|
||||
namespace YimMenu
|
||||
{
|
||||
@@ -266,6 +268,43 @@ namespace YimMenu
|
||||
PED::SET_PED_RANDOM_COMPONENT_VARIATION(GetHandle(), 0);
|
||||
}
|
||||
|
||||
void Ped::RandomizeOutfit2()
|
||||
{
|
||||
ENTITY_ASSERT_VALID();
|
||||
ENTITY_ASSERT_CONTROL();
|
||||
|
||||
Outfit::OutfitComponents components{};
|
||||
Outfit::OutfitProps props{};
|
||||
auto ped = GetHandle();
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
|
||||
// Randomize components
|
||||
for (auto& item : components.items)
|
||||
{
|
||||
int drawable_id_max = PED::GET_NUMBER_OF_PED_DRAWABLE_VARIATIONS(ped, item.first) - 1;
|
||||
if (drawable_id_max <= 0) // 0 if one variation, -1 if no variation
|
||||
continue;
|
||||
|
||||
int drawable_id = std::uniform_int_distribution<>(0, drawable_id_max)(gen); // i in [a, b]
|
||||
int texture_id_max = PED::GET_NUMBER_OF_PED_TEXTURE_VARIATIONS(ped, item.first, drawable_id) - 1;
|
||||
int texture_id = texture_id_max > 0 ? std::uniform_int_distribution<>(0, texture_id_max)(gen) : 0;
|
||||
PED::SET_PED_COMPONENT_VARIATION(ped, item.first, drawable_id, texture_id, PED::GET_PED_PALETTE_VARIATION(ped, item.first));
|
||||
}
|
||||
// Randomize props
|
||||
for (auto& item : props.items)
|
||||
{
|
||||
int drawable_id_max = PED::GET_NUMBER_OF_PED_PROP_DRAWABLE_VARIATIONS(ped, item.first) - 1;
|
||||
if (drawable_id_max <= 0) // 0 if one variation, -1 if no variation
|
||||
continue;
|
||||
|
||||
int drawable_id = std::uniform_int_distribution<>(0, drawable_id_max)(gen); // i in [a, b]
|
||||
int texture_id_max = PED::GET_NUMBER_OF_PED_PROP_TEXTURE_VARIATIONS(ped, item.first, drawable_id) - 1;
|
||||
int texture_id = texture_id_max > 0 ? std::uniform_int_distribution<>(0, texture_id_max)(gen) : 0;
|
||||
PED::SET_PED_PROP_INDEX(ped, item.first, drawable_id, texture_id, TRUE, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Ped::StartScenario(std::string_view name, int duration, bool entry_anim)
|
||||
{
|
||||
ENTITY_ASSERT_VALID();
|
||||
|
||||
@@ -69,6 +69,7 @@ namespace YimMenu
|
||||
|
||||
// outfits
|
||||
void RandomizeOutfit();
|
||||
void RandomizeOutfit2();
|
||||
|
||||
// tasks
|
||||
void StartScenario(std::string_view name, int duration = -1, bool entry_anim = true);
|
||||
|
||||
Reference in New Issue
Block a user