This commit is contained in:
Даниил Заикин
2026-06-17 20:44:53 +03:00
parent 9d153773c2
commit b133e7c656
1880 changed files with 244545 additions and 0 deletions
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7ae8b0adf00c450d9e80e11ffa1d2cf7
timeCreated: 1678721517
@@ -0,0 +1,61 @@
using System;
using System.Globalization;
using SingularityGroup.HotReload.DTO;
using UnityEditor;
using UnityEditor.VSAttribution.HotReload;
using UnityEngine;
using UnityEngine.Analytics;
namespace SingularityGroup.HotReload.Editor {
internal static class Attribution {
internal const string LastLoginKey = "HotReload.Attribution.LastAttributionEventAt";
//Resend attribution event every 12 hours to be safe
static readonly TimeSpan resendPeriod = TimeSpan.FromHours(12);
//The last time the attribution event was sent.
//Returns unix epoch in case it has never been sent before.
static DateTime LastAttributionEventAt {
get {
if(EditorPrefs.HasKey(LastLoginKey)) {
return DateTime.ParseExact(EditorPrefs.GetString(LastLoginKey), "o", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
}
return DateTimeOffset.FromUnixTimeSeconds(0).UtcDateTime;
}
set {
EditorPrefs.SetString(LastLoginKey, value.ToUniversalTime().ToString("o"));
}
}
const string actionName = "Login";
const string partnerName = "The Naughty Cult Ltd.";
public static void RegisterLogin(LoginStatusResponse response) {
//Licensing might not be initialized yet.
//The hwId should be set eventually.
if(response?.hardwareId == null) {
return;
}
//Only forward attribution if this is an asset store build.
//We will still distribute this package outside of the asset store (i.e via our website).
if (!PackageConst.IsAssetStoreBuild) {
return;
}
var now = DateTime.UtcNow;
//If we sent an attribution event in the last 12 hours we should already be good.
if (now - LastAttributionEventAt < resendPeriod) {
return;
}
var result = VSAttribution.SendAttributionEvent(actionName, partnerName, response.hardwareId);
//Retry on transient errors
if (result == AnalyticsResult.NotInitialized) {
return;
}
LastAttributionEventAt = now;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 67658aafb8404f0eb9496812ba4bb8a4
timeCreated: 1678721795
@@ -0,0 +1,149 @@
using System;
using UnityEngine.Analytics;
#if UNITY_6000_0_OR_NEWER
namespace UnityEditor.VSAttribution.HotReload
{
internal static class VSAttribution
{
const int k_VersionId = 4;
const int k_MaxEventsPerHour = 10;
const int k_MaxNumberOfElements = 1000;
const string k_VendorKey = "unity.vsp-attribution";
const string k_EventName = "vspAttribution";
[Serializable]
private class VSAttributionData : IAnalytic.IData {
public string actionName;
public string partnerName;
public string customerUid;
public string extra;
}
[AnalyticInfo(k_EventName, k_VendorKey, k_VersionId, k_MaxEventsPerHour, k_MaxNumberOfElements)]
class VSAttributionAnalytics : IAnalytic {
private VSAttributionData m_Data;
private VSAttributionAnalytics(
string actionName,
string partnerName,
string customerUid,
string extra
) {
this.m_Data = new VSAttributionData {
actionName = actionName,
partnerName = partnerName,
customerUid = customerUid,
extra = extra
};
}
public bool TryGatherData(out IAnalytic.IData data, out Exception error) {
data = this.m_Data;
error = null;
return this.m_Data != null;
}
public static AnalyticsResult SendEvent(
string actionName,
string partnerName,
string customerUid,
string extra
) {
return EditorAnalytics.SendAnalytic(new VSAttributionAnalytics(actionName,
partnerName,
customerUid,
extra
));
}
}
/// <summary>
/// Registers and attempts to send a Verified Solutions Attribution event.
/// </summary>
/// <param name="actionName">Name of the action, identifying a place this event was called from.</param>
/// <param name="partnerName">Identifiable Verified Solutions Partner's name.</param>
/// <param name="customerUid">Unique identifier of the customer using Partner's Verified Solution.</param>
public static AnalyticsResult SendAttributionEvent(string actionName, string partnerName, string customerUid)
{
try
{
return VSAttributionAnalytics.SendEvent(actionName, partnerName, customerUid, "{}");
}
catch
{
// Fail silently
return AnalyticsResult.AnalyticsDisabled;
}
}
}
}
#else
namespace UnityEditor.VSAttribution.HotReload
{
internal static class VSAttribution
{
const int k_VersionId = 4;
const int k_MaxEventsPerHour = 10;
const int k_MaxNumberOfElements = 1000;
const string k_VendorKey = "unity.vsp-attribution";
const string k_EventName = "vspAttribution";
static bool RegisterEvent()
{
AnalyticsResult result = EditorAnalytics.RegisterEventWithLimit(k_EventName, k_MaxEventsPerHour,
k_MaxNumberOfElements, k_VendorKey, k_VersionId);
var isResultOk = result == AnalyticsResult.Ok;
return isResultOk;
}
[Serializable]
struct VSAttributionData
{
public string actionName;
public string partnerName;
public string customerUid;
public string extra;
}
/// <summary>
/// Registers and attempts to send a Verified Solutions Attribution event.
/// </summary>
/// <param name="actionName">Name of the action, identifying a place this event was called from.</param>
/// <param name="partnerName">Identifiable Verified Solutions Partner's name.</param>
/// <param name="customerUid">Unique identifier of the customer using Partner's Verified Solution.</param>
public static AnalyticsResult SendAttributionEvent(string actionName, string partnerName, string customerUid)
{
try
{
// Are Editor Analytics enabled ? (Preferences)
if (!EditorAnalytics.enabled)
return AnalyticsResult.AnalyticsDisabled;
if (!RegisterEvent())
return AnalyticsResult.InvalidData;
// Create an expected data object
var eventData = new VSAttributionData
{
actionName = actionName,
partnerName = partnerName,
customerUid = customerUid,
extra = "{}"
};
return EditorAnalytics.SendEventWithLimit(k_EventName, eventData, k_VersionId);
}
catch
{
// Fail silently
return AnalyticsResult.AnalyticsDisabled;
}
}
}
}
#endif
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d7493a30e78d4ec783ead20baea2c4d2
timeCreated: 1678721534
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ea93d0fb53fe44419e8a20d4701918ef
timeCreated: 1773924481
@@ -0,0 +1,34 @@
using System;
using System.Threading.Tasks;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor {
internal static class ReportWindowAPI {
public static void OpenBugReport(string title = null, string description = null) {
HotReloadBugReportWindow.Open(
ReportMode.BugReport,
discordUrl: Constants.DiscordInviteUrl,
contactUrl: Constants.ContactURL,
email: HotReloadPrefs.LicenseEmail,
details: description,
title: title
);
}
public static void OpenFeedback(
Func<Report, Task<string>> submitHandler = null,
string discordUrl = null,
string contactUrl = null,
string title = null,
string email = null,
string feedback = null
) {
HotReloadBugReportWindow.Open(
ReportMode.Feedback,
discordUrl: Constants.DiscordInviteUrl,
contactUrl: Constants.ContactURL,
email: HotReloadPrefs.LicenseEmail
);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8a7505b2e85b481479de6861e72e954a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,616 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using SingularityGroup.HotReload.Editor.Localization;
using Application = UnityEngine.Application;
#if UNITY_2021_3_OR_NEWER
using System.IO;
using System.IO.Compression;
using SingularityGroup.HotReload.Editor.Cli;
using SingularityGroup.HotReload.EditorDependencies;
using CompressionLevel = System.IO.Compression.CompressionLevel;
#endif
namespace SingularityGroup.HotReload.Editor {
internal enum ReportMode {
BugReport,
Feedback
}
internal class HotReloadBugReportWindow : EditorWindow {
static List<string> SeverityChoices => new List<string> {
Translations.BugReport.SeverityNormal,
Translations.BugReport.SeverityLow,
Translations.BugReport.SeverityHigh,
};
static List<string> FrequencyChoices => new List<string> {
Translations.BugReport.FrequencyFirstTime,
Translations.BugReport.FrequencySometimes,
Translations.BugReport.FrequencyAlways,
};
[SerializeField] ReportMode _mode = ReportMode.BugReport;
[SerializeField] VisualTreeAsset visualTree;
[SerializeField] StyleSheet styleSheet;
string _discordUrl;
string _contactUrl;
TextField _titleField;
PopupField<string> _severityPopup;
PopupField<string> _frequencyPopup;
TextField _emailField;
IMGUIContainer _detailsContainer;
[SerializeField] string _detailsText = "";
[SerializeField] string _emailText = "";
[SerializeField] string _titleText = "";
Label _detailsLabel;
Label _validationLabel;
Button _submitButton;
VisualElement _severitySection;
VisualElement _frequencySection;
ScrollView _scrollView;
VisualElement _detailsSlot;
VisualElement _successScreen;
VisualElement _failureScreen;
Label _failureErrorLabel;
Button _tabBugReport;
Button _tabFeedback;
bool _isOnResultScreen;
static Vector2 GetMinSize(ReportMode mode) {
return mode == ReportMode.Feedback
? new Vector2(420, 500)
: new Vector2(420, 620);
}
#region Public API
/// <summary>
/// Opens the window with the given mode and configuration.
/// Prefer using <see cref="ReportWindowAPI"/> for a cleaner call site.
/// </summary>
public static HotReloadBugReportWindow Open(
ReportMode mode,
string discordUrl = null,
string contactUrl = null,
string title = null,
string email = null,
string details = null
) {
var wnd = GetWindow<HotReloadBugReportWindow>(utility: true);
wnd._mode = mode;
wnd._discordUrl = discordUrl;
wnd._contactUrl = contactUrl;
wnd.titleContent = new GUIContent(
mode == ReportMode.Feedback
? Translations.BugReport.WindowTitleFeedback
: Translations.BugReport.WindowTitleBugReport);
wnd.minSize = wnd.maxSize = GetMinSize(mode);
wnd._titleText = title;
wnd._emailText = email;
wnd._detailsText = details;
wnd.RebuildUI();
return wnd;
}
/// <summary>
/// Prefills form fields on an already-open window. Null values are ignored.
/// </summary>
public void Prefill() {
if (_titleText != null && _titleField != null) {
_titleField.SetValueWithoutNotify(_titleText);
}
if (_emailText != null && _emailField != null) {
_emailField.SetValueWithoutNotify(_emailText);
}
}
#endregion
void OnEnable() {
RebuildUI();
}
bool HasUnsavedFormContent() {
if (_isOnResultScreen) {
return false;
}
if (!string.IsNullOrWhiteSpace(_titleField.value)) {
return true;
}
if (!string.IsNullOrWhiteSpace(_detailsText) && _detailsText != Translations.BugReport.PlaceholderDetails) {
return true;
}
return false;
}
void OnDestroy() {
if (_detailsContainer != null && _detailsSlot != null) {
_detailsSlot.Remove(_detailsContainer);
_detailsContainer = null;
}
if (!HasUnsavedFormContent()) {
return;
}
bool discard = EditorUtility.DisplayDialog(
Translations.BugReport.DiscardDialogTitle,
Translations.BugReport.DiscardDialogMessage,
Translations.BugReport.DiscardDialogConfirm,
Translations.BugReport.DiscardDialogCancel);
if (!discard) {
_emailText = _emailField.value;
_titleText = _titleField.value;
EditorApplication.delayCall += () => {
Open(_mode, _discordUrl, _contactUrl);
};
} else {
_detailsText = null;
_emailText = null;
_titleText = null;
}
}
void RebuildUI() {
DetachCallbacks();
if (visualTree == null) {
Log.Warning($"Could not open bug report. Please reach out to our support: {_contactUrl}");
return;
}
rootVisualElement.Clear();
visualTree.CloneTree(rootVisualElement);
if (styleSheet != null) {
rootVisualElement.styleSheets.Add(styleSheet);
}
QueryElements();
AttachIMGUIContainers();
ApplyTranslations();
CreatePopupFields();
ApplyMode();
SetupSubmit();
SetupResultScreenButtons();
FixSingleLineFieldAlignment(_titleField);
FixSingleLineFieldAlignment(_emailField);
Prefill();
AttachCallbacks();
ShowFormView();
}
void AttachIMGUIContainers() {
_detailsSlot = rootVisualElement.Q<VisualElement>("details-field-slot");
// Build the IMGUI textarea and inject it
_detailsContainer = new IMGUIContainer(DrawDetailsField);
_detailsSlot.Add(_detailsContainer);
}
void QueryElements() {
_titleField = rootVisualElement.Q<TextField>("title-field");
_emailField = rootVisualElement.Q<TextField>("email-field");
_detailsLabel = rootVisualElement.Q<Label>("details-label");
_validationLabel = rootVisualElement.Q<Label>("validation-label");
_submitButton = rootVisualElement.Q<Button>("submit-button");
_severitySection = rootVisualElement.Q<VisualElement>("severity-section");
_frequencySection = rootVisualElement.Q<VisualElement>("frequency-section");
_scrollView = rootVisualElement.Q<ScrollView>("scroll-view");
_successScreen = rootVisualElement.Q<VisualElement>("success-screen");
_failureScreen = rootVisualElement.Q<VisualElement>("failure-screen");
_failureErrorLabel = rootVisualElement.Q<Label>("failure-error-label");
_tabBugReport = rootVisualElement.Q<Button>("tab-bug-report");
_tabFeedback = rootVisualElement.Q<Button>("tab-feedback");
}
private void DrawDetailsField() {
// GUILayout.ExpandHeight lets it grow; clamp min so it doesn't collapse
_detailsText = EditorGUILayout.TextArea(
_detailsText,
GUILayout.ExpandWidth(true),
GUILayout.MinHeight(160)
);
}
void ApplyTranslations() {
// Form labels
rootVisualElement.Q<Label>(className: "section-label") // Title section — first one
?.SetText(Translations.BugReport.LabelTitle);
rootVisualElement.Q("severity-section")?.Q<Label>(className: "section-label")
?.SetText(Translations.BugReport.LabelIssueSeverity);
rootVisualElement.Q("frequency-section")?.Q<Label>(className: "section-label")
?.SetText(Translations.BugReport.LabelHowOften);
rootVisualElement.Q("tabs")?.Q<Button>("tab-bug-report")
?.SetText(Translations.BugReport.TabBugReport);
rootVisualElement.Q("tabs")?.Q<Button>("tab-feedback")
?.SetText(Translations.BugReport.TabFeedback);
// Contact section — scoped by class since it has no name
var contactSection = rootVisualElement.Q(className: "contact-section");
contactSection?.Q<Label>(className: "section-label")?.SetText(Translations.BugReport.LabelContactEmail);
contactSection?.Q<Label>(className: "fine-print")?.SetText(Translations.BugReport.LabelContactEmailFinePrint);
// Submit button
_submitButton.text = Translations.BugReport.ButtonSubmit;
// Success screen
var successContent = _successScreen?.Q(className: "result-content");
successContent?.Q<Label>(className: "result-title")?.SetText(Translations.BugReport.SuccessTitle);
successContent?.Q<Label>(className: "result-message")?.SetText(Translations.BugReport.SuccessMessage);
_successScreen?.Q<Button>("success-discord-button")?.SetText(Translations.BugReport.ButtonJoinDiscord);
// Failure screen
var failureContent = _failureScreen?.Q(className: "result-content");
failureContent?.Q<Label>(className: "result-title")?.SetText(Translations.BugReport.FailureTitle);
// result-message--error is the dynamic error label, skip it; translate the static one
var failureMessages = failureContent?.Query<Label>(className: "result-message").ToList();
if (failureMessages != null) {
foreach (var lbl in failureMessages) {
if (!lbl.ClassListContains("result-message--error")) {
lbl.text = Translations.BugReport.FailureMessage;
}
}
}
_failureScreen?.Q<Button>("failure-discord-button")?.SetText(Translations.BugReport.ButtonJoinDiscord);
_failureScreen?.Q<Button>("failure-contact-button")?.SetText(Translations.BugReport.ButtonContactUs);
}
#region View switching
void ShowFormView() {
_isOnResultScreen = false;
_scrollView.style.display = DisplayStyle.Flex;
_successScreen.RemoveFromClassList("result-screen--visible");
_failureScreen.RemoveFromClassList("result-screen--visible");
}
void ShowSuccessScreen() {
_isOnResultScreen = true;
_scrollView.style.display = DisplayStyle.None;
_tabFeedback.style.display = DisplayStyle.None;
_tabBugReport.style.display = DisplayStyle.None;
_failureScreen.RemoveFromClassList("result-screen--visible");
_successScreen.AddToClassList("result-screen--visible");
}
void ShowFailureScreen(string errorMessage) {
_scrollView.style.display = DisplayStyle.None;
_tabFeedback.style.display = DisplayStyle.None;
_tabBugReport.style.display = DisplayStyle.None;
_successScreen.RemoveFromClassList("result-screen--visible");
if (_failureErrorLabel != null) {
if (string.IsNullOrEmpty(errorMessage)) {
_failureErrorLabel.style.display = DisplayStyle.None;
} else {
_failureErrorLabel.text = errorMessage;
_failureErrorLabel.style.display = DisplayStyle.Flex;
}
}
_failureScreen.AddToClassList("result-screen--visible");
_isOnResultScreen = true;
}
#endregion
void ApplyMode() {
bool isBugReport = _mode == ReportMode.BugReport;
this.minSize = this.maxSize = GetMinSize(_mode);
if (isBugReport) {
_severitySection.style.display = DisplayStyle.Flex;
_frequencySection.style.display = DisplayStyle.Flex;
_severityPopup.style.display = DisplayStyle.Flex;
_frequencyPopup.style.display = DisplayStyle.Flex;
_detailsLabel.text = Translations.BugReport.LabelDetails;
_tabBugReport.AddToClassList("tab--active");
_tabFeedback.RemoveFromClassList("tab--active");
if (string.IsNullOrEmpty(_detailsText)) {
_detailsText = Translations.BugReport.PlaceholderDetails;
_detailsContainer.MarkDirtyRepaint();
}
} else {
_severitySection.style.display = DisplayStyle.None;
_frequencySection.style.display = DisplayStyle.None;
_severityPopup.style.display = DisplayStyle.None;
_frequencyPopup.style.display = DisplayStyle.None;
_detailsLabel.text = Translations.BugReport.LabelFeedback;
_tabFeedback.AddToClassList("tab--active");
_tabBugReport.RemoveFromClassList("tab--active");
if (_detailsText == Translations.BugReport.PlaceholderDetails) {
_detailsText = string.Empty;
_detailsContainer.MarkDirtyRepaint();
}
}
}
void CreatePopupFields() {
var severityContainer = rootVisualElement.Q<VisualElement>("severity-container");
_severityPopup = new PopupField<string>(
string.Empty,
SeverityChoices,
0);
_severityPopup.AddToClassList("input-field");
severityContainer.Add(_severityPopup);
var frequencyContainer = rootVisualElement.Q<VisualElement>("frequency-container");
_frequencyPopup = new PopupField<string>(
string.Empty,
FrequencyChoices,
0);
_frequencyPopup.AddToClassList("input-field");
frequencyContainer.Add(_frequencyPopup);
}
void SelectBugReport() {
_mode = ReportMode.BugReport;
ApplyMode();
}
void SelectFeedback() {
_mode = ReportMode.Feedback;
ApplyMode();
}
void AttachCallbacks() {
if (_tabBugReport != null) {
_tabBugReport.clicked += SelectBugReport;
}
if (_tabFeedback != null) {
_tabFeedback.clicked += SelectFeedback;
}
if (_emailField != null) {
_emailField.RegisterValueChangedCallback(SetEmailText);
}
if (_titleField != null) {
_titleField.RegisterValueChangedCallback(SetTitleText);
}
}
void DetachCallbacks() {
if (_tabBugReport != null) {
_tabBugReport.clicked -= SelectBugReport;
}
if (_tabFeedback != null) {
_tabFeedback.clicked -= SelectFeedback;
}
if (_emailField != null) {
_emailField.UnregisterValueChangedCallback(SetEmailText);
}
if (_titleField != null) {
_titleField.UnregisterValueChangedCallback(SetTitleText);
}
}
void SetEmailText(ChangeEvent<string> @event) {
_emailText = @event.newValue;
}
void SetTitleText(ChangeEvent<string> @event) {
_titleText = @event.newValue;
}
static void FixSingleLineFieldAlignment(TextField field) {
field.RegisterCallback<GeometryChangedEvent>(evt => {
var input = field.Q(className: "unity-text-field__input");
if (input != null) {
input.style.alignItems = Align.Center;
}
});
}
#region Validation & submit
void SetupSubmit() {
_submitButton.clickable.clicked += OnSubmitClicked;
}
async void OnSubmitClicked() {
HideValidation();
if (string.IsNullOrWhiteSpace(_titleField.value)) {
ShowValidation(Translations.BugReport.ValidationTitleRequired);
return;
}
var details = _detailsText;
var report = new Report {
Mode = _mode,
Id = Guid.NewGuid().ToString("N").Substring(0, 10),
Title = _titleField.value.Trim(),
Severity = _severityPopup != null ? _severityPopup.value : null,
Frequency = _frequencyPopup != null ? _frequencyPopup.value : null,
ContactEmail = _emailField.value != null ? _emailField.value.Trim() : null,
Details = details != null ? details.Trim() : null
};
_submitButton.SetEnabled(false);
_submitButton.text = Translations.BugReport.ButtonSubmitting;
string error;
try {
error = await HandleBugReport(report);
} catch (Exception ex) {
error = string.Format(Translations.BugReport.SubmitHandlerError, ex.Message);
}
if (this == null) {
return;
}
_submitButton.SetEnabled(true);
_submitButton.text = Translations.BugReport.ButtonSubmit;
if (error == null) {
ShowSuccessScreen();
if (report.Mode == ReportMode.BugReport) {
// Back up log and patches locally for this bug report
// They can be later used to simplify reproducing and fixing the issue, but due to our privacy policy they can't be auto uploaded so we need to ask for them
#if UNITY_2021_3_OR_NEWER
var logName = LogsHelper.FindRecentLog(HotReloadAboutTab.logsPath);
try {
CreateBugReportZip(
Path.Combine(CliUtils.GetCliTempDir(), "Backup"),
logName == null ? null : Path.Combine(HotReloadAboutTab.logsPath, logName),
Path.Combine(PackageConst.LibraryCachePath, "BugReports", $"{report.Id}.zip")
);
} catch {
// Fail silently. If zip is not available we will have to ask for a reproduce
}
#endif
}
} else {
ShowFailureScreen(error);
}
}
private static Task<string> HandleBugReport(Report report) {
var hwId = HotReloadPrefs.HardwareId;
if (string.IsNullOrEmpty(hwId)) {
hwId = "unknown";
}
return RequestHelper.SubmitBugReport(new BugReport {
reportId = report.Id,
label = report.Mode == ReportMode.Feedback ? "Feedback" : "Bug Report",
title = report.Title,
description = report.Details,
email = report.ContactEmail,
hwId = hwId,
hotReloadVersion = PackageConst.Version,
unityVersion = Application.unityVersion,
operatingSystemVersionInfo = SystemInfo.operatingSystem,
});
}
#if UNITY_2021_3_OR_NEWER
private static void CreateBugReportZip(
string sourceDir,
string sourceFile,
string outputZipPath
) {
var files = Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories);
if (files.Length == 0 && sourceFile == null) {
return;
}
Directory.CreateDirectory(new FileInfo(outputZipPath).DirectoryName!);
using (var stream = new FileStream(outputZipPath, FileMode.Create, FileAccess.Write, FileShare.None, 65536)) {
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: false)) {
if (sourceFile != null) {
AddFileToArchive(archive, sourceFile, Path.GetFileName(sourceFile));
}
foreach (string filePath in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories)) {
string relativePath = Path.GetRelativePath(sourceDir, filePath);
string entryName = $"Patches/{relativePath}".Replace('\\', '/');
AddFileToArchive(archive, filePath, entryName);
}
}
}
}
private static void AddFileToArchive(ZipArchive archive, string filePath, string entryName) {
var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
using (var entryStream = entry.Open()) {
using (var fileStream = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite, // allow concurrent writers
65536)) {
fileStream.CopyTo(entryStream);
}
}
}
#endif
void ShowValidation(string message) {
_validationLabel.text = message;
_validationLabel.AddToClassList("validation-label--visible");
}
void HideValidation() {
_validationLabel.RemoveFromClassList("validation-label--visible");
}
#endregion
#region Result screen buttons
void SetupResultScreenButtons() {
var successDiscord = rootVisualElement.Q<Button>("success-discord-button");
var failureDiscord = rootVisualElement.Q<Button>("failure-discord-button");
var failureContact = rootVisualElement.Q<Button>("failure-contact-button");
if (successDiscord != null) {
successDiscord.clickable.clicked += OnDiscordClicked;
}
if (failureDiscord != null) {
failureDiscord.clickable.clicked += OnDiscordClicked;
}
if (failureContact != null) {
failureContact.clickable.clicked += OnContactClicked;
}
bool hasDiscord = !string.IsNullOrEmpty(_discordUrl);
bool hasContact = !string.IsNullOrEmpty(_contactUrl);
if (successDiscord != null) {
successDiscord.style.display = hasDiscord ? DisplayStyle.Flex : DisplayStyle.None;
}
if (failureDiscord != null) {
failureDiscord.style.display = hasDiscord ? DisplayStyle.Flex : DisplayStyle.None;
}
if (failureContact != null) {
failureContact.style.display = hasContact ? DisplayStyle.Flex : DisplayStyle.None;
}
}
void OnDiscordClicked() {
if (!string.IsNullOrEmpty(_discordUrl)) {
Application.OpenURL(_discordUrl);
}
}
void OnContactClicked() {
if (!string.IsNullOrEmpty(_contactUrl)) {
Application.OpenURL(_contactUrl);
}
}
#endregion
}
[Serializable]
internal class Report {
public string Id;
public ReportMode Mode;
public string Title;
public string Severity;
public string Frequency;
public string ContactEmail;
public string Details;
}
}
// Small extension to avoid repetitive null checks on label/button text assignment
namespace SingularityGroup.HotReload.Editor {
internal static class VisualElementExtensions {
public static void SetText(this UnityEngine.UIElements.Label label, string text) {
if (label != null) label.text = text;
}
public static void SetText(this UnityEngine.UIElements.Button button, string text) {
if (button != null) button.text = text;
}
}
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 9cceb96662b8a6841a0c7d4adaf01331
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- m_ViewDataDictionary: {instanceID: 0}
- visualTree: {fileID: 9197481963319205126, guid: d9941d55822724b41bff198971988a21, type: 3}
- styleSheet: {fileID: 7433441132597879392, guid: b8a805d0a53f99248a999c327b3a151e, type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,218 @@
.root {
padding: 12px;
flex-grow: 1;
}
.section {
margin-bottom: 12px;
}
.section-label {
font-size: 13px;
-unity-font-style: bold;
margin-bottom: 4px;
color: var(--unity-colors-label-text);
}
.input-field {
height: 28px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.contact-section {
padding: 10px;
border-width: 1px;
border-color: var(--unity-colors-input_field-border);
border-top-left-radius: 6px;
border-top-right-radius: 6px;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
background-color: rgba(127, 127, 127, 0.08);
}
.fine-print {
font-size: 10px;
color: var(--unity-colors-label-text-disabled);
margin-top: 4px;
white-space: normal;
-unity-font-style: italic;
}
.details-section {
flex-grow: 1;
}
.details-input {
flex-grow: 1;
min-height: 160px;
}
.details-input > .unity-text-field__input {
flex-grow: 1;
-unity-text-align: upper-left;
padding: 6px 8px;
white-space: normal;
}
.details-input .unity-text-element {
white-space: normal;
}
.validation-label {
color: rgb(220, 60, 60);
font-size: 12px;
margin-bottom: 4px;
-unity-text-align: middle-center;
display: none;
}
.validation-label--visible {
display: flex;
}
.submit-row {
flex-direction: row-reverse;
margin-top: 4px;
padding-bottom: 8px;
}
.submit-button {
width: 100px;
height: 30px;
font-size: 13px;
-unity-font-style: bold;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
/* Placeholder styling */
.placeholder-active > .unity-text-field__input > .unity-text-element {
color: var(--unity-colors-label-text-disabled);
}
/* ── Result screens ── */
.result-screen {
display: none;
flex-grow: 1;
justify-content: center;
align-items: center;
padding: 32px;
}
.result-screen--visible {
display: flex;
}
.result-content {
align-items: center;
max-width: 360px;
}
.result-icon {
font-size: 48px;
margin-bottom: 12px;
}
.result-icon--success {
color: rgb(40, 160, 40);
}
.result-icon--failure {
color: rgb(200, 40, 40);
}
.result-title {
font-size: 18px;
-unity-font-style: bold;
margin-bottom: 8px;
color: var(--unity-colors-label-text);
-unity-text-align: middle-center;
}
.result-message {
font-size: 12px;
color: var(--unity-colors-label-text-disabled);
margin-bottom: 6px;
-unity-text-align: middle-center;
white-space: normal;
}
.result-message--error {
color: rgb(200, 60, 60);
-unity-font-style: italic;
margin-bottom: 12px;
}
.result-buttons {
margin-top: 16px;
align-items: center;
}
.result-button {
height: 30px;
min-width: 180px;
margin-bottom: 6px;
font-size: 13px;
-unity-font-style: bold;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.result-button--primary {
background-color: rgb(60, 120, 200);
color: rgb(240, 240, 240);
border-width: 0;
}
.result-button--primary:hover {
background-color: rgb(75, 135, 215);
}
.result-button--secondary {
background-color: rgba(0, 0, 0, 0);
color: var(--unity-colors-label-text-disabled);
border-width: 1px;
border-color: var(--unity-colors-input_field-border);
margin-top: 4px;
}
.result-button--secondary:hover {
color: var(--unity-colors-label-text);
border-color: var(--unity-colors-input_field-border-focus);
}
.tab-bar {
flex-direction: row;
border-bottom-width: 1px;
border-bottom-color: var(--unity-colors-toolbar-border);
background-color: var(--unity-colors-toolbar-background);
flex-shrink: 0;
padding: 0 4px;
}
.tab {
flex: 1;
background-color: transparent;
border-width: 0;
border-radius: 0;
color: var(--unity-colors-label-text-disabled);
padding: 6px 0 5px 0;
margin: 0 2px;
-unity-font-style: normal;
}
.tab--active {
color: var(--unity-colors-label-text);
background-color: var(--unity-colors-toolbar-button-background-checked);
border-width: 0 0 2px 0;
border-color: #3c79f2;
padding-bottom: 3px;
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: b8a805d0a53f99248a999c327b3a151e
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0
unsupportedSelectorAction: 0
@@ -0,0 +1,78 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
<ui:VisualElement name="tabs" class="tab-bar">
<ui:Button name="tab-bug-report" text="Bug Report" class="tab tab--active" />
<ui:Button name="tab-feedback" text="Feedback" class="tab" />
</ui:VisualElement>
<!-- Form view -->
<ui:ScrollView name="scroll-view" class="root">
<!-- Title -->
<ui:VisualElement class="section">
<ui:Label text="Title" class="section-label" />
<ui:TextField name="title-field" class="input-field" />
</ui:VisualElement>
<!-- Issue Severity (PopupField added from C#, hidden in Feedback mode) -->
<ui:VisualElement name="severity-section" class="section">
<ui:Label text="Issue Severity" class="section-label" />
<ui:VisualElement name="severity-container" />
</ui:VisualElement>
<!-- How often does it happen? (hidden in Feedback mode) -->
<ui:VisualElement name="frequency-section" class="section">
<ui:Label text="How often does it happen?" class="section-label" />
<ui:VisualElement name="frequency-container" />
</ui:VisualElement>
<!-- Contact Email -->
<ui:VisualElement class="section contact-section">
<ui:Label text="Contact Email (optional)" class="section-label" />
<ui:TextField name="email-field" class="input-field" />
<ui:Label text="This email will only be used to follow up on this bug report and will not be stored or shared for any other purpose." class="fine-print" />
</ui:VisualElement>
<!-- Details / Feedback -->
<ui:VisualElement class="section details-section">
<ui:Label name="details-label" text="Details" class="section-label" />
<ui:VisualElement name="details-field-slot" class="details-input"/>
</ui:VisualElement>
<!-- Validation message -->
<ui:Label name="validation-label" class="validation-label" />
<!-- Submit -->
<ui:VisualElement class="submit-row">
<ui:Button name="submit-button" text="Submit" class="submit-button" />
</ui:VisualElement>
</ui:ScrollView>
<!-- Success screen (hidden by default) -->
<ui:VisualElement name="success-screen" class="result-screen">
<ui:VisualElement class="result-content">
<ui:Label text="&#x2713;" class="result-icon result-icon--success" />
<ui:Label text="Report Submitted" class="result-title" />
<ui:Label text="Thank you! We've accepted your report and are processing it." class="result-message" />
<ui:VisualElement class="result-buttons">
<ui:Button name="success-discord-button" text="Join Our Discord" class="result-button result-button--primary" />
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
<!-- Failure screen (hidden by default) -->
<ui:VisualElement name="failure-screen" class="result-screen">
<ui:VisualElement class="result-content">
<ui:Label text="&#x2717;" class="result-icon result-icon--failure" />
<ui:Label text="Submission Failed" class="result-title" />
<ui:Label name="failure-error-label" text="" class="result-message result-message--error" />
<ui:Label text="Please reach out to us directly so we can help resolve your issue." class="result-message" />
<ui:VisualElement class="result-buttons">
<ui:Button name="failure-discord-button" text="Join Our Discord" class="result-button result-button--primary" />
<ui:Button name="failure-contact-button" text="Contact Us" class="result-button result-button--primary" />
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: d9941d55822724b41bff198971988a21
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a100625513d043c7bb875461043f4f86
timeCreated: 1673820086
@@ -0,0 +1,137 @@
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using SingularityGroup.HotReload.Newtonsoft.Json;
using UnityEngine;
using System;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Localization;
namespace SingularityGroup.HotReload.Editor.Cli {
internal static class CliUtils {
static readonly string projectIdentifier = GetProjectIdentifier();
class Config {
public bool singleInstance;
}
public static string GetProjectIdentifier() {
if (File.Exists(PackageConst.ConfigFilePath)) {
var config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFilePath));
if (config.singleInstance) {
return null;
}
}
var path = Path.GetFullPath(MultiplayerPlaymodeHelper.PathToMainProject("."));
var name = new DirectoryInfo(path).Name;
using (SHA256 sha256 = SHA256.Create()) {
byte[] inputBytes = Encoding.UTF8.GetBytes(path);
byte[] hashBytes = sha256.ComputeHash(inputBytes);
var hash = BitConverter.ToString(hashBytes).Replace("-", "").Substring(0, 6).ToUpper();
return $"{name}-{hash}";
}
}
public static string GetTempDownloadFilePath(string osxFileName) {
if (UnityHelper.Platform == RuntimePlatform.OSXEditor) {
// project specific temp directory that is writeable on MacOS (Path.GetTempPath() wasn't when run through HotReload.app)
return Path.GetFullPath(PackageConst.LibraryCachePath + $"/HotReloadServerTemp/{osxFileName}");
} else {
return Path.GetTempFileName();
}
}
public static string GetHotReloadTempDir() {
if (UnityHelper.Platform == RuntimePlatform.WindowsEditor) {
// library path interfereces with VS Code file watcher (Source Control window doesn't auto refresh)
// so we pick data path on windows instead
if (projectIdentifier != null) {
return Path.Combine(GetAppDataPath(), "HotReloadServerTemp", projectIdentifier);
} else {
return Path.Combine(GetAppDataPath(), "HotReloadServerTemp");
}
}
// store in library path on mac and linux since it works there
return Path.GetFullPath(Path.Combine(PackageConst.LibraryCachePath, "HotReloadServerTemp"));
}
public static string GetAppDataPath() {
# if (UNITY_EDITOR_OSX)
var baseDir = "/Users/Shared";
# elif (UNITY_EDITOR_LINUX)
var baseDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
# else
var baseDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
#endif
return Path.Combine(baseDir, "singularitygroup-hotreload");
}
public static string GetExecutableTargetDir() {
if (PackageConst.IsAssetStoreBuild) {
if (PackageConst.DefaultLocaleField == Locale.SimplifiedChinese) {
return Path.Combine(GetAppDataPath(), "asset-store", "zh", $"executables_{PackageConst.ServerVersion.Replace('.', '-')}");
}
return Path.Combine(GetAppDataPath(), "asset-store", $"executables_{PackageConst.ServerVersion.Replace('.', '-')}");
}
if (PackageConst.DefaultLocaleField == Locale.SimplifiedChinese) {
return Path.Combine(GetAppDataPath(), "zh", $"executables_{PackageConst.ServerVersion.Replace('.', '-')}");
}
return Path.Combine(GetAppDataPath(), $"executables_{PackageConst.ServerVersion.Replace('.', '-')}");
}
public static string GetCliTempDir() {
return Path.Combine(GetHotReloadTempDir(), "MethodPatches");
}
public static void Chmod(string targetFile, string flags = "+x") {
// ReSharper disable once PossibleNullReferenceException
Process.Start(new ProcessStartInfo("chmod", $"{flags} \"{targetFile}\"") {
UseShellExecute = false,
}).WaitForExit(2000);
}
public static bool TryFindServerDir(out string path) {
const string serverBasePath = "Packages/com.singularitygroup.hotreload/Server";
if(Directory.Exists(serverBasePath)) {
path = Path.GetFullPath(serverBasePath);
return true;
}
//Not found in packages. Try to find in assets folder.
//fast path - this is the expected folder
const string alternativeExecutablePath = "Assets/HotReload/Server";
if(Directory.Exists(alternativeExecutablePath)) {
path = Path.GetFullPath(alternativeExecutablePath);
return true;
}
//slow path - try to find the server directory somewhere in the assets folder
var candidates = Directory.GetDirectories("Assets", "HotReload", SearchOption.AllDirectories);
foreach(var candidate in candidates) {
var serverDir = Path.Combine(candidate, "Server");
if(Directory.Exists(serverDir)) {
path = Path.GetFullPath(serverDir);
return true;
}
}
path = null;
return false;
}
public static string GetPidFilePath(string hotreloadTempDir) {
return Path.GetFullPath(Path.Combine(hotreloadTempDir, "server.pid"));
}
public static void KillLastKnownHotReloadProcess() {
var pidPath = GetPidFilePath(GetHotReloadTempDir());
try {
var pid = int.Parse(File.ReadAllText(pidPath));
Process.GetProcessById(pid).Kill();
}
catch {
//ignore
}
File.Delete(pidPath);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b0243b348dec4a308dc7b98e09842d2c
timeCreated: 1673820875
@@ -0,0 +1,13 @@
using System.Threading.Tasks;
namespace SingularityGroup.HotReload.Editor.Cli {
class FallbackCliController : ICliController {
public string BinaryFileName => "";
public string PlatformName => "";
public bool CanOpenInBackground => false;
public Task Start(StartArgs args) => Task.CompletedTask;
public Task Stop() => Task.CompletedTask;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 090ed5d45f294f0d8799879206139bd6
timeCreated: 1673824275
@@ -0,0 +1,302 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
#if UNITY_EDITOR_WIN
using System.Net.NetworkInformation;
#else
using System.Net.Sockets;
#endif
using System.Threading.Tasks;
using SingularityGroup.HotReload.Editor.Localization;
using SingularityGroup.HotReload.Localization;
using SingularityGroup.HotReload.Newtonsoft.Json;
using UnityEditor;
using Translations = SingularityGroup.HotReload.Editor.Localization.Translations;
namespace SingularityGroup.HotReload.Editor.Cli {
[InitializeOnLoad]
public static class HotReloadCli {
internal static readonly ICliController controller;
//InitializeOnLoad ensures controller gets initialized on unity thread
static HotReloadCli() {
if (MultiplayerPlaymodeHelper.IsClone) {
controller = new FallbackCliController();
return;
}
controller =
#if UNITY_EDITOR_OSX
new OsxCliController();
#elif UNITY_EDITOR_LINUX
new LinuxCliController();
#elif UNITY_EDITOR_WIN
new WindowsCliController();
#else
new FallbackCliController();
#endif
}
public static bool CanOpenInBackground => controller.CanOpenInBackground;
/// <summary>
/// Public API: Starts the Hot Reload server. Must be on the main thread
/// </summary>
public static Task StartAsync() {
return StartAsync(
isReleaseMode: RequestHelper.IsReleaseMode(),
exposeServerToNetwork: HotReloadPrefs.ExposeServerToLocalNetwork,
allAssetChanges: HotReloadPrefs.AllAssetChanges,
createNoWindow: HotReloadPrefs.DisableConsoleWindow,
#if UNITY_EDITOR_WIN
useWatchman: HotReloadPrefs.UseWatchman,
#endif
detailedErrorReporting: !HotReloadPrefs.DisableDetailedErrorReporting,
disableTelemetry: HotReloadPrefs.DisableTelemetry
);
}
internal static async Task StartAsync(
bool exposeServerToNetwork,
bool allAssetChanges,
bool createNoWindow,
bool isReleaseMode,
bool detailedErrorReporting,
bool disableTelemetry,
#if UNITY_EDITOR_WIN
bool useWatchman = true,
#endif
LoginData loginData = null
) {
if (controller.PlatformName == "") {
return;
}
var port = await Prepare().ConfigureAwait(false);
await ThreadUtility.SwitchToThreadPool();
StartArgs args;
if (TryGetStartArgs(
UnityHelper.DataPath,
exposeServerToNetwork,
allAssetChanges,
createNoWindow,
isReleaseMode,
detailedErrorReporting,
disableTelemetry,
loginData,
port,
#if UNITY_EDITOR_WIN
useWatchman,
#endif
out args)) {
await controller.Start(args);
}
}
/// <summary>
/// Public API: Stops the Hot Reload server
/// </summary>
/// <remarks>
/// This is a no-op in case the server is not running
/// </remarks>
public static Task StopAsync() {
return controller.Stop();
}
class Config {
#pragma warning disable CS0649
public bool useBuiltInProjectGeneration;
#pragma warning restore CS0649
}
static bool TryGetStartArgs(
string dataPath,
bool exposeServerToNetwork,
bool allAssetChanges,
bool createNoWindow,
bool isReleaseMode,
bool detailedErrorReporting,
bool disableTelemetry,
LoginData loginData,
int port,
#if UNITY_EDITOR_WIN
bool useWatchman,
#endif
out StartArgs args
) {
string serverDir;
if(!CliUtils.TryFindServerDir(out serverDir)) {
Log.Warning(string.Format(Translations.Errors.WarningFailedToStartServer,
Translations.Utility.UnableToLocateServer +
Translations.Utility.UnableToLocateServerDetail));
args = null;
return false;
}
Config config;
if (File.Exists(PackageConst.ConfigFileName)) {
config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFileName));
} else {
config = new Config();
}
var hotReloadTmpDir = CliUtils.GetHotReloadTempDir();
var cliTempDir = CliUtils.GetCliTempDir();
// Versioned path so that we only need to extract the binary once. User can have multiple projects
// on their machine using different HotReload versions.
var executableTargetDir = CliUtils.GetExecutableTargetDir();
Directory.CreateDirectory(executableTargetDir); // ensure exists
var executableSourceDir = Path.Combine(serverDir, controller.PlatformName);
var unityProjDir = Path.GetDirectoryName(dataPath);
string slnPath;
if (config.useBuiltInProjectGeneration) {
var info = new DirectoryInfo(Path.GetFullPath("."));
slnPath = Path.Combine(Path.GetFullPath("."), info.Name + ".sln");
if (!File.Exists(slnPath)) {
Log.Warning(string.Format(Translations.Errors.WarningFailedToStartServer, Translations.Utility.CannotFindSolutionFile));
args = null;
return false;
}
Log.Info(Translations.Errors.InfoDefaultProjectGeneration);
try {
Directory.Delete(ProjectGeneration.ProjectGeneration.tempDir, true);
} catch(Exception ex) {
Log.Exception(ex);
}
} else {
slnPath = ProjectGeneration.ProjectGeneration.GetSolutionFilePath(dataPath);
}
if (!File.Exists(slnPath)) {
Log.Warning(Translations.Errors.WarningNoSlnFileFound);
}
var searchAssemblies = string.Join(";", CodePatcher.I.GetAssemblySearchPaths());
var cliArguments = $@"-u ""{unityProjDir}"" -s ""{slnPath}"" -t ""{cliTempDir}"" -a ""{searchAssemblies}"" -ver ""{PackageConst.Version}"" -proc ""{Process.GetCurrentProcess().Id}"" -assets ""{allAssetChanges}"" -p ""{port}"" -r {isReleaseMode} -detailed-error-reporting {detailedErrorReporting} -default-locale {PackageConst.DefaultLocale}";
if (loginData != null) {
cliArguments += $@" -email ""{loginData.email}"" -pass ""{loginData.password}""";
}
if (disableTelemetry) {
cliArguments += $@" -disableTelemetry true";
}
#if UNITY_EDITOR_WIN
cliArguments += $@" -w ""{useWatchman}""";
#endif
if (exposeServerToNetwork) {
// server will listen on local network interface (default is localhost only)
cliArguments += " -e true";
}
args = new StartArgs {
hotreloadTempDir = hotReloadTmpDir,
cliTempDir = cliTempDir,
executableTargetDir = executableTargetDir,
executableSourceDir = executableSourceDir,
cliArguments = cliArguments,
unityProjDir = unityProjDir,
createNoWindow = createNoWindow,
};
return true;
}
private static int DiscoverFreePort() {
var maxAttempts = 10;
for (int attempt = 0; attempt < maxAttempts; attempt++) {
var port = RequestHelper.defaultPort + attempt;
if (IsPortInUse(port)) {
continue;
}
return port;
}
// we give up at this point
return RequestHelper.defaultPort + maxAttempts;
}
public static bool IsPortInUse(int port) {
// Note that there is a racecondition that a port gets occupied after checking.
// However, it will very rare someone will run into this.
#if UNITY_EDITOR_WIN
IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
IPEndPoint[] activeTcpListeners = ipGlobalProperties.GetActiveTcpListeners();
foreach (IPEndPoint endPoint in activeTcpListeners) {
if (endPoint.Port == port) {
return true;
}
}
return false;
#else
try {
using (TcpClient tcpClient = new TcpClient()) {
tcpClient.Connect(IPAddress.Loopback, port); // Try to connect to the specified port
return true;
}
} catch (SocketException) {
return false;
} catch (Exception e) {
Log.Exception(e);
// act as if the port is allocated
return true;
}
#endif
}
static async Task<int> Prepare() {
await ThreadUtility.SwitchToMainThread();
var dataPath = UnityHelper.DataPath;
await ProjectGeneration.ProjectGeneration.EnsureSlnAndCsprojFiles(dataPath);
await PrepareBuildInfoAsync();
PrepareSystemPathsFile();
var port = DiscoverFreePort();
HotReloadState.ServerPort = port;
var serverInfo = RequestHelper.SetServerPort(port);
await Task.Run(() => File.WriteAllText(Path.Combine(PackageConst.LibraryCachePath, PackageConst.ServerInfoFileName), JsonConvert.SerializeObject(serverInfo)));
return port;
}
static bool didLogWarning;
internal static async Task PrepareBuildInfoAsync() {
await ThreadUtility.SwitchToMainThread();
var buildInfoInput = await BuildInfoHelper.GetGenerateBuildInfoInput();
await Task.Run(() => {
try {
var buildInfo = BuildInfoHelper.GenerateBuildInfoThreaded(buildInfoInput);
PrepareBuildInfo(buildInfo);
} catch (Exception e) {
if (!didLogWarning) {
Log.Warning(string.Format(Translations.Errors.WarningPreparingBuildInfoFailed, e));
didLogWarning = true;
} else {
Log.Debug(string.Format(Translations.Utility.PreparingBuildInfoFailed, e));
}
}
});
}
internal static void PrepareBuildInfo(BuildInfo buildInfo) {
// When starting server make sure it starts with correct player data state.
// (this fixes issue where Unity is in background and not sending files state).
// Always write player data because you can be on any build target and want to connect with a downloaded android build.
var json = buildInfo.ToJson();
var cliTempDir = CliUtils.GetCliTempDir();
Directory.CreateDirectory(cliTempDir);
File.WriteAllText(Path.Combine(cliTempDir, "playerdata.json"), json);
}
static void PrepareSystemPathsFile() {
#pragma warning disable CS0618 // obsolete since 2023
var lvl = PlayerSettings.GetApiCompatibilityLevel(EditorUserBuildSettings.selectedBuildTargetGroup);
#pragma warning restore CS0618
#if UNITY_2020_3_OR_NEWER
var dirs = UnityEditor.Compilation.CompilationPipeline.GetSystemAssemblyDirectories(lvl);
#else
var t = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.Scripting.ScriptCompilation.MonoLibraryHelpers");
var m = t.GetMethod("GetSystemReferenceDirectories");
var dirs = m.Invoke(null, new object[] { lvl });
#endif
Directory.CreateDirectory(PackageConst.LibraryCachePath);
File.WriteAllText(PackageConst.LibraryCachePath + "/systemAssemblies.json", JsonConvert.SerializeObject(dirs));
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9f756ed6b78d428b8b9f83a6544317fe
timeCreated: 1673820326
@@ -0,0 +1,13 @@
using System.Threading.Tasks;
namespace SingularityGroup.HotReload.Editor.Cli {
interface ICliController {
string BinaryFileName {get;}
string PlatformName {get;}
bool CanOpenInBackground {get;}
Task Start(StartArgs args);
Task Stop();
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8cba48e21f76483da3ba615915e731fd
timeCreated: 1673820542
@@ -0,0 +1,74 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using SingularityGroup.HotReload.Editor.Localization;
using Debug = UnityEngine.Debug;
namespace SingularityGroup.HotReload.Editor.Cli {
class LinuxCliController : ICliController {
Process process;
public string BinaryFileName => "CodePatcherCLI";
public string PlatformName => "linux-x64";
public bool CanOpenInBackground => true;
public Task Start(StartArgs args) {
var startScript = Path.Combine(args.executableSourceDir, "hotreload-start-script.sh");
if (!File.Exists(startScript)) {
throw new FileNotFoundException(startScript);
}
File.WriteAllText(startScript, File.ReadAllText(startScript).Replace("\r\n", "\n"));
CliUtils.Chmod(startScript);
var title = CodePatcher.TAG + "Server " + new DirectoryInfo(args.unityProjDir).Name;
title = title.Replace(" ", "-");
title = title.Replace("'", "");
var cliargsfile = Path.GetTempFileName();
File.WriteAllText(cliargsfile,args.cliArguments);
var codePatcherProc = Process.Start(new ProcessStartInfo {
FileName = startScript,
Arguments =
$"--title \"{title}\""
+ $" --executables-source-dir \"{args.executableSourceDir}\" "
+ $" --executable-taget-dir \"{args.executableTargetDir}\""
+ $" --pidfile \"{CliUtils.GetPidFilePath(args.hotreloadTempDir)}\""
+ $" --cli-arguments-file \"{cliargsfile}\""
+ $" --method-patch-dir \"{args.cliTempDir}\""
+ $" --create-no-window \"{args.createNoWindow}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true
});
if (codePatcherProc == null) {
if (File.Exists(cliargsfile)) {
File.Delete(cliargsfile);
}
throw new Exception(Translations.Errors.ExceptionCouldNotStartCodePatcher);
}
codePatcherProc.BeginErrorReadLine();
codePatcherProc.BeginOutputReadLine();
codePatcherProc.OutputDataReceived += (_, a) => {
};
// error data can also mean we kill the proc beningly
codePatcherProc.ErrorDataReceived += (_, a) => {
};
process = codePatcherProc;
return Task.CompletedTask;
}
public async Task Stop() {
await RequestHelper.KillServer();
try {
// process.CloseMainWindow throws if proc already exited.
// also we just rely on the pid file it is fine
CliUtils.KillLastKnownHotReloadProcess();
} catch {
//ignored
}
process = null;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c894a69d595d4ada8cfa4afe23c68ab9
timeCreated: 1673820131
@@ -0,0 +1,196 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using SingularityGroup.HotReload.Editor.Localization;
using SingularityGroup.HotReload.Editor.Semver;
using Debug = UnityEngine.Debug;
namespace SingularityGroup.HotReload.Editor.Cli {
class OsxCliController : ICliController {
Process process;
public string BinaryFileName => "HotReload.app.zip";
public string PlatformName => "osx-x64";
public bool CanOpenInBackground => true;
/// In MacOS 13 Ventura, our app cannot launch a terminal window.
/// We use a custom app that launches HotReload server and shows it's output (just like a terminal would).
// Including MacOS 12 Monterey as well so I can dogfood it -Troy
private static bool UseCustomConsoleApp() => MacOSVersion.Value.Major >= 12;
// dont use static because null comparison on SemVersion is broken
private static readonly Lazy<SemVersion> MacOSVersion = new Lazy<SemVersion>(() => {
//UnityHelper.OperatingSystem; // in Unity 2018 it returns 10.16 on monterey (no idea why)
//Environment.OSVersion returns unix version like 21.x
var startinfo = new ProcessStartInfo {
FileName = "/usr/bin/sw_vers",
Arguments = "-productVersion",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
};
var process = Process.Start(startinfo);
string osVersion = process.StandardOutput.ReadToEnd().Trim();
SemVersion macosVersion;
if (SemVersion.TryParse(osVersion, out macosVersion)) {
return macosVersion;
}
// should never happen
Log.Warning(Translations.Errors.WarningMacOSVersionDetectionFailed);
return SemVersion.None;
});
public async Task Start(StartArgs args) {
// Unzip the .app.zip to temp folder .app
var appExecutablePath = $"{args.executableTargetDir}/HotReload.app/Contents/MacOS/HotReload";
var cliExecutablePath = $"{args.executableTargetDir}/HotReload.app/Contents/Resources/CodePatcherCLI";
// ensure running on threadpool
await ThreadUtility.SwitchToThreadPool();
// executableTargetDir is versioned, so only need to extract once.
if (!File.Exists(appExecutablePath)) {
try {
// delete only the extracted app folder (must not delete downloaded zip which is in same folder)
Directory.Delete(args.executableTargetDir + "/HotReload.app", true);
} catch (IOException) {
// ignore directory not found
}
Directory.CreateDirectory(args.executableTargetDir);
UnzipMacOsPackage($"{args.executableTargetDir}/{BinaryFileName}", args.executableTargetDir + "/");
}
try {
// Always stop first because rarely it has happened that the server process was still running after custom console closed.
// Note: this will also stop Hot Reload started by other Unity projects.
await Stop();
} catch {
// ignored
}
if (args.createNoWindow) {
await StartExecutableOrApp(args, cliExecutablePath);
} else if (UseCustomConsoleApp()) {
await StartExecutableOrApp(args, appExecutablePath);
} else {
await StartTerminal(args, cliExecutablePath);
}
}
public Task StartExecutableOrApp(StartArgs args, string executablePath) {
process = Process.Start(new ProcessStartInfo {
// Path to the HotReload.app
FileName = executablePath,
Arguments = args.cliArguments,
UseShellExecute = false,
});
var pidFilePath = CliUtils.GetPidFilePath(args.hotreloadTempDir);
// ReSharper disable once PossibleNullReferenceException
File.WriteAllText(pidFilePath, process.Id.ToString());
return Task.CompletedTask;
}
public Task StartTerminal(StartArgs args, string executablePath) {
var pidFilePath = CliUtils.GetPidFilePath(args.hotreloadTempDir);
// To run in a Terminal window (so you can see compiler logs), we must put the arguments into a script file
// and run the script in Terminal. Terminal.app does not forward the arguments passed to it via `open --args`.
// *.command files are opened with the user's default terminal app.
var executableScriptPath = Path.Combine(Path.GetTempPath(), "Start_HotReloadServer.command");
// You don't need to copy the cli executable on mac
// omit hashbang line, let shell use the default interpreter (easier than detecting your default shell beforehand)
File.WriteAllText(executableScriptPath, $"echo $$ > \"{pidFilePath}\"" +
$"\ncd \"{Environment.CurrentDirectory}\"" + // set cwd because 'open' launches script with $HOME as cwd.
$"\n\"{executablePath}\" {args.cliArguments} || read");
CliUtils.Chmod(executableScriptPath); // make it executable
CliUtils.Chmod(executablePath); // make it executable
Directory.CreateDirectory(args.hotreloadTempDir);
Directory.CreateDirectory(args.executableTargetDir);
Directory.CreateDirectory(args.cliTempDir);
process = Process.Start(new ProcessStartInfo {
FileName = "open",
Arguments = $"{(args.createNoWindow ? "-gj" : "")} '{executableScriptPath}'",
UseShellExecute = true,
});
if (process.WaitForExit(1000)) {
if (process.ExitCode != 0) {
Log.Warning(Translations.Errors.WarningFailedToRunServerCommand, process.ExitCode, executableScriptPath);
}
}
else {
process.EnableRaisingEvents = true;
process.Exited += (_, __) => {
if (process.ExitCode != 0) {
Log.Warning(Translations.Errors.WarningFailedToRunServerCommand, process.ExitCode, executableScriptPath);
}
};
}
return Task.CompletedTask;
}
public async Task Stop() {
// kill HotReload server process (on mac it has different pid to the window which started it)
await RequestHelper.KillServer();
// process.CloseMainWindow throws if proc already exited.
// We rely on the pid file for killing the trampoline script (in-case script is just starting and HotReload server not running yet)
process = null;
CliUtils.KillLastKnownHotReloadProcess();
}
static void UnzipMacOsPackage(string zipPath, string unzippedFolderPath) {
//Log.Info("UnzipMacOsPackage called with {0}\n workingDirectory = {1}", zipPath, unzippedFolderPath);
if (!zipPath.EndsWith(".zip")) {
throw new ArgumentException(string.Format(Translations.Errors.ExceptionExpectedZipFile, zipPath), nameof(zipPath));
}
if (!File.Exists(zipPath)) {
throw new ArgumentException(string.Format(Translations.Errors.ExceptionZipFileNotFound, zipPath), nameof(zipPath));
}
var processStartInfo = new ProcessStartInfo {
FileName = "unzip",
Arguments = $"-o \"{zipPath}\"",
WorkingDirectory = unzippedFolderPath, // unzip extracts to working directory by default
UseShellExecute = true,
CreateNoWindow = true
};
Process process = Process.Start(processStartInfo);
process.WaitForExit();
if (process.ExitCode != 0) {
throw new Exception(string.Format(Translations.Errors.ExceptionUnzipFailed, process.ExitCode));
}
//Log.Info($"did unzip to {unzippedFolderPath}");
// Move the .app folder to unzippedFolderPath
// find the .app directory which is now inside unzippedFolderPath directory
var foundDirs = Directory.GetDirectories(unzippedFolderPath, "*.app", SearchOption.AllDirectories);
var done = false;
var destDir = unzippedFolderPath + "HotReload.app";
foreach (var dir in foundDirs) {
if (dir.EndsWith(".app")) {
done = true;
if (dir == destDir) {
// already in the right place
break;
}
Directory.Move(dir, destDir);
//Log.Info("Moved to " + destDir);
break;
}
}
if (!done) {
throw new Exception(string.Format(Translations.Errors.ExceptionFailedToFindAppDirectory, destDir));
}
//Log.Info($"did unzip to {unzippedFolderPath}");
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5ebeed1c29454bc78e5a9ee64f2c9def
timeCreated: 1673821666
@@ -0,0 +1,12 @@
namespace SingularityGroup.HotReload.Editor.Cli {
class StartArgs {
public string hotreloadTempDir;
// aka method patch temp dir
public string cliTempDir;
public string executableTargetDir;
public string executableSourceDir;
public string cliArguments;
public string unityProjDir;
public bool createNoWindow;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 43d69eb7ae8aef4428da83562105bfaa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,33 @@
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace SingularityGroup.HotReload.Editor.Cli {
class WindowsCliController : ICliController {
Process process;
public string BinaryFileName => "CodePatcherCLI.exe";
public string PlatformName => "win-x64";
public bool CanOpenInBackground => true;
public Task Start(StartArgs args) {
process = Process.Start(new ProcessStartInfo {
FileName = Path.GetFullPath(Path.Combine(args.executableTargetDir, "CodePatcherCLI.exe")),
Arguments = args.cliArguments,
UseShellExecute = !args.createNoWindow,
CreateNoWindow = args.createNoWindow,
});
return Task.CompletedTask;
}
public async Task Stop() {
await RequestHelper.KillServer();
try {
process?.CloseMainWindow();
} catch {
//ignored
}
process = null;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e5644af69ec7404a8039ff2833610d48
timeCreated: 1673822169
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 80c2056f805745542a2c295385b25479
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,74 @@
#if UNITY_2019_1_OR_NEWER
using System;
using System.IO;
using System.Threading.Tasks;
using SingularityGroup.HotReload.Editor.Localization;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor {
class DefaultCompileChecker : ICompileChecker {
static string recompileFilePath = PackageConst.LibraryCachePath + "/recompile.txt";
public bool hasCompileErrors { get; private set; }
bool recompile;
public DefaultCompileChecker() {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
CompilationPipeline.assemblyCompilationFinished += DetectCompileErrors;
CompilationPipeline.compilationFinished += OnCompilationFinished;
var currentSessionId = EditorAnalyticsSessionInfo.id;
Task.Run(() => {
try {
var compileSessionId = File.ReadAllText(recompileFilePath);
if(compileSessionId == currentSessionId.ToString()) {
ThreadUtility.RunOnMainThread(() => {
recompile = true;
_onCompilationFinished?.Invoke();
});
}
File.Delete(recompileFilePath);
} catch(DirectoryNotFoundException) {
//dir doesn't exist -> no recompile required
} catch(FileNotFoundException) {
//file doesn't exist -> no recompile required
} catch(Exception ex) {
Log.Warning(Translations.Errors.WarningCompileCheckerIssue, ex.GetType().Name, ex.Message);
}
});
}
void DetectCompileErrors(string _, CompilerMessage[] messages) {
for (int i = 0; i < messages.Length; i++) {
if (messages[i].type == CompilerMessageType.Error) {
hasCompileErrors = true;
return;
}
}
hasCompileErrors = false;
}
void OnCompilationFinished(object _) {
//Don't recompile on compile errors
if(!hasCompileErrors) {
Directory.CreateDirectory(Path.GetDirectoryName(recompileFilePath));
File.WriteAllText(recompileFilePath, EditorAnalyticsSessionInfo.id.ToString());
}
}
Action _onCompilationFinished;
public event Action onCompilationFinished {
add {
if(recompile && value != null) {
value();
}
_onCompilationFinished += value;
}
remove {
_onCompilationFinished -= value;
}
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ab09f7c657e6ecb44b65dd9f8cfc3d9f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
using System;
namespace SingularityGroup.HotReload.Editor {
interface ICompileChecker {
event Action onCompilationFinished;
bool hasCompileErrors { get; }
}
static class CompileChecker {
internal static ICompileChecker Create() {
#if UNITY_2019_1_OR_NEWER
return new DefaultCompileChecker();
#else
return new LegacyCompileChecker();
#endif
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 82bf36f2126bbd1498d4964272426e0f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,55 @@
#if !UNITY_2019_1_OR_NEWER
using System;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
namespace SingularityGroup.HotReload.Editor {
class LegacyCompileChecker : ICompileChecker {
static readonly string timestampFilePath = PackageConst.LibraryCachePath + "/lastCompileTimestamp.txt";
public bool hasCompileErrors { get; }
const string assemblyPath = "Library/ScriptAssemblies";
bool recompile;
public LegacyCompileChecker() {
Task.Run(() => {
var info = new DirectoryInfo(assemblyPath);
if(!info.Exists) {
return;
}
var currentCompileTimestamp = default(DateTime);
foreach (var file in info.GetFiles("*.dll")) {
var fileWriteDate = file.LastWriteTimeUtc;
if(fileWriteDate > currentCompileTimestamp) {
currentCompileTimestamp = fileWriteDate;
}
}
if(File.Exists(timestampFilePath)) {
var lastTimestampStr = File.ReadAllText(timestampFilePath);
var lastTimestamp = DateTime.ParseExact(lastTimestampStr, "o", CultureInfo.CurrentCulture).ToUniversalTime();
if(currentCompileTimestamp > lastTimestamp) {
ThreadUtility.RunOnMainThread(() => {
recompile = true;
_onCompilationFinished?.Invoke();
});
}
}
Directory.CreateDirectory(Path.GetDirectoryName(timestampFilePath));
File.WriteAllText(timestampFilePath, currentCompileTimestamp.ToString("o"));
});
}
Action _onCompilationFinished;
public event Action onCompilationFinished {
add {
if(recompile && value != null) {
value();
}
_onCompilationFinished += value;
}
remove {
_onCompilationFinished -= value;
}
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f56ec68ce4b1fcc4b9c8ba5962d890f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,50 @@
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Editor.Localization;
using SingularityGroup.HotReload.Localization;
using Translations = SingularityGroup.HotReload.Editor.Localization.Translations;
namespace SingularityGroup.HotReload.Editor {
internal static class Constants {
public const string WebsiteURL = PackageConst.DefaultLocale == Locale.SimplifiedChinese ?
"https://hotreload.net/zh" :
"https://hotreload.net";
public const string ProductPurchaseURL = WebsiteURL + "/pricing";
public const string ProductPurchaseBusinessURL = ProductPurchaseURL + "?tab=business";
public const string DocumentationURL = WebsiteURL + "/documentation";
public const string AdditionalContentURL = DocumentationURL + "/getting-started#downloading-additional-content";
public const string DownloadUrl = WebsiteURL + "/download";
public const string ContactURL = WebsiteURL + "/contact";
public const string ForumURL = "https://forum.unity.com/threads/hot-reload-edit-code-without-compiling.1389969/";
public const string ManageLicenseURL = "https://billing.stripe.com/p/login/28odTObUQ0CU0Za3cc";
public const string ManageAccountURL = "https://users.licensespring.com/login";
public const string ForgotPasswordURL = "https://users.licensespring.com/reset-password";
public const string ReportIssueURL = "https://gitlab.com/singularitygroup/hot-reload-for-unity/-/issues/new";
public const string TroubleshootingURL = DocumentationURL + "/troubleshooting";
public const string RecompileTroubleshootingURL = TroubleshootingURL + "#unity-recompiles-every-time-i-enterexit-playmode";
public const string FeaturesDocumentationURL = DocumentationURL + "/features";
public const string MultipleEditorsURL = DocumentationURL + "/multiple-editors";
public const string DebuggerURL = DocumentationURL + "/debugger";
public const string SecurityURL = DocumentationURL + "/security";
public const string UndetectedChangesURL = DocumentationURL + "/getting-started#undetected-changes";
public const string VoteForAwardURL = "https://awards.unity.com/#best-development-tool";
public const string UnityStoreRateAppURL = "https://assetstore.unity.com/packages/slug/254358#reviews";
public const string ChangelogURL = WebsiteURL + "/changelog";
public const string DiscordInviteUrl = "https://discord.com/invite/kgxAS4Bqxr";
public const int DaysToRateApp = 5;
public const int RecompileButtonTextHideWidth = 460;
public const int IndicationTextHideWidth = 360;
public const int StartButtonTextHideWidth = 400;
public const int EventsListHideHeight = 360;
public const int EventsListHideWidth = 425;
public const int UpgradeLicenseNoteHideWidth = 325;
public const int UpgradeLicenseNoteHideHeight = 150;
public const int RateAppHideHeight = 325;
public const int RateAppHideWidth = 300;
public const int EventFiltersShownHideWidth = 325;
public const int ConsumptionsHideWidth = 300;
public const int ConsumptionsHideHeight = 360;
public static string Only40EntriesShown => Translations.Timeline.MessageOnly40EntriesShown;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ce502822e7fa34844bcb385f44091eb9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7c5c2596a7a469c42a1a6b35017d8a49
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,26 @@
using System.Collections;
using System.IO;
using SingularityGroup.HotReload.Demo;
using UnityEditor;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor.Demo {
class EditorDemo : IDemo {
public bool IsServerRunning() {
return ServerHealthCheck.I.IsServerHealthy;
}
public void OpenHotReloadWindow() {
HotReloadWindow.Open();
}
public void OpenScriptFile(TextAsset textAsset, int line, int column) {
var path = Path.GetFullPath(AssetDatabase.GetAssetPath(textAsset));
#if UNITY_2019_4_OR_NEWER
Unity.CodeEditor.CodeEditor.CurrentEditor.OpenProject(path, line, column);
#else
EditorUtility.OpenWithDefaultApp(path);
#endif
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fde6b5b57a3aeba4888a7bdaa16b3074
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ac7b192276a4a9d4f9098377d317cb2e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Editor.Localization;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor {
internal static class EditorIndicationState {
internal enum IndicationStatus {
Stopped,
Started,
Stopping,
Installing,
Starting,
Reloaded,
PartiallySupported,
Unsupported,
Patching,
Loading,
Compiling,
CompileErrors,
ActivationFailed,
FinishRegistration,
DisabledDuringDebugging,
Undetected,
Paused,
}
internal static readonly string greyIconPath = "Hot_Reload_status_grey";
internal static readonly string greenIconPath = "Hot_Reload_status_green";
internal static readonly string redIconPath = "Hot_Reload_status_red";
private static readonly Dictionary<IndicationStatus, string> IndicationIcon = new Dictionary<IndicationStatus, string> {
// grey icon:
{ IndicationStatus.FinishRegistration, greyIconPath },
{ IndicationStatus.Stopped, greyIconPath },
{ IndicationStatus.Paused, greyIconPath },
// green icon:
{ IndicationStatus.Started, greenIconPath },
// log icons:
{ IndicationStatus.Reloaded, HotReloadTimelineHelper.alertIconString[AlertType.AppliedChange] },
{ IndicationStatus.Unsupported, HotReloadTimelineHelper.alertIconString[AlertType.UnsupportedChange] },
{ IndicationStatus.Undetected, HotReloadTimelineHelper.alertIconString[AlertType.UndetectedChange] },
{ IndicationStatus.DisabledDuringDebugging, HotReloadTimelineHelper.alertIconString[AlertType.UnsupportedChange] },
{ IndicationStatus.PartiallySupported, HotReloadTimelineHelper.alertIconString[AlertType.PartiallySupportedChange] },
{ IndicationStatus.CompileErrors, HotReloadTimelineHelper.alertIconString[AlertType.CompileError] },
// spinner:
{ IndicationStatus.Stopping, Spinner.SpinnerIconPath },
{ IndicationStatus.Starting, Spinner.SpinnerIconPath },
{ IndicationStatus.Patching, Spinner.SpinnerIconPath },
{ IndicationStatus.Loading, Spinner.SpinnerIconPath },
{ IndicationStatus.Compiling, Spinner.SpinnerIconPath },
{ IndicationStatus.Installing, Spinner.SpinnerIconPath },
// red icon:
{ IndicationStatus.ActivationFailed, redIconPath },
};
private static readonly IndicationStatus[] SpinnerIndications = IndicationIcon
.Where(kvp => kvp.Value == Spinner.SpinnerIconPath)
.Select(kvp => kvp.Key)
.ToArray();
// NOTE: if you add longer text, make sure UI is wide enough for it
public static Dictionary<IndicationStatus, string> IndicationText => new Dictionary<IndicationStatus, string> {
{ IndicationStatus.FinishRegistration, Translations.Miscellaneous.IndicationFinishRegistration },
{ IndicationStatus.Started, Translations.Miscellaneous.IndicationStarted },
{ IndicationStatus.Stopping, Translations.Miscellaneous.IndicationStopping },
{ IndicationStatus.Stopped, Translations.Miscellaneous.IndicationStopped },
{ IndicationStatus.Paused, Translations.Miscellaneous.IndicationPaused },
{ IndicationStatus.Installing, Translations.Miscellaneous.IndicationInstalling },
{ IndicationStatus.Starting, Translations.Miscellaneous.IndicationStarting },
{ IndicationStatus.Reloaded, Translations.Miscellaneous.IndicationReloaded },
{ IndicationStatus.PartiallySupported, Translations.Miscellaneous.IndicationPartiallySupported },
{ IndicationStatus.Unsupported, Translations.Miscellaneous.IndicationUnsupported },
{ IndicationStatus.Patching, Translations.Miscellaneous.IndicationPatching },
{ IndicationStatus.DisabledDuringDebugging, Translations.Miscellaneous.IndicationDisabledDuringDebugging },
{ IndicationStatus.Compiling, Translations.Miscellaneous.IndicationCompiling },
{ IndicationStatus.CompileErrors, Translations.Miscellaneous.IndicationCompileErrors },
{ IndicationStatus.ActivationFailed, Translations.Miscellaneous.IndicationActivationFailed },
{ IndicationStatus.Loading, Translations.Miscellaneous.IndicationLoading },
{ IndicationStatus.Undetected, Translations.Miscellaneous.IndicationUndetected},
};
private const int MinSpinnerDuration = 200;
private static DateTime spinnerStartedAt;
private static IndicationStatus latestStatus;
private static bool SpinnerCompletedMinDuration => DateTime.UtcNow - spinnerStartedAt > TimeSpan.FromMilliseconds(MinSpinnerDuration);
private static IndicationStatus GetIndicationStatus() {
var status = GetIndicationStatusCore();
// Note: performance sensitive code, don't use Link
bool newStatusIsSpinner = false;
for (var i = 0; i < SpinnerIndications.Length; i++) {
if (SpinnerIndications[i] == status) {
newStatusIsSpinner = true;
}
}
bool latestStatusIsSpinner = false;
for (var i = 0; i < SpinnerIndications.Length; i++) {
if (SpinnerIndications[i] == latestStatus) {
newStatusIsSpinner = true;
}
}
if (status == latestStatus) {
return status;
} else if (latestStatusIsSpinner) {
if (newStatusIsSpinner) {
return status;
} else if (SpinnerCompletedMinDuration) {
latestStatus = status;
return status;
} else {
return latestStatus;
}
} else if (newStatusIsSpinner) {
spinnerStartedAt = DateTime.UtcNow;
latestStatus = status;
return status;
} else {
spinnerStartedAt = DateTime.UtcNow;
latestStatus = IndicationStatus.Loading;
return status;
}
}
private static IndicationStatus GetIndicationStatusCore() {
if (RedeemLicenseHelper.I.RegistrationRequired)
return IndicationStatus.FinishRegistration;
if (EditorCodePatcher.DownloadRequired && EditorCodePatcher.DownloadStarted || EditorCodePatcher.RequestingDownloadAndRun && !EditorCodePatcher.Starting && !EditorCodePatcher.Stopping)
return IndicationStatus.Installing;
if (EditorCodePatcher.Stopping)
return IndicationStatus.Stopping;
if (EditorCodePatcher.Compiling && !EditorCodePatcher.Stopping && !EditorCodePatcher.Starting && EditorCodePatcher.Running)
return IndicationStatus.Compiling;
if (EditorCodePatcher.Starting && !EditorCodePatcher.Stopping)
return IndicationStatus.Starting;
if (!Application.isPlaying && HotReloadPrefs.PauseHotReloadInEditMode)
return IndicationStatus.Paused;
if (!EditorCodePatcher.Running)
return IndicationStatus.Stopped;
if (EditorCodePatcher.Status?.isLicensed != true && EditorCodePatcher.Status?.isFree != true && EditorCodePatcher.Status?.freeSessionFinished == true)
return IndicationStatus.ActivationFailed;
if (EditorCodePatcher.compileError)
return IndicationStatus.CompileErrors;
// fallback on patch status
if (!EditorCodePatcher.Started && !EditorCodePatcher.Running) {
return IndicationStatus.Stopped;
}
if (Debugger.IsAttached && !CodePatcher.I.debuggerCompatibilityEnabled) {
return IndicationStatus.DisabledDuringDebugging;
}
switch (EditorCodePatcher.patchStatus) {
case PatchStatus.Idle:
if (!EditorCodePatcher.Compiling && !EditorCodePatcher.firstPatchAttempted && !EditorCodePatcher.compileError) {
return IndicationStatus.Started;
}
if (EditorCodePatcher._applyingFailed) {
return IndicationStatus.Unsupported;
}
if (EditorCodePatcher._appliedPartially) {
return IndicationStatus.PartiallySupported;
}
if (EditorCodePatcher._appliedUndetected) {
return IndicationStatus.Undetected;
}
return IndicationStatus.Reloaded;
case PatchStatus.Patching: return IndicationStatus.Patching;
case PatchStatus.Unsupported: return IndicationStatus.Unsupported;
case PatchStatus.Compiling: return IndicationStatus.Compiling;
case PatchStatus.CompileError: return IndicationStatus.CompileErrors;
case PatchStatus.None:
default: return IndicationStatus.Reloaded;
}
}
internal static IndicationStatus CurrentIndicationStatus => GetIndicationStatus();
internal static bool SpinnerActive => SpinnerIndications.Contains(CurrentIndicationStatus);
internal static string IndicationIconPath => IndicationIcon[CurrentIndicationStatus];
internal static string IndicationStatusText {
get {
var indicationStatus = CurrentIndicationStatus;
string txt;
if (indicationStatus == IndicationStatus.Starting && EditorCodePatcher.StartupProgress != null) {
txt = EditorCodePatcher.StartupProgress.Item2;
} else if (!IndicationText.TryGetValue(indicationStatus, out txt)) {
Log.Warning(Translations.Errors.WarningIndicationTextNotFound, indicationStatus);
} else {
txt = IndicationText[indicationStatus];
}
return txt;
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ee342ddb17e444c7a8927be3bd792ae2
timeCreated: 1686087206
@@ -0,0 +1,87 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using Debug = UnityEngine.Debug;
namespace SingularityGroup.HotReload.Editor {
internal static class GitUtil {
/// <remarks>
/// Fallback is PatchServerInfo.UnknownCommitHash
/// </remarks>
public static string GetShortCommitHashOrFallback(int timeoutAfterMillis = 5000) {
var shortCommitHash = PatchServerInfo.UnknownCommitHash;
var commitHash = GetShortCommitHashSafe(timeoutAfterMillis);
// On MacOS GetShortCommitHash() returns 7 characters, on Windows it returns 8 characters.
// When git command produced an unexpected result, use a fallback string
if (commitHash != null && commitHash.Length >= 6) {
shortCommitHash = commitHash.Length < 8 ? commitHash : commitHash.Substring(0, 8);
}
return shortCommitHash;
}
// only log exception once per domain reload, to prevent spamming the console
private static bool loggedExceptionInGetShortCommitHashSafe = false;
/// <summary>
/// Get the git commit hash, returning null if it takes too long.
/// </summary>
/// <param name="timeoutAfterMillis"></param>
/// <returns></returns>
/// <remarks>
/// This method is 'better safe than sorry' because we must not break the user's build.<br/>
/// It is better to not know the commit hash than to fail the build.
/// </remarks>
private static string GetShortCommitHashSafe(int timeoutAfterMillis) {
Process process = null;
// Note: don't use ReadToEndAsync because waiting on that task blocks forever.
try {
process = StartGitCommand("log", " -n 1 --pretty=format:%h");
var stdout = process.StandardOutput;
if (process.WaitForExit(timeoutAfterMillis)) {
return stdout.ReadToEnd();
} else {
// In a git repo with git lfs, git log can be blocked by waiting for switch branches / download lfs objects
// For that reason I disabled this warning log until a better solution is implemented (e.g. cache the commit and use cached if timeout).
// Log.Warning(
// $"[{CodePatcher.TAG}] Timed out trying to get the git commit hash, HotReload will not warn you about" +
// " a build connecting to a server running on a different commit (which is not supported)");
return null;
}
} catch (Win32Exception ex) {
if (ex.NativeErrorCode == 2) {
// git not found, ignore because user doesn't use git for version control
return null;
} else if (!loggedExceptionInGetShortCommitHashSafe) {
loggedExceptionInGetShortCommitHashSafe = true;
Debug.LogException(ex);
}
} catch (Exception ex) {
if (!loggedExceptionInGetShortCommitHashSafe) {
loggedExceptionInGetShortCommitHashSafe = true;
Log.Exception(ex);
}
} finally {
if (process != null) {
process.Dispose();
}
}
return null;
}
static Process StartGitCommand(string command, string arguments, Action<ProcessStartInfo> modifySettings = null) {
var startInfo = new ProcessStartInfo("git", command + " " + arguments) {
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
if (modifySettings != null) {
modifySettings(startInfo);
}
return Process.Start(startInfo);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f994bd5bb9f33f740ae37f8c79048a10
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 387b31d7da35b27428629a83bb4ac589
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using UnityEditor;
using System.Linq;
using System.Runtime.CompilerServices;
using SingularityGroup.HotReload.Editor.Localization;
using SingularityGroup.HotReload.Newtonsoft.Json;
using UnityEditor.Compilation;
[assembly: InternalsVisibleTo("SingularityGroup.HotReload.EditorTests")]
namespace SingularityGroup.HotReload.Editor {
internal static class AssemblyOmission {
// [MenuItem("Window/Hot Reload Dev/List omitted projects")]
private static void Check() {
Log.Info(Translations.Errors.InfoOmitProjectsForPlayerBuild);
var omitted = GetOmittedProjects(EditorUserBuildSettings.activeScriptCompilationDefines);
Log.Info(Translations.Errors.InfoSeparator);
foreach (var name in omitted) {
Log.Info(Translations.Errors.InfoOmittedEditorProject, name);
}
}
[JsonObject(MemberSerialization.Fields)]
private class AssemblyDefinitionJson {
public string name;
public string[] defineConstraints;
}
// scripts in Assets/ (with no asmdef) are always compiled into Assembly-CSharp
private static readonly string alwaysIncluded = "Assembly-CSharp";
private class Cache : AssetPostprocessor {
public static string[] ommitedProjects;
private static void OnPostprocessAllAssets(string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths) {
ommitedProjects = null;
}
}
// main thread only
public static string[] GetOmittedProjects(string allDefineSymbols, bool verboseLogs = false) {
if (Cache.ommitedProjects != null) {
return Cache.ommitedProjects;
}
var arr = allDefineSymbols.Split(';');
var omitted = GetOmittedProjects(arr, verboseLogs);
Cache.ommitedProjects = omitted;
return omitted;
}
// must be deterministic (return projects in same order each time)
private static string[] GetOmittedProjects(string[] allDefineSymbols, bool verboseLogs = false) {
// HotReload uses names of assemblies.
var editorAssemblies = GetEditorAssemblies();
editorAssemblies.Remove(alwaysIncluded);
var omittedByConstraint = DefineConstraints.GetOmittedAssemblies(allDefineSymbols);
editorAssemblies.AddRange(omittedByConstraint);
// Note: other platform player assemblies are also returned here, but I haven't seen it cause issues
// when using Hot Reload with IdleGame Android build.
var playerAssemblies = GetPlayerAssemblies().ToArray();
if (verboseLogs) {
foreach (var name in editorAssemblies) {
Log.Info(Translations.Errors.InfoFoundProjectNamed, name);
}
foreach (var playerAssemblyName in playerAssemblies) {
Log.Debug(string.Format(Translations.Utility.PlayerAssemblyDebug, playerAssemblyName));
}
}
// leaves the editor assemblies that are not built into player assemblies (e.g. editor and test assemblies)
var toOmit = editorAssemblies.Except(playerAssemblies.Select(asm => asm.name));
var unique = new HashSet<string>(toOmit);
return unique.OrderBy(s => s).ToArray();
}
// main thread only
public static List<string> GetEditorAssemblies() {
return CompilationPipeline
.GetAssemblies(AssembliesType.Editor)
.Select(asm => asm.name)
.ToList();
}
public static Assembly[] GetPlayerAssemblies() {
var playerAssemblyNames = CompilationPipeline
#if UNITY_2019_3_OR_NEWER
.GetAssemblies(AssembliesType.PlayerWithoutTestAssemblies) // since Unity 2019.3
#else
.GetAssemblies(AssembliesType.Player)
#endif
.ToArray();
return playerAssemblyNames;
}
internal static class DefineConstraints {
/// <summary>
/// When define constraints evaluate to false, we need
/// </summary>
/// <param name="defineSymbols"></param>
/// <returns></returns>
/// <remarks>
/// Not aware of a Unity api to read defineConstraints, so we do it ourselves.<br/>
/// Find any asmdef files where the define constraints evaluate to false.
/// </remarks>
public static string[] GetOmittedAssemblies(string[] defineSymbols) {
var guids = AssetDatabase.FindAssets("t:asmdef");
var asmdefFiles = guids.Select(AssetDatabase.GUIDToAssetPath);
var shouldOmit = new List<string>();
foreach (var asmdefFile in asmdefFiles) {
var asmdef = ReadDefineConstraints(asmdefFile);
if (asmdef == null) continue;
if (asmdef.defineConstraints == null || asmdef.defineConstraints.Length == 0) {
// Hot Reload already handles assemblies correctly if they have no define symbols.
continue;
}
var allPass = asmdef.defineConstraints.All(constraint => EvaluateDefineConstraint(constraint, defineSymbols));
if (!allPass) {
shouldOmit.Add(asmdef.name);
}
}
return shouldOmit.ToArray();
}
static AssemblyDefinitionJson ReadDefineConstraints(string path) {
try {
var json = File.ReadAllText(path);
var asmdef = JsonConvert.DeserializeObject<AssemblyDefinitionJson>(json);
return asmdef;
} catch (Exception) {
// ignore malformed asmdef
return null;
}
}
// Unity Define Constraints syntax is described in the docs https://docs.unity3d.com/Manual/class-AssemblyDefinitionImporter.html
static readonly Dictionary<string, string> syntaxMap = new Dictionary<string, string> {
{ "OR", "||" },
{ "AND", "&&" },
{ "NOT", "!" }
};
/// <summary>
/// Evaluate a define constraint like 'UNITY_ANDROID || UNITY_IOS'
/// </summary>
/// <param name="input"></param>
/// <param name="defineSymbols"></param>
/// <returns></returns>
public static bool EvaluateDefineConstraint(string input, string[] defineSymbols) {
// map Unity defineConstraints syntax to DataTable syntax (unity supports both)
foreach (var item in syntaxMap) {
// surround with space because || may not have spaces around it
input = input.Replace(item.Value, $" {item.Key} ");
}
// remove any extra spaces we just created
input = input.Replace(" ", " ");
var tokens = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var token in tokens) {
if (!syntaxMap.ContainsKey(token) && token != "false" && token != "true") {
var index = input.IndexOf(token, StringComparison.Ordinal);
// replace symbols with true or false depending if they are in the array or not.
input = input.Substring(0, index) + defineSymbols.Contains(token) + input.Substring(index + token.Length);
}
}
var dt = new DataTable();
return (bool)dt.Compute(input, "");
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0b94f2314a044b109de488be1ccd5640
timeCreated: 1674233674
@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor {
struct BuildInfoInput {
public readonly string allDefineSymbols;
public readonly BuildTarget activeBuildTarget;
public readonly string[] omittedProjects;
public readonly bool batchMode;
public readonly string locale;
public readonly bool disableTelemetry;
public BuildInfoInput(string allDefineSymbols, BuildTarget activeBuildTarget, string[] omittedProjects, bool batchMode, string locale, bool disableTelemetry) {
this.allDefineSymbols = allDefineSymbols;
this.activeBuildTarget = activeBuildTarget;
this.omittedProjects = omittedProjects;
this.batchMode = batchMode;
this.locale = locale;
this.disableTelemetry = disableTelemetry;
}
}
static class BuildInfoHelper {
public static async Task<BuildInfoInput> GetGenerateBuildInfoInput() {
var buildTarget = EditorUserBuildSettings.activeBuildTarget;
var activeDefineSymbols = EditorUserBuildSettings.activeScriptCompilationDefines;
var batchMode = Application.isBatchMode;
var disableTelemetry = HotReloadPrefs.DisableTelemetry;
var allDefineSymbols = await Task.Run(() => {
return GetAllAndroidMonoBuildDefineSymbolsThreaded(activeDefineSymbols);
});
// cached so unexpensive most of the time
var omittedProjects = AssemblyOmission.GetOmittedProjects(allDefineSymbols);
var locale = PackageConst.DefaultLocale;
return new BuildInfoInput(
allDefineSymbols: allDefineSymbols,
activeBuildTarget: buildTarget,
omittedProjects: omittedProjects,
batchMode: batchMode,
locale: locale,
disableTelemetry: disableTelemetry
);
}
public static BuildInfo GenerateBuildInfoMainThread() {
return GenerateBuildInfoMainThread(EditorUserBuildSettings.activeBuildTarget);
}
public static BuildInfo GenerateBuildInfoMainThread(BuildTarget buildTarget) {
var allDefineSymbols = GetAllAndroidMonoBuildDefineSymbolsThreaded(EditorUserBuildSettings.activeScriptCompilationDefines);
return GenerateBuildInfoThreaded(new BuildInfoInput(
allDefineSymbols: allDefineSymbols,
activeBuildTarget: buildTarget,
omittedProjects: AssemblyOmission.GetOmittedProjects(allDefineSymbols),
batchMode: Application.isBatchMode,
locale: PackageConst.DefaultLocale,
disableTelemetry: HotReloadPrefs.DisableTelemetry
));
}
public static BuildInfo GenerateBuildInfoThreaded(BuildInfoInput input) {
var omittedProjectRegex = String.Join("|", input.omittedProjects.Select(name => Regex.Escape(name)));
var shortCommitHash = GitUtil.GetShortCommitHashOrFallback();
var hostname = IsHumanControllingUs(input.batchMode) ? IpHelper.GetIpAddress() : null;
// Note: add a string to uniquely identify the Unity project. Could use filepath to /MyProject/Assets/ (editor Application.dataPath)
// or application identifier (com.company.appname).
// Do this when supporting multiple projects: SG-28807
// The matching code is in Runtime assembly which compares server response with built BuildInfo.
return new BuildInfo {
projectIdentifier = "SG-29580",
commitHash = shortCommitHash,
defineSymbols = input.allDefineSymbols,
projectOmissionRegex = omittedProjectRegex,
buildMachineHostName = hostname,
buildMachinePort = RequestHelper.port,
activeBuildTarget = input.activeBuildTarget.ToString(),
buildMachineRequestOrigin = RequestHelper.origin,
locale = input.locale,
disableTelemetry = input.disableTelemetry,
};
}
public static bool IsHumanControllingUs(bool batchMode) {
if (batchMode) {
return false;
}
var isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
return !isCI;
}
private static readonly string[] editorSymbolsToRemove = {
"PLATFORM_ARCH_64",
"UNITY_64",
"UNITY_INCLUDE_TESTS",
"UNITY_EDITOR",
"UNITY_EDITOR_64",
"UNITY_EDITOR_WIN",
"ENABLE_UNITY_COLLECTIONS_CHECKS",
"ENABLE_BURST_AOT",
"RENDER_SOFTWARE_CURSOR",
"PLATFORM_STANDALONE_WIN",
"PLATFORM_STANDALONE",
"UNITY_STANDALONE_WIN",
"UNITY_STANDALONE",
"ENABLE_MOVIES",
"ENABLE_OUT_OF_PROCESS_CRASH_HANDLER",
"ENABLE_WEBSOCKET_HOST",
"ENABLE_CLUSTER_SYNC",
"ENABLE_CLUSTERINPUT",
};
private static readonly string[] androidSymbolsToAdd = {
"CSHARP_7_OR_LATER",
"CSHARP_7_3_OR_NEWER",
"PLATFORM_ANDROID",
"UNITY_ANDROID",
"UNITY_ANDROID_API",
"ENABLE_EGL",
"DEVELOPMENT_BUILD",
"ENABLE_CLOUD_SERVICES_NATIVE_CRASH_REPORTING",
"PLATFORM_SUPPORTS_ADS_ID",
"UNITY_CAN_SHOW_SPLASH_SCREEN",
"UNITY_HAS_GOOGLEVR",
"UNITY_HAS_TANGO",
"ENABLE_SPATIALTRACKING",
"ENABLE_RUNTIME_PERMISSIONS",
"ENABLE_ENGINE_CODE_STRIPPING",
"UNITY_ASTC_ONLY_DECOMPRESS",
"ANDROID_USE_SWAPPY",
"ENABLE_ONSCREEN_KEYBOARD",
"ENABLE_UNITYADS_RUNTIME",
"UNITY_UNITYADS_API",
};
// Currently there is no better way. Alternatively we could hook into unity's call to csc.exe and parse the /define: arguments.
// Hardcoding the differences was less effort and is less error prone.
// I also looked into it and tried all the Build interfaces like this one https://docs.unity3d.com/ScriptReference/Build.IPostBuildPlayerScriptDLLs.html
// and logging EditorUserBuildSettings.activeScriptCompilationDefines in the callbacks - result: all same like editor, so I agree that hardcode is best.
private static string GetAllAndroidMonoBuildDefineSymbolsThreaded(string[] defineSymbols) {
var defines = new HashSet<string>(defineSymbols);
defines.ExceptWith(editorSymbolsToRemove);
defines.UnionWith(androidSymbolsToAdd);
// sort for consistency, must be deterministic
var definesArray = defines.OrderBy(def => def).ToArray();
return String.Join(";", definesArray);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f41ad09ae4f04088bf6c9ad9a4fc0885
timeCreated: 1674220023
@@ -0,0 +1,106 @@
using System;
using System.Text.RegularExpressions;
using UnityEngine;
using System.Threading.Tasks;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
using SingularityGroup.HotReload.Editor.Localization;
namespace SingularityGroup.HotReload.Editor {
internal static class EditorWindowHelper {
#if UNITY_2020_1_OR_NEWER
public static bool supportsNotifications = true;
#else
public static bool supportsNotifications = false;
#endif
private static readonly Regex ValidEmailRegex = new Regex(@"^(?!\.)(""([^""\r\\]|\\[""\r\\])*""|"
+ @"([-a-z0-9!#$%&'*+/=?^_`{|}~]|(?<!\.)\.)*)(?<!\.)"
+ @"@[a-z0-9][\w\.-]*[a-z0-9]\.[a-z][a-z\.]*[a-z]$", RegexOptions.IgnoreCase);
public static bool IsValidEmailAddress(string email) {
return ValidEmailRegex.IsMatch(email);
}
public static bool IsHumanControllingUs() {
if (Application.isBatchMode) {
// allow for running tests
if (MultiplayerPlaymodeHelper.HasCommandLineArgument(Environment.GetCommandLineArgs(), "-runTests")) {
return true;
}
return false;
}
var isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
return !isCI;
}
internal enum NotificationStatus {
None,
Patching,
NeedsRecompile
}
private static Dictionary<NotificationStatus, GUIContent> notificationContent => new Dictionary<NotificationStatus, GUIContent> {
{ NotificationStatus.Patching, new GUIContent(Translations.Miscellaneous.NotificationPatching)},
{ NotificationStatus.NeedsRecompile, new GUIContent(Translations.Miscellaneous.NotificationNeedsRecompile)},
};
static Type gameViewT;
private static EditorWindow[] gameViewWindows {
get {
gameViewT = gameViewT ?? typeof(EditorWindow).Assembly.GetType("UnityEditor.GameView");
return Resources.FindObjectsOfTypeAll(gameViewT).Cast<EditorWindow>().ToArray();
}
}
private static EditorWindow[] sceneWindows {
get {
return Resources.FindObjectsOfTypeAll(typeof(SceneView)).Cast<EditorWindow>().ToArray();
}
}
private static EditorWindow[] notificationWindows {
get {
return gameViewWindows.Concat(sceneWindows).ToArray();
}
}
static NotificationStatus lastNotificationStatus;
private static DateTime? latestNotificationStartedAt;
private static bool notificationShownRecently => latestNotificationStartedAt != null && DateTime.UtcNow - latestNotificationStartedAt < TimeSpan.FromSeconds(1);
internal static void ShowNotification(NotificationStatus notificationType, float maxDuration = 3) {
// Patch status goes from Unsupported changes to patching rapidly when making unsupported change
// patching also shows right before unsupported changes sometimes
// so we don't override NeedsRecompile notification ever
bool willOverrideNeedsCompileNotification = notificationType != NotificationStatus.NeedsRecompile && notificationShownRecently || lastNotificationStatus == NotificationStatus.NeedsRecompile && notificationShownRecently;
if (!supportsNotifications || willOverrideNeedsCompileNotification) {
return;
}
foreach (EditorWindow notificationWindow in notificationWindows) {
notificationWindow.ShowNotification(notificationContent[notificationType], maxDuration);
notificationWindow.Repaint();
}
latestNotificationStartedAt = DateTime.UtcNow;
lastNotificationStatus = notificationType;
}
internal static void RemoveNotification() {
if (!supportsNotifications) {
return;
}
// only patching notifications should be removed after showing less than 1 second
if (notificationShownRecently && lastNotificationStatus != NotificationStatus.Patching) {
return;
}
foreach (EditorWindow notificationWindow in notificationWindows) {
notificationWindow.RemoveNotification();
notificationWindow.Repaint();
}
latestNotificationStartedAt = null;
lastNotificationStatus = NotificationStatus.None;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fd463b1f0bfddf34caa662ebe375e5fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,177 @@
using System;
using UnityEngine;
using System.Collections.Generic;
namespace SingularityGroup.HotReload.Editor {
internal enum InvertibleIcon {
BugReport,
Events,
EventsNew,
Recompile,
Logo,
LogoNew,
Close,
FoldoutOpen,
FoldoutClosed,
Spinner,
Stop,
Start,
}
internal static class GUIHelper {
private static readonly Dictionary<InvertibleIcon, string> supportedInvertibleIcons = new Dictionary<InvertibleIcon, string> {
{ InvertibleIcon.BugReport, "Hot_Reload_report_bug" },
{ InvertibleIcon.Events, "Hot_Reload_events" },
{ InvertibleIcon.Recompile, "Hot_Reload_refresh" },
{ InvertibleIcon.Logo, "Hot_Reload_logo" },
{ InvertibleIcon.Close, "Hot_Reload_close" },
{ InvertibleIcon.FoldoutOpen, "Hot_Reload_foldout_open" },
{ InvertibleIcon.FoldoutClosed, "Hot_Reload_foldout_closed" },
{ InvertibleIcon.Spinner, "Hot_Reload_icon_loading_star_light_mode_96" },
{ InvertibleIcon.Stop, "Hot_Reload_Icn_Stop" },
{ InvertibleIcon.Start, "Hot_Reload_Icn_play" },
};
private static readonly Dictionary<InvertibleIcon, Texture2D> invertibleIconCache = new Dictionary<InvertibleIcon, Texture2D>();
private static readonly Dictionary<InvertibleIcon, Texture2D> invertibleIconInvertedCache = new Dictionary<InvertibleIcon, Texture2D>();
private static readonly Dictionary<string, Texture2D> iconCache = new Dictionary<string, Texture2D>();
internal static Texture2D InvertTextureColor(Texture2D originalTexture) {
if (!originalTexture) {
return originalTexture;
}
// Get the original pixels from the texture
Color[] originalPixels = originalTexture.GetPixels();
// Create a new array for the inverted colors
Color[] invertedPixels = new Color[originalPixels.Length];
// Iterate through the pixels and invert the colors while preserving the alpha channel
for (int i = 0; i < originalPixels.Length; i++) {
Color originalColor = originalPixels[i];
Color invertedColor = new Color(1 - originalColor.r, 1 - originalColor.g, 1 - originalColor.b, originalColor.a);
invertedPixels[i] = invertedColor;
}
// Create a new texture and set its pixels
Texture2D invertedTexture = new Texture2D(originalTexture.width, originalTexture.height);
invertedTexture.SetPixels(invertedPixels);
// Apply the changes to the texture
invertedTexture.Apply();
return invertedTexture;
}
internal static Texture2D GetInvertibleIcon(InvertibleIcon invertibleIcon) {
Texture2D iconTexture;
var cache = HotReloadWindowStyles.IsDarkMode ? invertibleIconInvertedCache : invertibleIconCache;
if (!cache.TryGetValue(invertibleIcon, out iconTexture) || !iconTexture) {
var type = invertibleIcon;
if (invertibleIcon == InvertibleIcon.EventsNew) {
type = InvertibleIcon.Events;
} else if (invertibleIcon == InvertibleIcon.LogoNew) {
type = InvertibleIcon.Logo;
}
iconTexture = Resources.Load<Texture2D>(supportedInvertibleIcons[type]);
// we assume icons are for light mode by default
// therefore if its dark mode we should invert them
if (HotReloadWindowStyles.IsDarkMode) {
iconTexture = InvertTextureColor(iconTexture);
// Prevent Unity from destroying this during play mode exit
if (iconTexture) {
iconTexture.hideFlags = HideFlags.HideAndDontSave;
}
}
cache[type] = iconTexture;
// we combine dot image with Events icon to create a new alert version
if (invertibleIcon == InvertibleIcon.LogoNew || invertibleIcon == InvertibleIcon.EventsNew) {
var redDot = Resources.Load<Texture2D>("Hot_Reload_red_dot");
iconTexture = CombineImages(iconTexture, redDot);
// Same here — this texture is programmatically created
if (iconTexture) {
iconTexture.hideFlags = HideFlags.HideAndDontSave;
}
cache[invertibleIcon] = iconTexture;
}
}
return cache[invertibleIcon];
}
internal static Texture2D GetLocalIcon(string iconName) {
Texture2D iconTexture;
if (!iconCache.TryGetValue(iconName, out iconTexture) || !iconTexture) {
iconTexture = Resources.Load<Texture2D>(iconName);
iconCache[iconName] = iconTexture;
}
return iconTexture;
}
static Texture2D CombineImages(Texture2D image1, Texture2D image2) {
if (!image1 || !image2) {
return image1;
}
var combinedImage = new Texture2D(Mathf.Max(image1.width, image2.width), Mathf.Max(image1.height, image2.height));
for (int y = 0; y < combinedImage.height; y++) {
for (int x = 0; x < combinedImage.width; x++) {
Color color1 = x < image1.width && y < image1.height ? image1.GetPixel(x, y) : Color.clear;
Color color2 = x < image2.width && y < image2.height ? image2.GetPixel(x, y) : Color.clear;
combinedImage.SetPixel(x, y, Color.Lerp(color1, color2, color2.a));
}
}
combinedImage.Apply();
return combinedImage;
}
private static readonly Dictionary<Color, Texture2D> textureColorCache = new Dictionary<Color, Texture2D>();
internal static Texture2D ConvertTextureToColor(Color color) {
Texture2D texture;
if (!textureColorCache.TryGetValue(color, out texture) || !texture) {
texture = new Texture2D(1, 1);
texture.SetPixel(0, 0, color);
texture.Apply();
textureColorCache[color] = texture;
}
return texture;
}
private static readonly Dictionary<string, Texture2D> grayTextureCache = new Dictionary<string, Texture2D>();
private static readonly Dictionary<string, Color> colorFactor = new Dictionary<string, Color> {
{ "Hot_Reload_error", new Color(0.6f, 0.587f, 0.114f) },
};
internal static Texture2D ConvertToGrayscale(string localIcon) {
Texture2D _texture;
if (!grayTextureCache.TryGetValue(localIcon, out _texture) || !_texture) {
var icon = GUIHelper.GetLocalIcon(localIcon);
// Create a copy of the texture
Texture2D copiedTexture = new Texture2D(icon.width, icon.height, TextureFormat.RGBA32, false);
// Convert the copied texture to grayscale
Color[] pixels = icon.GetPixels();
for (int i = 0; i < pixels.Length; i++) {
Color pixel = pixels[i];
Color factor;
if (!colorFactor.TryGetValue(localIcon, out factor)) {
factor = new Color(0.299f, 0.587f, 0.114f);
}
float grayscale = factor.r * pixel.r + factor.g * pixel.g + factor.b * pixel.b;
pixels[i] = new Color(grayscale, grayscale, grayscale, pixel.a); // Preserve alpha channel
}
copiedTexture.SetPixels(pixels);
copiedTexture.Apply();
// Store the grayscale texture in the cache
grayTextureCache[localIcon] = copiedTexture;
return copiedTexture;
}
return _texture;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b4be912211814333ab61898b6440dc8e
timeCreated: 1694518358
@@ -0,0 +1,602 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Localization;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;
using Translations = SingularityGroup.HotReload.Editor.Localization.Translations;
namespace SingularityGroup.HotReload.Editor {
internal static class HotReloadSuggestionsHelper {
internal static void SetSuggestionsShown(HotReloadSuggestionKind hotReloadSuggestionKind) {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
if (EditorPrefs.GetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}")) {
return;
}
EditorPrefs.SetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}", true);
EditorPrefs.SetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}", true);
AlertEntry entry;
if (suggestionMap.TryGetValue(hotReloadSuggestionKind, out entry) && !HotReloadTimelineHelper.Suggestions.Contains(entry)) {
HotReloadTimelineHelper.Suggestions.Insert(0, entry);
HotReloadState.ShowingRedDot = true;
}
}
internal static bool CheckSuggestionActive(HotReloadSuggestionKind hotReloadSuggestionKind) {
return EditorPrefs.GetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}");
}
internal static bool CheckSuggestionShown(HotReloadSuggestionKind hotReloadSuggestionKind) {
return EditorPrefs.GetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}");
}
internal static bool CanShowServerSuggestion(HotReloadSuggestionKind hotReloadSuggestionKind) {
if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerWithSideEffects) {
return !HotReloadState.ShowedFieldInitializerWithSideEffects;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited) {
return !HotReloadState.ShowedFieldInitializerExistingInstancesEdited;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited) {
return !HotReloadState.ShowedFieldInitializerExistingInstancesUnedited;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.AddMonobehaviourMethod) {
return !HotReloadState.ShowedAddMonobehaviourMethods;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.DetailedErrorReportingIsEnabled) {
return !CheckSuggestionShown(HotReloadSuggestionKind.DetailedErrorReportingIsEnabled);
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.UTF8EncodingRequired) {
return true;
}
return false;
}
internal static void SetServerSuggestionShown(HotReloadSuggestionKind hotReloadSuggestionKind) {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
if (hotReloadSuggestionKind == HotReloadSuggestionKind.DetailedErrorReportingIsEnabled) {
HotReloadSuggestionsHelper.SetSuggestionsShown(hotReloadSuggestionKind);
return;
}
if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerWithSideEffects) {
HotReloadState.ShowedFieldInitializerWithSideEffects = true;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited) {
HotReloadState.ShowedFieldInitializerExistingInstancesEdited = true;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited) {
HotReloadState.ShowedFieldInitializerExistingInstancesUnedited = true;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.AddMonobehaviourMethod) {
HotReloadState.ShowedAddMonobehaviourMethods = true;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.UTF8EncodingRequired) {
// Allow showing it multiple times
} else {
return;
}
HotReloadSuggestionsHelper.SetSuggestionActive(hotReloadSuggestionKind);
}
// used for cases where suggestion might need to be shown more than once
internal static void SetSuggestionActive(HotReloadSuggestionKind hotReloadSuggestionKind) {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
if (EditorPrefs.GetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}")) {
return;
}
EditorPrefs.SetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}", true);
AlertEntry entry;
if (suggestionMap.TryGetValue(hotReloadSuggestionKind, out entry) && !HotReloadTimelineHelper.Suggestions.Contains(entry)) {
HotReloadTimelineHelper.Suggestions.Insert(0, entry);
HotReloadState.ShowingRedDot = true;
}
}
internal static void SetSuggestionInactive(HotReloadSuggestionKind hotReloadSuggestionKind) {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
EditorPrefs.SetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}", false);
AlertEntry entry;
if (suggestionMap.TryGetValue(hotReloadSuggestionKind, out entry)) {
HotReloadTimelineHelper.Suggestions.Remove(entry);
}
}
private static void InitSuggestions() {
foreach (HotReloadSuggestionKind value in Enum.GetValues(typeof(HotReloadSuggestionKind))) {
if (!CheckSuggestionActive(value)) {
continue;
}
AlertEntry entry;
if (suggestionMap.TryGetValue(value, out entry) && !HotReloadTimelineHelper.Suggestions.Contains(entry)) {
HotReloadTimelineHelper.Suggestions.Insert(0, entry);
}
}
}
internal static HotReloadSuggestionKind? FindSuggestionKind(AlertEntry targetEntry) {
foreach (KeyValuePair<HotReloadSuggestionKind, AlertEntry> pair in suggestionMap) {
if (pair.Value.Equals(targetEntry)) {
return pair.Key;
}
}
return null;
}
internal static readonly OpenURLButton recompileTroubleshootingButton = new OpenURLButton(Translations.Suggestions.ButtonDocs, Constants.RecompileTroubleshootingURL);
internal static readonly OpenURLButton featuresDocumentationButton = new OpenURLButton(Translations.Suggestions.ButtonDocs, Constants.FeaturesDocumentationURL);
internal static readonly OpenURLButton multipleEditorsDocumentationButton = new OpenURLButton(Translations.Suggestions.ButtonDocs, Constants.MultipleEditorsURL);
internal static readonly OpenURLButton debuggerDocumentationButton = new OpenURLButton(Translations.Suggestions.ButtonMoreInfo, Constants.DebuggerURL);
internal static readonly OpenURLButton securityDocumentationButton = new OpenURLButton(Translations.Suggestions.ButtonMoreInfo, Constants.SecurityURL);
public static Dictionary<HotReloadSuggestionKind, AlertEntry> suggestionMap = new Dictionary<HotReloadSuggestionKind, AlertEntry> {
{ HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.Award2023Title,
Translations.Suggestions.Award2023Message,
actionData: () => {
GUILayout.Space(6f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonVote)) {
Application.OpenURL(Constants.VoteForAwardURL);
SetSuggestionInactive(HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.TelemetryCollection, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.TelemetryCollectionTitle,
Translations.Suggestions.TelemetryCollectionMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonOK)) {
SetSuggestionInactive(HotReloadSuggestionKind.TelemetryCollection);
}
securityDocumentationButton.OnGUI();
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.UnsupportedChanges, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.UnsupportedChangesTitle,
Translations.Suggestions.UnsupportedChangesMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
featuresDocumentationButton.OnGUI();
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.UnsupportedPackages, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.UnsupportedPackagesTitle,
Translations.Suggestions.UnsupportedPackagesMessage,
iconType: AlertType.UnsupportedChange,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
HotReloadAboutTab.contactButton.OnGUI();
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.AutoRecompiledPlaymodeTitle,
Translations.Suggestions.AutoRecompiledPlaymodeMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
recompileTroubleshootingButton.OnGUI();
GUILayout.Space(5f);
HotReloadAboutTab.discordButton.OnGUI();
GUILayout.Space(5f);
HotReloadAboutTab.contactButton.OnGUI();
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
#if UNITY_2022_1_OR_NEWER
{ HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.AutoRecompiled2022Title,
Translations.Suggestions.AutoRecompiled2022Message,
iconType: AlertType.UnsupportedChange,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonUseBuildTimeOnlyAtlas)) {
if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2) {
EditorSettings.spritePackerMode = SpritePackerMode.SpriteAtlasV2Build;
} else {
EditorSettings.spritePackerMode = SpritePackerMode.BuildTimeOnlyAtlas;
}
}
if (GUILayout.Button(Translations.Suggestions.ButtonOpenSettings)) {
SettingsService.OpenProjectSettings("Project/Editor");
}
if (GUILayout.Button(Translations.Suggestions.ButtonIgnoreSuggestion)) {
SetSuggestionInactive(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
hasExitButton: false
)},
#endif
{ HotReloadSuggestionKind.MultidimensionalArrays, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.MultidimensionalArraysTitle,
Translations.Suggestions.MultidimensionalArraysMessage,
iconType: AlertType.UnsupportedChange,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonLearnMore)) {
string url;
if (PackageConst.DefaultLocaleField == Locale.SimplifiedChinese) {
url = "https://learn.microsoft.com/zh-cn/dotnet/fundamentals/code-analysis/quality-rules/ca1814";
} else {
url = "https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1814";
}
Application.OpenURL(url);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.EditorsWithoutHRRunning, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.EditorsWithoutHRTitle,
Translations.Suggestions.EditorsWithoutHRMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonStopHotReload)) {
EditorCodePatcher.StopCodePatcher().Forget();
}
GUILayout.Space(5f);
multipleEditorsDocumentationButton.OnGUI();
GUILayout.Space(5f);
if (GUILayout.Button(Translations.Suggestions.ButtonDontShowAgain)) {
HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.EditorsWithoutHRRunning);
HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
}
GUILayout.FlexibleSpace();
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.UnsupportedChange
)},
// Not in use (never reported from the server)
{ HotReloadSuggestionKind.FieldInitializerWithSideEffects, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.FieldInitializerSideEffectsTitle,
Translations.Suggestions.FieldInitializerSideEffectsMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonOK)) {
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(Translations.Suggestions.ButtonDontShowAgain)) {
SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
}
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.DetailedErrorReportingIsEnabled, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.DetailedErrorReportingTitle,
Translations.Suggestions.DetailedErrorReportingMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
GUILayout.Space(4f);
if (GUILayout.Button(Translations.Suggestions.ButtonOKPadded)) {
SetSuggestionInactive(HotReloadSuggestionKind.DetailedErrorReportingIsEnabled);
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(Translations.Suggestions.ButtonDisable)) {
HotReloadSettingsTab.DisableDetailedErrorReportingInner(true);
SetSuggestionInactive(HotReloadSuggestionKind.DetailedErrorReportingIsEnabled);
}
GUILayout.Space(10f);
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
// Not in use (never reported from the server)
{ HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.FieldInitializerEditedTitle,
Translations.Suggestions.FieldInitializerEditedMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonTurnOff)) {
#pragma warning disable CS0618
HotReloadSettingsTab.ApplyApplyFieldInitializerEditsToExistingClassInstances(false);
#pragma warning restore CS0618
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
}
if (GUILayout.Button(Translations.Suggestions.ButtonOpenSettings)) {
HotReloadWindow.Current.SelectTab(typeof(HotReloadSettingsTab));
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(Translations.Suggestions.ButtonDontShowAgain)) {
SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
}
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.FieldInitializerUneditedTitle,
Translations.Suggestions.FieldInitializerUneditedMessage,
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonOK)) {
SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited);
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.AddMonobehaviourMethod, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.AddMonobehaviourMethodTitle,
Translations.Suggestions.AddMonobehaviourMethodMessage,
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonOK)) {
SetSuggestionInactive(HotReloadSuggestionKind.AddMonobehaviourMethod);
}
if (GUILayout.Button(Translations.Suggestions.ButtonAutoRecompile)) {
SetSuggestionInactive(HotReloadSuggestionKind.AddMonobehaviourMethod);
HotReloadPrefs.AutoRecompilePartiallyUnsupportedChanges = true;
HotReloadPrefs.DisplayNewMonobehaviourMethodsAsPartiallySupported = true;
HotReloadRunTab.RecompileWithChecks();
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(Translations.Suggestions.ButtonDontShowAgain)) {
SetSuggestionsShown(HotReloadSuggestionKind.AddMonobehaviourMethod);
SetSuggestionInactive(HotReloadSuggestionKind.AddMonobehaviourMethod);
}
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
#if UNITY_2020_1_OR_NEWER
{ HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.SwitchToDebugModeTitle,
Translations.Suggestions.SwitchToDebugModeMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonSwitchToDebugMode) && HotReloadRunTab.ConfirmExitPlaymode(Translations.Suggestions.SwitchToDebugModeConfirmation)) {
HotReloadRunTab.SwitchToDebugMode();
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.UnsupportedChange
)},
#endif
{ HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.DebuggerAttachedTitle,
Translations.Suggestions.DebuggerAttachedMessagePaused,
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonKeepEnabledDuringDebugging)) {
SetSuggestionInactive(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
HotReloadPrefs.AutoDisableHotReloadWithDebugger = false;
}
GUILayout.FlexibleSpace();
debuggerDocumentationButton.OnGUI();
if (GUILayout.Button(Translations.Suggestions.ButtonDontShowAgain)) {
SetSuggestionsShown(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
SetSuggestionInactive(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
}
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.DebuggerMethodsTitle,
Translations.Suggestions.DebuggerMethodsMessage,
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonRecompile)) {
SetSuggestionInactive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached);
if (HotReloadRunTab.ConfirmExitPlaymode(Translations.Suggestions.DebuggerMethodsConfirmation)) {
HotReloadRunTab.Recompile();
}
}
GUILayout.FlexibleSpace();
debuggerDocumentationButton.OnGUI();
GUILayout.Space(8f);
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.UnsupportedChange,
hasExitButton: false
)},
{ HotReloadSuggestionKind.UTF8EncodingRequired, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.UTF8EncodingRequiredTitle,
Translations.Suggestions.UTF8EncodingRequiredMessage,
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonOK)) {
SetSuggestionInactive(HotReloadSuggestionKind.UTF8EncodingRequired);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.UnsupportedChange,
hasExitButton: false
)},
};
static ListRequest listRequest;
static string[] unsupportedPackages = new[] {
"com.unity.entities",
"com.firstgeargames.fishnet",
};
static List<string> unsupportedPackagesList;
static DateTime lastPlaymodeChange;
public static void Init() {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
listRequest = Client.List(offlineMode: false, includeIndirectDependencies: true);
EditorApplication.playModeStateChanged += state => {
lastPlaymodeChange = DateTime.UtcNow;
};
CompilationPipeline.compilationStarted += obj => {
if (DateTime.UtcNow - lastPlaymodeChange < TimeSpan.FromSeconds(1) && !HotReloadState.RecompiledUnsupportedChangesOnExitPlaymode) {
#if UNITY_2022_1_OR_NEWER
SetSuggestionsShown(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
#else
SetSuggestionsShown(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges);
#endif
}
HotReloadState.RecompiledUnsupportedChangesOnExitPlaymode = false;
};
InitSuggestions();
}
private static DateTime lastCheckedUnityInstances = DateTime.UtcNow;
public static void Check() {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
if (listRequest.IsCompleted &&
unsupportedPackagesList == null)
{
unsupportedPackagesList = new List<string>();
if (listRequest.Result != null) {
foreach (var packageInfo in listRequest.Result) {
if (unsupportedPackages.Contains(packageInfo.name)) {
unsupportedPackagesList.Add(packageInfo.name);
}
}
}
if (unsupportedPackagesList.Count > 0) {
SetSuggestionsShown(HotReloadSuggestionKind.UnsupportedPackages);
}
}
CheckEditorsWithoutHR();
#if UNITY_2022_1_OR_NEWER
if (EditorSettings.spritePackerMode == SpritePackerMode.AlwaysOnAtlas || EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2) {
SetSuggestionsShown(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
} else if (CheckSuggestionActive(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022)) {
SetSuggestionInactive(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
EditorPrefs.SetBool($"HotReloadWindow.SuggestionsShown.{HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022}", false);
}
#endif
}
private static void CheckEditorsWithoutHR() {
if (!ServerHealthCheck.I.IsServerHealthy) {
HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
return;
}
if (checkingEditorsWihtoutHR ||
(DateTime.UtcNow - lastCheckedUnityInstances).TotalSeconds < 5)
{
return;
}
CheckEditorsWithoutHRAsync().Forget();
}
static bool checkingEditorsWihtoutHR;
private static async Task CheckEditorsWithoutHRAsync() {
try {
checkingEditorsWihtoutHR = true;
var editorsWithoutHr = await RequestHelper.RequestEditorsWithoutHRRunning();
if (editorsWithoutHr == null) {
return;
}
var showSuggestion = editorsWithoutHr.editorsWithoutHRRunning;
if (!showSuggestion) {
HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
return;
}
if (!HotReloadState.ShowedEditorsWithoutHR && ServerHealthCheck.I.IsServerHealthy) {
HotReloadSuggestionsHelper.SetSuggestionActive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
}
} finally {
checkingEditorsWihtoutHR = false;
lastCheckedUnityInstances = DateTime.UtcNow;
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9cc471e812b143599ef5dde1d7ec022a
timeCreated: 1694632601
@@ -0,0 +1,640 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using JetBrains.Annotations;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Localization;
using SingularityGroup.HotReload.Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
using Translations = SingularityGroup.HotReload.Editor.Localization.Translations;
namespace SingularityGroup.HotReload.Editor {
internal enum TimelineType {
Suggestions,
Timeline,
}
internal enum AlertType {
Suggestion,
UnsupportedChange,
CompileError,
PartiallySupportedChange,
AppliedChange,
UndetectedChange,
}
internal enum AlertEntryType {
Error,
Failure,
InlinedMethod,
PatchApplied,
PartiallySupportedChange,
UndetectedChange,
}
internal enum EntryType {
Parent,
Child,
Standalone,
Foldout,
}
internal class PersistedAlertData {
public readonly AlertData[] alertDatas;
public PersistedAlertData(AlertData[] alertDatas) {
this.alertDatas = alertDatas;
}
}
internal class AlertData {
public readonly AlertEntryType alertEntryType;
public readonly string errorString;
public readonly string methodName;
public readonly string methodSimpleName;
public readonly PartiallySupportedChange partiallySupportedChange;
public readonly EntryType entryType;
public readonly bool detiled;
public readonly DateTime createdAt;
public readonly string[] patchedMembersDisplayNames;
public readonly bool isCompile;
public AlertData(AlertEntryType alertEntryType, DateTime createdAt, bool detiled = false, EntryType entryType = EntryType.Standalone, string errorString = null, string methodName = null, string methodSimpleName = null, PartiallySupportedChange partiallySupportedChange = default(PartiallySupportedChange), string[] patchedMembersDisplayNames = null, bool isCompile = false) {
this.alertEntryType = alertEntryType;
this.createdAt = createdAt;
this.detiled = detiled;
this.entryType = entryType;
this.errorString = errorString;
this.methodName = methodName;
this.methodSimpleName = methodSimpleName;
this.partiallySupportedChange = partiallySupportedChange;
this.patchedMembersDisplayNames = patchedMembersDisplayNames;
this.isCompile = isCompile;
}
}
internal class AlertEntry {
internal readonly AlertType alertType;
internal readonly string title;
internal readonly DateTime timestamp;
internal readonly string description;
[CanBeNull] internal readonly Action actionData;
internal readonly AlertType iconType;
internal readonly string shortDescription;
internal readonly EntryType entryType;
internal readonly AlertData alertData;
internal readonly bool hasExitButton;
internal AlertEntry(AlertType alertType, string title, string description, DateTime timestamp, string shortDescription = null, Action actionData = null, AlertType? iconType = null, EntryType entryType = EntryType.Standalone, AlertData alertData = default(AlertData), bool hasExitButton = true) {
this.alertType = alertType;
this.title = title;
this.description = description;
this.shortDescription = shortDescription;
this.actionData = actionData;
this.iconType = iconType ?? alertType;
this.timestamp = timestamp;
this.entryType = entryType;
this.alertData = alertData;
this.hasExitButton = hasExitButton;
}
}
internal static class HotReloadTimelineHelper {
internal const int maxVisibleEntries = 40;
private static List<AlertEntry> eventsTimeline = new List<AlertEntry>();
internal static List<AlertEntry> EventsTimeline => eventsTimeline;
static readonly string filePath = Path.Combine(PackageConst.LibraryCachePath, "eventEntries.json");
public static async Task InitPersistedEvents() {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
if (!File.Exists(filePath)) {
return;
}
var redDotShown = HotReloadState.ShowingRedDot;
try {
var persistedAlertData = await Task.Run(() => JsonConvert.DeserializeObject<PersistedAlertData>(File.ReadAllText(filePath)));
eventsTimeline = new List<AlertEntry>(persistedAlertData.alertDatas.Length);
for (int i = persistedAlertData.alertDatas.Length - 1; i >= 0; i--) {
AlertData alertData = persistedAlertData.alertDatas[i];
switch (alertData.alertEntryType) {
case AlertEntryType.Error:
CreateErrorEventEntry(errorString: alertData.errorString, entryType: alertData.entryType, createdAt: alertData.createdAt);
break;
#if UNITY_2020_1_OR_NEWER
case AlertEntryType.InlinedMethod:
CreateInlinedMethodsEntry(alertData.patchedMembersDisplayNames, alertData.entryType, alertData.createdAt);
break;
#endif
case AlertEntryType.Failure:
if (alertData.entryType == EntryType.Parent) {
CreateReloadFinishedWithWarningsEventEntry(createdAt: alertData.createdAt, patchedMembersDisplayNames: alertData.patchedMembersDisplayNames);
} else {
CreatePatchFailureEventEntry(errorString: alertData.errorString, methodName: alertData.methodName, methodSimpleName: alertData.methodSimpleName, entryType: alertData.entryType, createdAt: alertData.createdAt);
}
break;
case AlertEntryType.PatchApplied:
CreateReloadFinishedEventEntry(
createdAt: alertData.createdAt,
patchedMethodsDisplayNames: alertData.patchedMembersDisplayNames,
isCompile: alertData.isCompile
);
break;
case AlertEntryType.PartiallySupportedChange:
if (alertData.entryType == EntryType.Parent) {
CreateReloadPartiallyAppliedEventEntry(createdAt: alertData.createdAt, patchedMethodsDisplayNames: alertData.patchedMembersDisplayNames);
} else {
CreatePartiallyAppliedEventEntry(alertData.partiallySupportedChange, entryType: alertData.entryType, detailed: alertData.detiled, createdAt: alertData.createdAt);
}
break;
case AlertEntryType.UndetectedChange:
CreateReloadUndetectedChangeEventEntry(createdAt: alertData.createdAt);
break;
}
}
} catch (Exception e) {
Log.Warning(Translations.Errors.WarningInitializingEventEntries, e);
} finally {
// Ensure red dot is not triggered for existing entries
HotReloadState.ShowingRedDot = redDotShown;
}
}
internal static async Task PersistTimeline() {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
var persistedData = new PersistedAlertData(eventsTimeline.Where(x => x.alertType != AlertType.CompileError).Select(x => x.alertData).ToArray());
try {
await Task.Run(() => File.WriteAllText(path: filePath, contents: JsonConvert.SerializeObject(persistedData)));
} catch (Exception e) {
Log.Warning(Translations.Errors.WarningPersistingEventEntries, e);
}
}
internal static void ClearPersistance() {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
Task.Run(() => File.Delete(filePath));
eventsTimeline = new List<AlertEntry>();
}
internal static readonly Dictionary<AlertType, string> alertIconString = new Dictionary<AlertType, string> {
{ AlertType.Suggestion, "Hot_Reload_alert_info" },
{ AlertType.UnsupportedChange, "Hot_Reload_warning" },
{ AlertType.CompileError, "Hot_Reload_error" },
{ AlertType.PartiallySupportedChange, "Hot_Reload_infos" },
{ AlertType.AppliedChange, "Hot_Reload_applied_patch" },
{ AlertType.UndetectedChange, "Hot_Reload_status_undetected" },
};
#pragma warning disable CS0612 // obsolete
public static Dictionary<PartiallySupportedChange, string> partiallySupportedChangeDescriptions => new Dictionary<PartiallySupportedChange, string> {
{PartiallySupportedChange.LambdaClosure, Translations.Timeline.PartiallySupportedLambdaClosure},
{PartiallySupportedChange.EditAsyncMethod, Translations.Timeline.PartiallySupportedEditAsyncMethod},
{PartiallySupportedChange.AddMonobehaviourMethod, Translations.Timeline.PartiallySupportedAddMonobehaviourMethod},
{PartiallySupportedChange.EditMonobehaviourField, Translations.Timeline.PartiallySupportedEditMonobehaviourField},
{PartiallySupportedChange.EditCoroutine, Translations.Timeline.PartiallySupportedEditCoroutine},
{PartiallySupportedChange.EditGenericFieldInitializer, Translations.Timeline.PartiallySupportedEditGenericFieldInitializer},
{PartiallySupportedChange.AddEnumMember, Translations.Timeline.PartiallySupportedAddEnumMember},
{PartiallySupportedChange.EditFieldInitializer, Translations.Timeline.PartiallySupportedEditFieldInitializer},
{PartiallySupportedChange.AddMethodWithAttributes, Translations.Timeline.PartiallySupportedAddMethodWithAttributes},
{PartiallySupportedChange.AddFieldWithAttributes, Translations.Timeline.PartiallySupportedAddFieldWithAttributes},
{PartiallySupportedChange.GenericMethodInGenericClass, Translations.Timeline.PartiallySupportedGenericMethodInGenericClass},
{PartiallySupportedChange.NewCustomSerializableField, Translations.Timeline.PartiallySupportedNewCustomSerializableField},
{PartiallySupportedChange.MultipleFieldsEditedInTheSameType, Translations.Timeline.PartiallySupportedMultipleFieldsEditedInTheSameType},
};
#pragma warning restore CS0612
public static int CountTimelineEnties(Predicate<AlertEntry> predicate = null) {
var count = 0;
foreach (var alertEntry in EventsTimeline) {
if (predicate == null || predicate(alertEntry)) {
count++;
}
if (!HotReloadPrefs.TimelineViewAll && alertEntry.alertData?.isCompile == true) {
// break on first compile entry if we only viewing recent events
break;
}
}
return count;
}
internal static List<AlertEntry> Suggestions = new List<AlertEntry>();
internal static int UnsupportedChangesCount => CountTimelineEnties(alert => alert.alertType == AlertType.UnsupportedChange && alert.entryType != EntryType.Child);
internal static int PartiallySupportedChangesCount => CountTimelineEnties(alert => alert.alertType == AlertType.PartiallySupportedChange && alert.entryType != EntryType.Child);
internal static int UndetectedChangesCount => CountTimelineEnties(alert => alert.alertType == AlertType.UndetectedChange && alert.entryType != EntryType.Child);
internal static int CompileErrorsCount => CountTimelineEnties(alert => alert.alertType == AlertType.CompileError);
internal static int AppliedChangesCount => CountTimelineEnties(alert => alert.alertType == AlertType.AppliedChange);
static Regex shortDescriptionRegex = new Regex(PackageConst.DefaultLocale == Locale.SimplifiedChinese ? @"^([\p{L}\p{N}_]+)\s([\p{L}\p{N}_]+)(?=:)" : @"^(\w+)\s(\w+)(?=:)", RegexOptions.Compiled);
internal static int GetRunTabTimelineEventCount() {
int total = 0;
if (HotReloadPrefs.RunTabUnsupportedChangesFilter) {
total += UnsupportedChangesCount;
}
if (HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter) {
total += PartiallySupportedChangesCount;
}
if (HotReloadPrefs.RunTabUndetectedPatchesFilter) {
total += UndetectedChangesCount;
}
if (HotReloadPrefs.RunTabCompileErrorFilter) {
total += CompileErrorsCount;
}
if (HotReloadPrefs.RunTabAppliedPatchesFilter) {
total += AppliedChangesCount;
}
return total;
}
internal static List<AlertEntry> expandedEntries = new List<AlertEntry>();
internal static void RenderCompileButton() {
if (GUILayout.Button(Translations.Common.ButtonRecompile.Trim(), GUILayout.Width(80))) {
HotReloadRunTab.RecompileWithChecks();
}
}
internal static void RenderReportBugButton(string title, string detail = null) {
if (GUILayout.Button(Translations.Common.ButtonBugReport.Trim(), GUILayout.Width(80))) {
ReportWindowAPI.OpenBugReport(title?.Replace($", {Translations.UI.TapHereToSeeMore}", ""), detail);
}
}
private static float maxScrollPos;
internal static void RenderErrorEventActions(string description, ErrorData errorData) {
int maxLen = 2400;
string text = errorData.stacktrace;
if (text.Length > maxLen) {
text = text.Substring(0, maxLen) + "...";
}
GUILayout.TextArea(text, HotReloadWindowStyles.StacktraceTextAreaStyle);
if (errorData.file || !errorData.stacktrace.Contains("error CS")) {
GUILayout.Space(10f);
}
using (new EditorGUILayout.HorizontalScope()) {
if (!errorData.stacktrace.Contains("error CS")) {
RenderCompileButton();
}
RenderReportBugButton(errorData.error, errorData.stacktrace);
// Link
if (errorData.file) {
GUILayout.FlexibleSpace();
if (GUILayout.Button(errorData.linkString, HotReloadWindowStyles.LinkStyle)) {
AssetDatabase.OpenAsset(errorData.file, Math.Max(errorData.lineNumber, 1));
}
}
}
}
private static Texture2D GetFilterIcon(int count, AlertType alertType) {
if (count == 0) {
return GUIHelper.ConvertToGrayscale(alertIconString[alertType]);
}
return GUIHelper.GetLocalIcon(alertIconString[alertType]);
}
internal static void RenderAlertFilters() {
using (new EditorGUILayout.HorizontalScope()) {
var text = AppliedChangesCount > 999 ? "999+" : " " + AppliedChangesCount;
HotReloadPrefs.RunTabAppliedPatchesFilter = GUILayout.Toggle(
HotReloadPrefs.RunTabAppliedPatchesFilter,
new GUIContent(text, GetFilterIcon(AppliedChangesCount, AlertType.AppliedChange)),
HotReloadWindowStyles.EventFiltersStyle);
GUILayout.Space(-1f);
text = UndetectedChangesCount > 999 ? "999+" : " " + UndetectedChangesCount;
HotReloadPrefs.RunTabUndetectedPatchesFilter = GUILayout.Toggle(
HotReloadPrefs.RunTabUndetectedPatchesFilter,
new GUIContent(text, GetFilterIcon(UnsupportedChangesCount, AlertType.UndetectedChange)),
HotReloadWindowStyles.EventFiltersStyle);
GUILayout.Space(-1f);
text = PartiallySupportedChangesCount > 999 ? "999+" : " " + PartiallySupportedChangesCount;
HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter = GUILayout.Toggle(
HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter,
new GUIContent(text, GetFilterIcon(PartiallySupportedChangesCount, AlertType.PartiallySupportedChange)),
HotReloadWindowStyles.EventFiltersStyle);
GUILayout.Space(-1f);
text = UnsupportedChangesCount > 999 ? "999+" : " " + UnsupportedChangesCount;
HotReloadPrefs.RunTabUnsupportedChangesFilter = GUILayout.Toggle(
HotReloadPrefs.RunTabUnsupportedChangesFilter,
new GUIContent(text, GetFilterIcon(UnsupportedChangesCount, AlertType.UnsupportedChange)),
HotReloadWindowStyles.EventFiltersStyle);
GUILayout.Space(-1f);
text = CompileErrorsCount > 999 ? "999+" : " " + CompileErrorsCount;
HotReloadPrefs.RunTabCompileErrorFilter = GUILayout.Toggle(
HotReloadPrefs.RunTabCompileErrorFilter,
new GUIContent(text, GetFilterIcon(CompileErrorsCount, AlertType.CompileError)),
HotReloadWindowStyles.EventFiltersStyle);
}
}
internal static void CreateErrorEventEntry(string errorString, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
var timestamp = createdAt ?? DateTime.Now;
var alertType = errorString.Contains("error CS")
? AlertType.CompileError
: AlertType.UnsupportedChange;
var title = errorString.Contains("error CS")
? Translations.Utility.CompileError
: Translations.Utility.UnsupportedChange;
ErrorData errorData = ErrorData.GetErrorData(errorString);
var description = errorData.error;
string shortDescription = null;
if (alertType != AlertType.CompileError) {
shortDescription = shortDescriptionRegex.Match(description).Value;
}
Action actionData = () => RenderErrorEventActions(description, errorData);
InsertEntry(new AlertEntry(
timestamp: timestamp,
alertType: alertType,
title: title,
description: description,
shortDescription: shortDescription,
actionData: actionData,
entryType: entryType,
alertData: new AlertData(AlertEntryType.Error, createdAt: timestamp, errorString: errorString, entryType: entryType)
));
}
#if UNITY_2020_1_OR_NEWER
internal static void CreateInlinedMethodsEntry(string[] patchedMethodsDisplayNames, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
var truncated = false;
if (patchedMethodsDisplayNames?.Length > 25) {
patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
truncated = true;
}
var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
var timestamp = createdAt ?? DateTime.Now;
var title = Translations.Timeline.EventTitleFailedApplyingPatch;
var entry = new AlertEntry(
timestamp: timestamp,
alertType : AlertType.UnsupportedChange,
title: title,
description: $"{Translations.Timeline.EventDescriptionInlinedMethods}\n\n• {(truncated ? patchesList + "\n..." : patchesList)}",
entryType: EntryType.Parent,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
RenderCompileButton();
RenderReportBugButton(title);
var suggestion = HotReloadSuggestionsHelper.suggestionMap[HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods];
if (suggestion?.actionData != null) {
suggestion.actionData();
}
}
},
alertData: new AlertData(AlertEntryType.InlinedMethod, createdAt: timestamp, patchedMembersDisplayNames: patchedMethodsDisplayNames, entryType: EntryType.Parent)
);
InsertEntry(entry);
if (patchedMethodsDisplayNames?.Length > 0) {
expandedEntries.Add(entry);
}
}
#endif
internal static void CreatePatchFailureEventEntry(string errorString, string methodName, string methodSimpleName = null, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
var timestamp = createdAt ?? DateTime.Now;
ErrorData errorData = ErrorData.GetErrorData(errorString);
var title = Translations.Timeline.EventTitleFailedApplyingPatch;
Action actionData = () => RenderErrorEventActions(errorData.error, errorData);
InsertEntry(new AlertEntry(
timestamp: timestamp,
alertType : AlertType.UnsupportedChange,
title: title,
description: string.Format(Translations.Timeline.EventDescriptionFailedApplyingPatchTapForMore, title, methodName),
shortDescription: methodSimpleName,
actionData: actionData,
entryType: entryType,
alertData: new AlertData(AlertEntryType.Failure, createdAt: timestamp, errorString: errorString, methodName: methodName, methodSimpleName: methodSimpleName, entryType: entryType)
));
}
public static T[] TruncateList<T>(T[] originalList, int len) {
if (originalList.Length <= len) {
return originalList;
}
// Create a new list with a maximum of 25 items
T[] truncatedList = new T[len];
for (int i = 0; i < originalList.Length && i < len; i++) {
truncatedList[i] = originalList[i];
}
return truncatedList;
}
internal static void CreateReloadFinishedEventEntry(DateTime? createdAt = null, string[] patchedMethodsDisplayNames = null, bool isCompile = false) {
var truncated = false;
if (patchedMethodsDisplayNames?.Length > 25) {
patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
truncated = true;
}
var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
var timestamp = createdAt ?? DateTime.Now;
var entry = new AlertEntry(
timestamp: timestamp,
alertType: AlertType.AppliedChange,
title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Reloaded],
description: patchedMethodsDisplayNames?.Length > 0
? $"• {(truncated ? patchesList + "\n..." : patchesList)}"
: Translations.Timeline.EventDescriptionNoIssuesFound,
entryType: patchedMethodsDisplayNames?.Length > 0 ? EntryType.Parent : EntryType.Standalone,
alertData: new AlertData(
AlertEntryType.PatchApplied,
createdAt: timestamp,
entryType: EntryType.Standalone,
isCompile: isCompile,
patchedMembersDisplayNames: patchedMethodsDisplayNames)
);
InsertEntry(entry);
if (patchedMethodsDisplayNames?.Length > 0) {
expandedEntries.Add(entry);
}
}
internal static void CreateReloadFinishedWithWarningsEventEntry(DateTime? createdAt = null, string[] patchedMembersDisplayNames = null) {
var truncated = false;
if (patchedMembersDisplayNames?.Length > 25) {
patchedMembersDisplayNames = TruncateList(patchedMembersDisplayNames, 25);
truncated = true;
}
var patchesList = patchedMembersDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMembersDisplayNames) : "";
var timestamp = createdAt ?? DateTime.Now;
var entry = new AlertEntry(
timestamp: timestamp,
alertType: AlertType.UnsupportedChange,
title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Unsupported],
description: patchedMembersDisplayNames?.Length > 0 ? $"• {(truncated ? patchesList + "\n...\n\n" + Translations.Timeline.EventDescriptionSeeUnsupportedChangesBelow : patchesList + "\n\n" + Translations.Timeline.EventDescriptionSeeUnsupportedChangesBelow)}" : Translations.Timeline.EventDescriptionSeeDetailedEntriesBelow,
entryType: EntryType.Parent,
alertData: new AlertData(AlertEntryType.Failure, createdAt: timestamp, entryType: EntryType.Parent, patchedMembersDisplayNames: patchedMembersDisplayNames)
);
InsertEntry(entry);
if (patchedMembersDisplayNames?.Length > 0) {
expandedEntries.Add(entry);
}
}
internal static void CreateReloadPartiallyAppliedEventEntry(DateTime? createdAt = null, string[] patchedMethodsDisplayNames = null) {
var truncated = false;
if (patchedMethodsDisplayNames?.Length > 25) {
patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
truncated = true;
}
var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
var timestamp = createdAt ?? DateTime.Now;
var entry = new AlertEntry(
timestamp: timestamp,
alertType: AlertType.PartiallySupportedChange,
title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.PartiallySupported],
description: patchedMethodsDisplayNames?.Length > 0 ? $"• {(truncated ? patchesList + "\n...\n\n" + Translations.Timeline.EventDescriptionSeePartiallyAppliedChangesBelow : patchesList + "\n\n" + Translations.Timeline.EventDescriptionSeePartiallyAppliedChangesBelow)}" : Translations.Timeline.EventDescriptionSeeDetailedEntriesBelow,
entryType: EntryType.Parent,
alertData: new AlertData(AlertEntryType.PartiallySupportedChange, createdAt: timestamp, entryType: EntryType.Parent, patchedMembersDisplayNames: patchedMethodsDisplayNames)
);
InsertEntry(entry);
if (patchedMethodsDisplayNames?.Length > 0) {
expandedEntries.Add(entry);
}
}
internal static void CreateReloadUndetectedChangeEventEntry(DateTime? createdAt = null) {
var timestamp = createdAt ?? DateTime.Now;
var title = EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Undetected];
InsertEntry(new AlertEntry(
timestamp: timestamp,
alertType : AlertType.UndetectedChange,
title: title,
description: Translations.Timeline.EventDescriptionUndetectedChange,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
RenderCompileButton();
RenderReportBugButton(title);
GUILayout.FlexibleSpace();
OpenURLButton.Render(Translations.Suggestions.ButtonDocs, Constants.UndetectedChangesURL);
GUILayout.Space(10f);
}
},
entryType: EntryType.Foldout,
alertData: new AlertData(AlertEntryType.UndetectedChange, createdAt: timestamp, entryType: EntryType.Parent)
));
}
internal static void CreatePartiallyAppliedEventEntry(PartiallySupportedChange partiallySupportedChange, EntryType entryType = EntryType.Standalone, bool detailed = true, DateTime? createdAt = null) {
var timestamp = createdAt ?? DateTime.Now;
string description;
if (!partiallySupportedChangeDescriptions.TryGetValue(partiallySupportedChange, out description)) {
return;
}
var title = detailed ? Translations.Timeline.EventTitleChangePartiallyApplied : ToString(partiallySupportedChange);
InsertEntry(new AlertEntry(
timestamp: timestamp,
alertType : AlertType.PartiallySupportedChange,
title : title,
description : description,
shortDescription: detailed ? ToString(partiallySupportedChange) : null,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
RenderCompileButton();
RenderReportBugButton(title);
GUILayout.FlexibleSpace();
if (GetPartiallySupportedChangePref(partiallySupportedChange)) {
if (GUILayout.Button(Translations.Timeline.ButtonIgnoreEventType, HotReloadWindowStyles.LinkStyle)) {
HidePartiallySupportedChange(partiallySupportedChange);
HotReloadRunTab.RepaintInstant();
}
}
}
},
entryType: entryType,
alertData: new AlertData(AlertEntryType.PartiallySupportedChange, createdAt: timestamp, partiallySupportedChange: partiallySupportedChange, entryType: entryType, detiled: detailed)
));
}
internal static void InsertEntry(AlertEntry entry) {
eventsTimeline.Insert(0, entry);
if (entry.alertType != AlertType.AppliedChange) {
HotReloadState.ShowingRedDot = true;
}
}
internal static void ClearEntries() {
eventsTimeline.Clear();
}
internal static bool GetPartiallySupportedChangePref(PartiallySupportedChange key) {
return EditorPrefs.GetBool($"HotReloadWindow.ShowPartiallySupportedChangeType.{key}", true);
}
internal static void HidePartiallySupportedChange(PartiallySupportedChange key) {
EditorPrefs.SetBool($"HotReloadWindow.ShowPartiallySupportedChangeType.{key}", false);
// loop over scroll entries to remove hidden entries
for (var i = EventsTimeline.Count - 1; i >= 0; i--) {
var eventEntry = EventsTimeline[i];
if (eventEntry.alertData.partiallySupportedChange == key) {
EventsTimeline.Remove(eventEntry);
}
}
}
// performance optimization (Enum.ToString uses reflection)
internal static string ToString(this PartiallySupportedChange change) {
#pragma warning disable CS0612 // obsolete
switch (change) {
case PartiallySupportedChange.LambdaClosure:
return nameof(PartiallySupportedChange.LambdaClosure);
case PartiallySupportedChange.EditAsyncMethod:
return nameof(PartiallySupportedChange.EditAsyncMethod);
case PartiallySupportedChange.AddMonobehaviourMethod:
return nameof(PartiallySupportedChange.AddMonobehaviourMethod);
case PartiallySupportedChange.EditMonobehaviourField:
return nameof(PartiallySupportedChange.EditMonobehaviourField);
case PartiallySupportedChange.EditCoroutine:
return nameof(PartiallySupportedChange.EditCoroutine);
case PartiallySupportedChange.EditGenericFieldInitializer:
return nameof(PartiallySupportedChange.EditGenericFieldInitializer);
case PartiallySupportedChange.AddEnumMember:
return nameof(PartiallySupportedChange.AddEnumMember);
case PartiallySupportedChange.EditFieldInitializer:
return nameof(PartiallySupportedChange.EditFieldInitializer);
case PartiallySupportedChange.AddMethodWithAttributes:
return nameof(PartiallySupportedChange.AddMethodWithAttributes);
case PartiallySupportedChange.GenericMethodInGenericClass:
return nameof(PartiallySupportedChange.GenericMethodInGenericClass);
case PartiallySupportedChange.AddFieldWithAttributes:
return nameof(PartiallySupportedChange.AddFieldWithAttributes);
case PartiallySupportedChange.NewCustomSerializableField:
return nameof(PartiallySupportedChange.NewCustomSerializableField);
case PartiallySupportedChange.MultipleFieldsEditedInTheSameType:
return nameof(PartiallySupportedChange.MultipleFieldsEditedInTheSameType);
#pragma warning restore CS0612
default:
throw new ArgumentOutOfRangeException(nameof(change), change, null);
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ffb65be71b8b4d14800f8b28bf68d0ab
timeCreated: 1695210350
@@ -0,0 +1,80 @@
using System;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor {
internal class Spinner {
internal static string SpinnerIconPath => "Hot_Reload_icon_loading_star_light_mode_96";
internal static Texture2D spinnerTexture => GUIHelper.GetInvertibleIcon(InvertibleIcon.Spinner);
private Texture2D _rotatedTextureLight;
private Texture2D _rotatedTextureDark;
private Texture2D rotatedTextureLight => _rotatedTextureLight ? _rotatedTextureLight : _rotatedTextureLight = GetCopy(spinnerTexture);
private Texture2D rotatedTextureDark => _rotatedTextureDark ? _rotatedTextureDark : _rotatedTextureDark = GetCopy(spinnerTexture);
internal Texture2D rotatedTexture => HotReloadWindowStyles.IsDarkMode ? rotatedTextureDark : rotatedTextureLight;
private float _rotationAngle;
private DateTime _lastRotation;
private int _rotationPeriod;
internal Spinner(int rotationPeriodInMilliseconds) {
_rotationPeriod = rotationPeriodInMilliseconds;
}
internal Texture2D GetIcon() {
if (DateTime.UtcNow - _lastRotation > TimeSpan.FromMilliseconds(_rotationPeriod)) {
_lastRotation = DateTime.UtcNow;
_rotationAngle += 45;
if (_rotationAngle >= 360f)
_rotationAngle -= 360f;
return RotateImage(spinnerTexture, _rotationAngle);
}
return rotatedTexture;
}
private Texture2D RotateImage(Texture2D originalTexture, float angle) {
int w = originalTexture.width;
int h = originalTexture.height;
int x, y;
float centerX = w / 2f;
float centerY = h / 2f;
for (x = 0; x < w; x++) {
for (y = 0; y < h; y++) {
float dx = x - centerX;
float dy = y - centerY;
float distance = Mathf.Sqrt(dx * dx + dy * dy);
float oldAngle = Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
float newAngle = oldAngle + angle;
float newX = centerX + distance * Mathf.Cos(newAngle * Mathf.Deg2Rad);
float newY = centerY + distance * Mathf.Sin(newAngle * Mathf.Deg2Rad);
if (newX >= 0 && newX < w && newY >= 0 && newY < h) {
rotatedTexture.SetPixel(x, y, originalTexture.GetPixel((int)newX, (int)newY));
} else {
rotatedTexture.SetPixel(x, y, Color.clear);
}
}
}
rotatedTexture.Apply();
return rotatedTexture;
}
public static Texture2D GetCopy(Texture2D tex, TextureFormat format = TextureFormat.RGBA32, bool mipChain = false) {
var tmp = RenderTexture.GetTemporary(tex.width, tex.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear);
Graphics.Blit(tex, tmp);
RenderTexture.active = tmp;
try {
var copy = new Texture2D(tex.width, tex.height, format, mipChain: mipChain);
copy.ReadPixels(new Rect(0, 0, tmp.width, tmp.height), 0, 0);
copy.Apply();
return copy;
} finally {
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(tmp);
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8bd77f0465824c5da3e1454f75c6e93c
timeCreated: 1685871830
@@ -0,0 +1,95 @@
using UnityEngine;
using System.Reflection;
using System;
using System.Collections;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("SingularityGroup.HotReload.Demo")]
namespace SingularityGroup.HotReload.Editor {
internal class UnitySettingsHelper {
public static UnitySettingsHelper I = new UnitySettingsHelper();
private bool initialized;
private object pref;
private PropertyInfo prefColorProp;
private MethodInfo setMethod;
private Type settingsType;
private Type prefColorType;
const string currentPlaymodeTintPrefKey = "Playmode tint";
internal bool playmodeTintSupported => EditorCodePatcher.config.changePlaymodeTint && EnsureInitialized();
private UnitySettingsHelper() {
EnsureInitialized();
}
private bool EnsureInitialized() {
if (initialized) {
return true;
}
try {
// cache members for performance
settingsType = settingsType ?? (settingsType = typeof(UnityEditor.Editor).Assembly.GetType($"UnityEditor.PrefSettings"));
prefColorType = prefColorType ?? (prefColorType = typeof(UnityEditor.Editor).Assembly.GetType($"UnityEditor.PrefColor"));
prefColorProp = prefColorProp ?? (prefColorProp = prefColorType?.GetProperty("Color", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public));
pref = pref ?? (pref = GetPref(settingsType: settingsType, prefColorType: prefColorType));
setMethod = setMethod ?? (setMethod = GetSetMethod(settingsType: settingsType, prefColorType: prefColorType));
if (prefColorProp == null
|| pref == null
|| setMethod == null
) {
return false;
}
// clear cache for performance
settingsType = null;
prefColorType = null;
initialized = true;
return true;
} catch {
return false;
}
}
private static MethodInfo GetSetMethod(Type settingsType, Type prefColorType) {
var setMethodBase = settingsType?.GetMethod("Set", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
return setMethodBase?.MakeGenericMethod(prefColorType);
}
private static object GetPref(Type settingsType, Type prefColorType) {
var prefsMethodBase = settingsType?.GetMethod("Prefs", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
var prefsMethod = prefsMethodBase?.MakeGenericMethod(prefColorType);
var prefs = (IEnumerable)prefsMethod?.Invoke(null, Array.Empty<object>());
if (prefs != null) {
foreach (object kvp in prefs) {
var key = kvp.GetType().GetProperty("Key", BindingFlags.Instance | BindingFlags.Public)?.GetMethod.Invoke(kvp, Array.Empty<object>());
if (key?.ToString() == currentPlaymodeTintPrefKey) {
return kvp.GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public)?.GetMethod.Invoke(kvp, Array.Empty<object>());
}
}
}
return null;
}
public Color? GetCurrentPlaymodeColor() {
if (!playmodeTintSupported) {
return null;
}
return (Color)prefColorProp.GetValue(pref);
}
public void SetPlaymodeTint(Color color) {
if (!playmodeTintSupported) {
return;
}
prefColorProp.SetValue(pref, color);
setMethod.Invoke(null, new object[] { currentPlaymodeTintPrefKey, pref });
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 34fb1222dc00466ab4e3db7383bd00ee
timeCreated: 1694279476
@@ -0,0 +1,35 @@
#if ODIN_INSPECTOR
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using SingularityGroup.HotReload.EditorDependencies;
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
namespace SingularityGroup.HotReload.Editor {
public class HotReloadAttributeProcessor : OdinAttributeProcessor {
public override bool CanProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member) {
return member is FieldInfo;
}
static object nullObject = new object();
public override void ProcessChildMemberAttributes(InspectorProperty property, MemberInfo member, List<Attribute> attributes) {
var field = member as FieldInfo;
if (field?.DeclaringType == null) {
return;
}
if (UnityFieldHelper.TryGetFieldAttributes(field, out var fieldAttributes)) {
attributes.Clear();
attributes.AddRange(fieldAttributes);
}
if (UnityFieldHelper.IsFieldHidden(field.DeclaringType, field.Name)) {
attributes.Add(new HideIfAttribute("@true"));
}
// we assume this is always not null. Most of the times it will not be. If it is the side effect is some memory footprint which hopefully gets cleared when enough objects
var key = property.ParentValues.FirstOrDefault() ?? nullObject;
UnityFieldHelper.CacheFieldInvalidation(key, field, property.RefreshSetup);
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c63452dd912fe4c46909c1c5ce844e69
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,105 @@
using System.Diagnostics;
using UnityEngine;
using UnityEditor;
namespace SingularityGroup.HotReload.Editor {
public enum PopupSource {
Window,
Overlay,
}
public class HotReloadEventPopup : PopupWindowContent {
public static HotReloadEventPopup I = new HotReloadEventPopup();
private Vector2 _PopupScrollPos;
public bool open { get; private set; }
private PopupSource source;
private HotReloadRunTabState currentState;
public static void Open(PopupSource source, Vector2 pos) {
I.source = source;
PopupWindow.Show(new Rect(pos.x, pos.y, 0, 0), I);
}
public override Vector2 GetWindowSize() {
if (HotReloadRunTab.ShouldRenderConsumption(currentState)
&& (HotReloadWindowStyles.windowScreenWidth <= Constants.ConsumptionsHideWidth
|| HotReloadWindowStyles.windowScreenHeight <= Constants.ConsumptionsHideHeight
|| source == PopupSource.Overlay)
) {
return new Vector2(600, 450);
} else {
return new Vector2(500, 375);
}
}
public void Repaint() {
if (open) {
PopupWindow.GetWindow<PopupWindow>().Repaint();
}
}
public override void OnGUI(Rect rect) {
if (Event.current.type == EventType.Layout) {
currentState = HotReloadRunTabState.Current;
}
if (HotReloadWindowStyles.windowScreenWidth <= Constants.UpgradeLicenseNoteHideWidth
|| HotReloadWindowStyles.windowScreenHeight <= Constants.UpgradeLicenseNoteHideHeight
|| source == PopupSource.Overlay
) {
HotReloadRunTab.RenderUpgradeLicenseNote(currentState, HotReloadWindowStyles.UpgradeLicenseButtonOverlayStyle);
}
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) {
using (var scope = new EditorGUILayout.ScrollViewScope(_PopupScrollPos, GUIStyle.none, GUI.skin.verticalScrollbar, GUILayout.MaxHeight(495))) {
_PopupScrollPos.x = scope.scrollPosition.x;
_PopupScrollPos.y = scope.scrollPosition.y;
var renderDebuggerInfo = Debugger.IsAttached && !CodePatcher.I.debuggerCompatibilityEnabled;
if ((HotReloadWindowStyles.windowScreenWidth <= Constants.ConsumptionsHideWidth
|| HotReloadWindowStyles.windowScreenHeight <= Constants.ConsumptionsHideHeight
|| source == PopupSource.Overlay) && !renderDebuggerInfo
) {
HotReloadRunTab.RenderLicenseInfo(currentState);
}
if (renderDebuggerInfo) {
HotReloadRunTab.RenderDebuggerAttachedInfo(true);
}
HotReloadRunTab.RenderBars(currentState);
}
}
bool rateAppShown = HotReloadWindow.ShouldShowRateApp();
if ((HotReloadWindowStyles.windowScreenWidth <= Constants.RateAppHideWidth
|| HotReloadWindowStyles.windowScreenHeight <= Constants.RateAppHideHeight
|| source == PopupSource.Overlay)
&& rateAppShown
) {
HotReloadWindow.RenderRateApp();
}
if (HotReloadWindowStyles.windowScreenWidth <= Constants.EventFiltersShownHideWidth
|| source == PopupSource.Overlay
) {
using (new EditorGUILayout.HorizontalScope()) {
GUILayout.Space(21);
HotReloadTimelineHelper.RenderAlertFilters();
if (GUILayout.Button(GUIHelper.GetInvertibleIcon(InvertibleIcon.BugReport), GUILayout.MaxHeight(20), GUILayout.MaxWidth(30))) {
ReportWindowAPI.OpenBugReport();
}
}
}
HotReloadState.ShowingRedDot = false;
}
public override void OnOpen() {
open = true;
}
public override void OnClose() {
_PopupScrollPos = Vector2.zero;
open = false;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 00ec214cde074cf298acef73bb09a4fc
timeCreated: 1696574416
@@ -0,0 +1,168 @@
#if UNITY_2021_3_OR_NEWER
using System;
using System.Collections.Generic;
using System.Reflection;
using SingularityGroup.HotReload.Editor.Localization;
using UnityEditor.Overlays;
using UnityEngine.UIElements;
using UnityEditor;
using UnityEngine;
using UnityEditor.Toolbars;
namespace SingularityGroup.HotReload.Editor {
[Overlay(typeof(SceneView), Translations.MenuItems.OverlayDescription, true)]
[Icon("Assets/HotReload/Editor/Resources/Icon_DarkMode.png")]
internal class HotReloadOverlay : ToolbarOverlay {
HotReloadOverlay() : base(HotReloadToolbarLogoButton.id, HotReloadToolbarIndicationButton.id, HotReloadToolbarRecompileButton.id) {
EditorApplication.update += Update;
}
[EditorToolbarElement(id, typeof(SceneView))]
class HotReloadToolbarLogoButton : EditorToolbarButton, IAccessContainerWindow {
internal const string id = "HotReloadOverlay/LogoButton";
public EditorWindow containerWindow { get; set; }
bool lastShowingRedDot;
internal HotReloadToolbarLogoButton() {
icon = HotReloadState.ShowingRedDot ? GUIHelper.GetInvertibleIcon(InvertibleIcon.LogoNew) : GUIHelper.GetInvertibleIcon(InvertibleIcon.Logo);
tooltip = "Hot Reload";
clicked += OnClick;
EditorApplication.update += Update;
}
void OnClick() {
HotReloadWindow.Open();
if (HotReloadWindow.Current) {
HotReloadWindow.Current.SelectTab(typeof(HotReloadRunTab));
}
}
void Update() {
if (lastShowingRedDot != HotReloadState.ShowingRedDot) {
icon = HotReloadState.ShowingRedDot ? GUIHelper.GetInvertibleIcon(InvertibleIcon.LogoNew) : GUIHelper.GetInvertibleIcon(InvertibleIcon.Logo);
lastShowingRedDot = HotReloadState.ShowingRedDot;
}
}
~HotReloadToolbarLogoButton() {
clicked -= OnClick;
EditorApplication.update -= Update;
}
}
EditorIndicationState.IndicationStatus lastIndicationStatus;
[EditorToolbarElement(id, typeof(SceneView))]
class HotReloadToolbarIndicationButton : EditorToolbarButton, IAccessContainerWindow {
internal const string id = "HotReloadOverlay/IndicationButton";
public EditorWindow containerWindow { get; set; }
EditorIndicationState.IndicationStatus lastIndicationStatus;
internal HotReloadToolbarIndicationButton() {
icon = GetIndicationIcon();
tooltip = string.Format(Translations.Timeline.IndicationTooltip, EditorIndicationState.IndicationStatusText);
clicked += OnClick;
EditorApplication.update += Update;
}
void OnClick() {
HotReloadEventPopup.Open(PopupSource.Overlay, Event.current.mousePosition);
}
void Update() {
if (lastIndicationStatus != EditorIndicationState.CurrentIndicationStatus) {
icon = GetIndicationIcon();
tooltip = string.Format(Translations.Timeline.IndicationTooltip, EditorIndicationState.IndicationStatusText);
lastIndicationStatus = EditorIndicationState.CurrentIndicationStatus;
}
}
~HotReloadToolbarIndicationButton() {
clicked -= OnClick;
EditorApplication.update -= Update;
}
}
[EditorToolbarElement(id, typeof(SceneView))]
class HotReloadToolbarRecompileButton : EditorToolbarButton, IAccessContainerWindow {
internal const string id = "HotReloadOverlay/RecompileButton";
public EditorWindow containerWindow { get; set; }
private Texture2D refreshIcon => GUIHelper.GetInvertibleIcon(InvertibleIcon.Recompile);
internal HotReloadToolbarRecompileButton() {
icon = refreshIcon;
tooltip = Translations.Miscellaneous.OverlayTooltipRecompile;
clicked += HotReloadRunTab.RecompileWithChecks;
}
}
private static Texture2D latestIcon;
private static Dictionary<string, Texture2D> iconTextures = new Dictionary<string, Texture2D>();
private static Spinner spinner = new Spinner(100);
private static Texture2D GetIndicationIcon() {
if (EditorIndicationState.IndicationIconPath == null || EditorIndicationState.SpinnerActive) {
latestIcon = spinner.GetIcon();
} else {
latestIcon = GUIHelper.GetLocalIcon(EditorIndicationState.IndicationIconPath);
}
return latestIcon;
}
private static Image indicationIcon;
private static Label indicationText;
bool initialized;
/// <summary>
/// Create Hot Reload overlay panel.
/// </summary>
public override VisualElement CreatePanelContent() {
#if UNITY_2022_1_OR_NEWER
return CreateContent(Layout.HorizontalToolbar);
#elif UNITY_2021_3_OR_NEWER
return (VisualElement)typeof(Overlay).GetMethod("CreateContent", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null, new Type[] {typeof(Layout)}, null)?.Invoke(this, new object[] {
Layout.HorizontalToolbar,
});
#endif
}
static bool _repaint;
static bool _instantRepaint;
static DateTime _lastRepaint;
private void Update() {
if (!initialized) {
return;
}
if (lastIndicationStatus != EditorIndicationState.CurrentIndicationStatus) {
indicationIcon.image = GetIndicationIcon();
indicationText.text = EditorIndicationState.IndicationStatusText;
lastIndicationStatus = EditorIndicationState.CurrentIndicationStatus;
}
try {
if (HotReloadEventPopup.I.open
&& EditorWindow.mouseOverWindow
&& EditorWindow.mouseOverWindow?.GetType() == typeof(UnityEditor.PopupWindow)
) {
_repaint = true;
}
} catch (NullReferenceException) {
// Unity randomly throws nullrefs when EditorWindow.mouseOverWindow gets accessed
}
if (_repaint && DateTime.UtcNow - _lastRepaint > TimeSpan.FromMilliseconds(33)) {
_repaint = false;
_instantRepaint = true;
}
if (_instantRepaint) {
HotReloadEventPopup.I.Repaint();
}
}
~HotReloadOverlay() {
EditorApplication.update -= Update;
}
}
}
#endif
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 91650b4b0d054bdf9c1e922305e6a61a
timeCreated: 1685130321
@@ -0,0 +1,545 @@
using System;
using System.Globalization;
using System.IO;
using JetBrains.Annotations;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Editor.Cli;
using UnityEditor;
using UnityEngine;
using RuntimeLocalization = SingularityGroup.HotReload.Localization;
namespace SingularityGroup.HotReload.Editor {
internal static class HotReloadPrefs {
private const string RemoteServerKey = "HotReloadWindow.RemoteServer";
private const string RemoteServerHostKey = "HotReloadWindow.RemoteServerHost";
private const string LicenseEmailKey = "HotReloadWindow.LicenseEmail";
private const string RenderAuthLoginKey = "HotReloadWindow.RenderAuthLogin";
private const string FirstLoginCachedKey = "HotReloadWindow.FirstLoginCachedKey";
[Obsolete]
private const string ShowOnStartupKey = "HotReloadWindow.ShowOnStartup";
private const string PasswordCachedKey = "HotReloadWindow.PasswordCached";
private const string ExposeServerToLocalNetworkKey = "HotReloadWindow.ExposeServerToLocalNetwork";
private const string ErrorHiddenCachedKey = "HotReloadWindow.ErrorHiddenCachedKey";
private const string HardwareIdKey = "HotReloadWindow.HardwareId";
private const string RefreshManuallyTipCachedKey = "HotReloadWindow.RefreshManuallyTipCachedKey";
private const string ShowLoginCachedKey = "HotReloadWindow.ShowLoginCachedKey";
private const string ConfigurationKey = "HotReloadWindow.Configuration";
private const string AvdancedKey = "HotReloadWindow.Avdanced";
private const string ShowPromoCodesCachedKey = "HotReloadWindow.ShowPromoCodesCached";
private const string ShowOnDeviceKey = "HotReloadWindow.ShowOnDevice";
private const string ShowChangelogKey = "HotReloadWindow.ShowChangelog";
private const string UnsupportedChangesKey = "HotReloadWindow.ShowUnsupportedChanges";
private const string LoggedBurstHintKey = "HotReloadWindow.LoggedBurstHint";
private const string ShouldDoAutoRefreshFixupKey = "HotReloadWindow.ShouldDoAutoRefreshFixup";
private const string ActiveDaysKey = "HotReloadWindow.ActiveDays";
[Obsolete]
private const string RateAppShownKey = "HotReloadWindow.RateAppShown";
private const string PatchesCollapseKey = "HotReloadWindow.PatchesCollapse";
private const string PatchesGroupAllKey = "HotReloadWindow.PatchesGroupAll";
private const string LaunchOnEditorStartKey = "HotReloadWindow.LaunchOnEditorStart";
[Obsolete]
private const string AutoClearTimelineKey = "HotReloadWindow.AutoClearTimeline";
private const string TimelineViewAllKey = "HotReloadWindow.TimelineViewAll";
private const string AutoRecompileUnsupportedChangesKey = "HotReloadWindow.AutoRecompileUnsupportedChanges";
private const string AutoRecompilePartiallyUnsupportedChangesKey = "HotReloadWindow.AutoRecompilePartiallyUnsupportedChanges";
private const string DisplayNewMonobehaviourMethodsAsPartiallySupportedKey = "HotReloadWindow.DisplayNewMonobehaviourMethodsAsPartiallySupported";
private const string ShowNotificationsKey = "HotReloadWindow.ShowNotifications";
private const string ShowPatchingNotificationsKey = "HotReloadWindow.ShowPatchingNotifications";
private const string ShowCompilingUnsupportedNotificationsKey = "HotReloadWindow.ShowCompilingUnsupportedNotifications";
private const string AutoRecompileUnsupportedChangesImmediatelyKey = "HotReloadWindow.AutoRecompileUnsupportedChangesImmediately";
private const string AutoRecompileUnsupportedChangesOnExitPlayModeKey = "HotReloadWindow.AutoRecompileUnsupportedChangesOnExitPlayMode";
private const string AutoRecompileUnsupportedChangesInPlayModeKey = "HotReloadWindow.AutoRecompileUnsupportedChangesInPlayMode";
private const string AutoRecompileUnsupportedChangesInEditModeKey = "HotReloadWindow.AutoRecompileUnsupportedChangesInEditMode";
private const string AutoRecompileInspectorFieldsEditKey = "HotReloadWindow.AutoRecompileInspectorFieldsEdit";
private const string AllowDisableUnityAutoRefreshKey = "HotReloadWindow.AllowDisableUnityAutoRefresh";
private const string DefaultAutoRefreshKey = "HotReloadWindow.DefaultAutoRefresh";
private const string DefaultAutoRefreshModeKey = "HotReloadWindow.DefaultAutoRefreshMode";
private const string DefaultScriptCompilationKeyKey = "HotReloadWindow.DefaultScriptCompilationKey";
private const string DefaultEditorTintKey = "HotReloadWindow.DefaultEditorTint";
private const string AppliedAutoRefreshKey = "HotReloadWindow.AppliedAutoRefresh";
private const string AppliedScriptCompilationKey = "HotReloadWindow.AppliedScriptCompilation";
private const string AppliedEditorTintKey = "HotReloadWindow.AppliedEditorTint";
private const string AllAssetChangesKey = "HotReloadWindow.AllAssetChanges";
private const string IncludeShaderChangesKey = "HotReloadWindow.IncludeShaderChanges";
private const string DisableConsoleWindowKey = "HotReloadWindow.DisableConsoleWindow";
private const string DisableDetailedErrorReportingKey = "HotReloadWindow.DisableDetailedErrorReporting";
private const string DisableTelemetryKey = "HotReloadWindow.DisableTelemetry";
private const string DebuggerCompatibilityEnabledKey = "HotReloadWindow.DebuggerCompatibilityEnabled";
private const string PauseHotReloadInEditModeKey = "HotReloadWindow.PauseHotReloadInEditMode";
private const string RedeemLicenseEmailKey = "HotReloadWindow.RedeemLicenseEmail";
private const string RedeemLicenseInvoiceKey = "HotReloadWindow.RedeemLicenseInvoice";
private const string RunTabEventsSuggestionsFoldoutKey = "HotReloadWindow.RunTabEventsSuggestionsFoldout";
private const string RunTabEventsTimelineFoldoutKey = "HotReloadWindow.RunTabEventsTimelineFoldout";
private const string RunTabUnsupportedChangesFilterKey = "HotReloadWindow.RunTabUnsupportedChangesFilter";
private const string RunTabCompileErrorFilterKey = "HotReloadWindow.RunTabCompileErrorFilter";
private const string RunTabPartiallyAppliedPatchesFilterKey = "HotReloadWindow.RunTabPartiallyAppliedPatchesFilter";
private const string RunTabUndetectedPatchesFilterKey = "HotReloadWindow.RunTabUndetectedPatchesFilter";
private const string RunTabAppliedPatchesFilterKey = "HotReloadWindow.RunTabAppliedPatchesFilter";
private const string RecompileDialogueShownKey = "HotReloadWindow.RecompileDialogueShown";
private const string ApplyFieldInitiailzerEditsToExistingClassInstancesKey = "HotReloadWindow.ApplyFieldInitiailzerEditsToExistingClassInstances";
private const string LoggedInlinedMethodsDialogueKey = "HotReloadWindow.LoggedInlinedMethodsDialogue";
private const string OpenedWindowAtLeastOnceKey = "HotReloadWindow.OpenedWindowAtLeastOnce";
private const string DeactivateHotReloadKey = "HotReloadWindow.DeactivateHotReload";
private const string ActiveLocaleKey = "HotReloadWindow.ActiveLocale";
public const string DontShowPromptForDownloadKey = "ServerDownloader.DontShowPromptForDownload";
public const string DebuggerOnboardingShownKey = "HotReloadWindow.DebuggerOnboardingShownKey";
#if UNITY_EDITOR_WIN
public const string UseWatchmanKey = "ServerDownloader.UseWatchman";
#endif
[Obsolete] public const string AllowHttpSettingCacheKey = "HotReloadWindow.AllowHttpSettingCacheKey";
[Obsolete] public const string AutoRefreshSettingCacheKey = "HotReloadWindow.AutoRefreshSettingCacheKey";
[Obsolete] public const string ScriptCompilationSettingCacheKey = "HotReloadWindow.ScriptCompilationSettingCacheKey";
[Obsolete] public const string ProjectGenerationSettingCacheKey = "HotReloadWindow.ProjectGenerationSettingCacheKey";
[Obsolete]
public static bool RemoteServer {
get { return EditorPrefs.GetBool(RemoteServerKey, false); }
set { EditorPrefs.SetBool(RemoteServerKey, value); }
}
public static bool DontShowPromptForDownload {
get { return EditorPrefs.GetBool(DontShowPromptForDownloadKey, false); }
set { EditorPrefs.SetBool(DontShowPromptForDownloadKey, value); }
}
[Obsolete]
public static string RemoteServerHost {
get { return EditorPrefs.GetString(RemoteServerHostKey); }
set { EditorPrefs.SetString(RemoteServerHostKey, value); }
}
public static string LicenseEmail {
get { return EditorPrefs.GetString(LicenseEmailKey); }
set { EditorPrefs.SetString(LicenseEmailKey, value); }
}
public static string LicensePassword {
get { return EditorPrefs.GetString(PasswordCachedKey); }
set { EditorPrefs.SetString(PasswordCachedKey, value); }
}
[Obsolete]
public static bool RenderAuthLogin { // false = render free trial
get { return EditorPrefs.GetBool(RenderAuthLoginKey); }
set { EditorPrefs.SetBool(RenderAuthLoginKey, value); }
}
[Obsolete]
public static bool FirstLogin {
get { return EditorPrefs.GetBool(FirstLoginCachedKey, true); }
set { EditorPrefs.SetBool(FirstLoginCachedKey, value); }
}
[Obsolete]
public static string ShowOnStartupLegacy { // WindowAutoOpen
get { return EditorPrefs.GetString(ShowOnStartupKey); }
set { EditorPrefs.SetString(ShowOnStartupKey, value); }
}
public static string showOnStartupPath { get; }= Path.Combine(CliUtils.GetAppDataPath(), "showOnStartup.txt");
static ShowOnStartupEnum? showOnStartup;
public static ShowOnStartupEnum ShowOnStartup {
get {
if (showOnStartup != null) {
return showOnStartup.Value;
}
if (!File.Exists(showOnStartupPath)) {
showOnStartup = ShowOnStartupEnum.Always;
return showOnStartup.Value;
}
var text = File.ReadAllText(showOnStartupPath);
ShowOnStartupEnum _showOnStartup;
if (Enum.TryParse(text, true, out _showOnStartup)) {
showOnStartup = _showOnStartup;
return showOnStartup.Value;
}
showOnStartup = ShowOnStartupEnum.Always;
return showOnStartup.Value;
}
set {
// ReSharper disable once AssignNullToNotNullAttribute
Directory.CreateDirectory(Path.GetDirectoryName(showOnStartupPath));
File.WriteAllText(showOnStartupPath, value.ToString());
showOnStartup = value;
}
}
public static bool ErrorHidden {
get { return EditorPrefs.GetBool(ErrorHiddenCachedKey); }
set { EditorPrefs.SetBool(ErrorHiddenCachedKey, value); }
}
public static string HardwareId {
get { return EditorPrefs.GetString(HardwareIdKey); }
set { EditorPrefs.SetString(HardwareIdKey, value); }
}
public static bool ShowLogin {
get { return EditorPrefs.GetBool(ShowLoginCachedKey, true); }
set { EditorPrefs.SetBool(ShowLoginCachedKey, value); }
}
public static bool ShowConfiguration {
get { return EditorPrefs.GetBool(ConfigurationKey, true); }
set { EditorPrefs.SetBool(ConfigurationKey, value); }
}
public static bool ShowAdvanced {
get { return EditorPrefs.GetBool(AvdancedKey, false); }
set { EditorPrefs.SetBool(AvdancedKey, value); }
}
public static bool ShowPromoCodes {
get { return EditorPrefs.GetBool(ShowPromoCodesCachedKey, true); }
set { EditorPrefs.SetBool(ShowPromoCodesCachedKey, value); }
}
public static bool ShowOnDevice {
get { return EditorPrefs.GetBool(ShowOnDeviceKey, true); }
set { EditorPrefs.SetBool(ShowOnDeviceKey, value); }
}
public static bool ShowChangeLog {
get { return EditorPrefs.GetBool(ShowChangelogKey, true); }
set { EditorPrefs.SetBool(ShowChangelogKey, value); }
}
public static bool ShowUnsupportedChanges {
get { return EditorPrefs.GetBool(UnsupportedChangesKey, true); }
set { EditorPrefs.SetBool(UnsupportedChangesKey, value); }
}
[Obsolete]
public static bool RefreshManuallyTip {
get { return EditorPrefs.GetBool(RefreshManuallyTipCachedKey); }
set { EditorPrefs.SetBool(RefreshManuallyTipCachedKey, value); }
}
public static bool LoggedBurstHint {
get { return EditorPrefs.GetBool(LoggedBurstHintKey); }
set { EditorPrefs.SetBool(LoggedBurstHintKey, value); }
}
[Obsolete]
public static bool ShouldDoAutoRefreshFixup {
get { return EditorPrefs.GetBool(ShouldDoAutoRefreshFixupKey, true); }
set { EditorPrefs.SetBool(ShouldDoAutoRefreshFixupKey, value); }
}
public static string ActiveDays {
get { return EditorPrefs.GetString(ActiveDaysKey, string.Empty); }
set { EditorPrefs.SetString(ActiveDaysKey, value); }
}
[Obsolete]
public static bool RateAppShownLegacy {
get { return EditorPrefs.GetBool(RateAppShownKey, false); }
set { EditorPrefs.SetBool(RateAppShownKey, value); }
}
static string rateAppPath = Path.Combine(CliUtils.GetAppDataPath(), "ratedApp.txt");
static bool? rateAppShown;
public static bool RateAppShown {
get {
if (rateAppShown != null) {
return rateAppShown.Value;
}
rateAppShown = File.Exists(rateAppPath);
return rateAppShown.Value;
}
set {
// ReSharper disable once AssignNullToNotNullAttribute
Directory.CreateDirectory(Path.GetDirectoryName(rateAppPath));
if (value && !File.Exists(rateAppPath)) {
using (File.Create(rateAppPath)) { }
} else if (!value && File.Exists(rateAppPath)) {
File.Delete(rateAppPath);
}
rateAppShown = value;
}
}
[Obsolete]
public static bool PatchesGroupAll {
get { return EditorPrefs.GetBool(PatchesGroupAllKey, false); }
set { EditorPrefs.SetBool(PatchesGroupAllKey, value); }
}
[Obsolete]
public static bool PatchesCollapse {
get { return EditorPrefs.GetBool(PatchesCollapseKey, true); }
set { EditorPrefs.SetBool(PatchesCollapseKey, value); }
}
[Obsolete]
public static ShowOnStartupEnum GetShowOnStartupEnum() {
ShowOnStartupEnum showOnStartupEnum;
if (Enum.TryParse(HotReloadPrefs.ShowOnStartupLegacy, true, out showOnStartupEnum)) {
return showOnStartupEnum;
}
return ShowOnStartupEnum.Always;
}
public static bool ExposeServerToLocalNetwork {
get { return EditorPrefs.GetBool(ExposeServerToLocalNetworkKey, false); }
set { EditorPrefs.SetBool(ExposeServerToLocalNetworkKey, value); }
}
public static bool LaunchOnEditorStart {
get { return EditorPrefs.GetBool(LaunchOnEditorStartKey, false); }
set { EditorPrefs.SetBool(LaunchOnEditorStartKey, value); }
}
[Obsolete]
public static bool AutoClearTimeline {
get { return EditorPrefs.GetBool(AutoClearTimelineKey, true); }
set { EditorPrefs.SetBool(AutoClearTimelineKey, value); }
}
public static bool TimelineViewAll {
get { return EditorPrefs.GetBool(TimelineViewAllKey, false); }
set { EditorPrefs.SetBool(TimelineViewAllKey, value); }
}
public static bool AutoRecompileUnsupportedChanges {
get { return EditorPrefs.GetBool(AutoRecompileUnsupportedChangesKey, false); }
set { EditorPrefs.SetBool(AutoRecompileUnsupportedChangesKey, value); }
}
public static bool AutoRecompilePartiallyUnsupportedChanges {
get { return EditorPrefs.GetBool(AutoRecompilePartiallyUnsupportedChangesKey, false); }
set { EditorPrefs.SetBool(AutoRecompilePartiallyUnsupportedChangesKey, value); }
}
public static bool DisplayNewMonobehaviourMethodsAsPartiallySupported {
get { return EditorPrefs.GetBool(DisplayNewMonobehaviourMethodsAsPartiallySupportedKey, false); }
set { EditorPrefs.SetBool(DisplayNewMonobehaviourMethodsAsPartiallySupportedKey, value); }
}
public static bool ShowNotifications {
get { return EditorPrefs.GetBool(ShowNotificationsKey, true); }
set { EditorPrefs.SetBool(ShowNotificationsKey, value); }
}
public static bool ShowPatchingNotifications {
get { return EditorPrefs.GetBool(ShowPatchingNotificationsKey, true); }
set { EditorPrefs.SetBool(ShowPatchingNotificationsKey, value); }
}
public static bool ShowCompilingUnsupportedNotifications {
get { return EditorPrefs.GetBool(ShowCompilingUnsupportedNotificationsKey, true); }
set { EditorPrefs.SetBool(ShowCompilingUnsupportedNotificationsKey, value); }
}
public static bool AutoRecompileUnsupportedChangesImmediately {
get { return EditorPrefs.GetBool(AutoRecompileUnsupportedChangesImmediatelyKey, false); }
set { EditorPrefs.SetBool(AutoRecompileUnsupportedChangesImmediatelyKey, value); }
}
public static bool AutoRecompileUnsupportedChangesOnExitPlayMode {
get { return EditorPrefs.GetBool(AutoRecompileUnsupportedChangesOnExitPlayModeKey, false); }
set { EditorPrefs.SetBool(AutoRecompileUnsupportedChangesOnExitPlayModeKey, value); }
}
public static bool AutoRecompileUnsupportedChangesInPlayMode {
get { return EditorPrefs.GetBool(AutoRecompileUnsupportedChangesInPlayModeKey, false); }
set { EditorPrefs.SetBool(AutoRecompileUnsupportedChangesInPlayModeKey, value); }
}
public static bool AutoRecompileInspectorFieldsEdit {
get { return EditorPrefs.GetBool(AutoRecompileInspectorFieldsEditKey, false); }
set { EditorPrefs.SetBool(AutoRecompileInspectorFieldsEditKey, value); }
}
public static bool AutoRecompileUnsupportedChangesInEditMode {
get { return EditorPrefs.GetBool(AutoRecompileUnsupportedChangesInEditModeKey, true); }
set { EditorPrefs.SetBool(AutoRecompileUnsupportedChangesInEditModeKey, value); }
}
public static bool AllowDisableUnityAutoRefresh {
get { return EditorPrefs.GetBool(AllowDisableUnityAutoRefreshKey, false); }
set { EditorPrefs.SetBool(AllowDisableUnityAutoRefreshKey, value); }
}
public static int DefaultAutoRefresh {
get { return EditorPrefs.GetInt(DefaultAutoRefreshKey, -1); }
set { EditorPrefs.SetInt(DefaultAutoRefreshKey, value); }
}
[UsedImplicitly]
public static int DefaultAutoRefreshMode {
get { return EditorPrefs.GetInt(DefaultAutoRefreshModeKey, -1); }
set { EditorPrefs.SetInt(DefaultAutoRefreshModeKey, value); }
}
public static int DefaultScriptCompilation {
get { return EditorPrefs.GetInt(DefaultScriptCompilationKeyKey, -1); }
set { EditorPrefs.SetInt(DefaultScriptCompilationKeyKey, value); }
}
public static Color? DefaultEditorTint {
get { return ColorFromString(EditorPrefs.GetString(DefaultEditorTintKey, string.Empty)); }
set { EditorPrefs.SetString(DefaultEditorTintKey, ColorToString(value)); }
}
public static bool AppliedAutoRefresh {
get { return EditorPrefs.GetBool(AppliedAutoRefreshKey); }
set { EditorPrefs.SetBool(AppliedAutoRefreshKey, value); }
}
public static bool AppliedScriptCompilation {
get { return EditorPrefs.GetBool(AppliedScriptCompilationKey); }
set { EditorPrefs.SetBool(AppliedScriptCompilationKey, value); }
}
public static Color? AppliedEditorTint {
get { return ColorFromString(EditorPrefs.GetString(AppliedEditorTintKey, string.Empty)); }
set { EditorPrefs.SetString(AppliedEditorTintKey, ColorToString(value)); }
}
public static bool AllAssetChanges {
get { return EditorPrefs.GetBool(AllAssetChangesKey, false); }
set { EditorPrefs.SetBool(AllAssetChangesKey, value); }
}
public static bool IncludeShaderChanges {
get { return EditorPrefs.GetBool(IncludeShaderChangesKey, false); }
set { EditorPrefs.SetBool(IncludeShaderChangesKey, value); }
}
public static bool DisableConsoleWindow {
get { return EditorPrefs.GetBool(DisableConsoleWindowKey, false); }
set { EditorPrefs.SetBool(DisableConsoleWindowKey, value); }
}
public static string RedeemLicenseEmail {
get { return EditorPrefs.GetString(RedeemLicenseEmailKey); }
set { EditorPrefs.SetString(RedeemLicenseEmailKey, value); }
}
public static string RedeemLicenseInvoice {
get { return EditorPrefs.GetString(RedeemLicenseInvoiceKey); }
set { EditorPrefs.SetString(RedeemLicenseInvoiceKey, value); }
}
public static bool RunTabEventsTimelineFoldout {
get { return EditorPrefs.GetBool(RunTabEventsTimelineFoldoutKey, true); }
set { EditorPrefs.SetBool(RunTabEventsTimelineFoldoutKey, value); }
}
public static bool RunTabEventsSuggestionsFoldout {
get { return EditorPrefs.GetBool(RunTabEventsSuggestionsFoldoutKey, true); }
set { EditorPrefs.SetBool(RunTabEventsSuggestionsFoldoutKey, value); }
}
public static bool RunTabUnsupportedChangesFilter {
get { return EditorPrefs.GetBool(RunTabUnsupportedChangesFilterKey, true); }
set { EditorPrefs.SetBool(RunTabUnsupportedChangesFilterKey, value); }
}
public static bool RunTabCompileErrorFilter {
get { return EditorPrefs.GetBool(RunTabCompileErrorFilterKey, true); }
set { EditorPrefs.SetBool(RunTabCompileErrorFilterKey, value); }
}
public static bool RunTabPartiallyAppliedPatchesFilter {
get { return EditorPrefs.GetBool(RunTabPartiallyAppliedPatchesFilterKey, true); }
set { EditorPrefs.SetBool(RunTabPartiallyAppliedPatchesFilterKey, value); }
}
public static bool RunTabUndetectedPatchesFilter {
get { return EditorPrefs.GetBool(RunTabUndetectedPatchesFilterKey, true); }
set { EditorPrefs.SetBool(RunTabUndetectedPatchesFilterKey, value); }
}
public static bool RunTabAppliedPatchesFilter {
get { return EditorPrefs.GetBool(RunTabAppliedPatchesFilterKey, true); }
set { EditorPrefs.SetBool(RunTabAppliedPatchesFilterKey, value); }
}
public static bool RecompileDialogueShown {
get { return EditorPrefs.GetBool(RecompileDialogueShownKey); }
set { EditorPrefs.SetBool(RecompileDialogueShownKey, value); }
}
public static bool OpenedWindowAtLeastOnce {
get { return EditorPrefs.GetBool(OpenedWindowAtLeastOnceKey); }
set { EditorPrefs.SetBool(OpenedWindowAtLeastOnceKey, value); }
}
private const string rgbaDelimiter = ";";
public static string ColorToString(Color? _color) {
if (_color == null) {
return null;
}
var color = _color.Value;
var cultInfo = CultureInfo.InvariantCulture;
string[] rgbaList = { color.r.ToString(cultInfo), color.g.ToString(cultInfo), color.b.ToString(cultInfo), color.a.ToString(cultInfo)};
return String.Join(rgbaDelimiter, rgbaList);
}
public static Color? ColorFromString(string ser) {
if (string.IsNullOrEmpty(ser)) {
return null;
}
string[] rgbaParts = ser.Split(rgbaDelimiter.ToCharArray());
return new Color(float.Parse(rgbaParts[0]), float.Parse(rgbaParts[1]),float.Parse(rgbaParts[2]),float.Parse(rgbaParts[3]));
}
[Obsolete("was not implemented")]
public static bool ApplyFieldInitiailzerEditsToExistingClassInstances {
get { return EditorPrefs.GetBool(ApplyFieldInitiailzerEditsToExistingClassInstancesKey); }
set { EditorPrefs.SetBool(ApplyFieldInitiailzerEditsToExistingClassInstancesKey, value); }
}
public static bool LoggedInlinedMethodsDialogue {
get { return EditorPrefs.GetBool(LoggedInlinedMethodsDialogueKey); }
set { EditorPrefs.SetBool(LoggedInlinedMethodsDialogueKey, value); }
}
public static bool DeactivateHotReload {
get { return EditorPrefs.GetBool(DeactivateHotReloadKey); }
set { EditorPrefs.SetBool(DeactivateHotReloadKey, value); }
}
public static bool DisableDetailedErrorReporting {
get { return EditorPrefs.GetBool(DisableDetailedErrorReportingKey, false); }
set { EditorPrefs.SetBool(DisableDetailedErrorReportingKey, value); }
}
public static bool DisableTelemetry {
get { return EditorPrefs.GetBool(DisableTelemetryKey, false); }
set { EditorPrefs.SetBool(DisableTelemetryKey, value); }
}
public static bool PauseHotReloadInEditMode {
get { return EditorPrefs.GetBool(PauseHotReloadInEditModeKey, false); }
set { EditorPrefs.SetBool(PauseHotReloadInEditModeKey, value); }
}
public static bool AutoDisableHotReloadWithDebugger {
get { return EditorPrefs.GetBool(DebuggerCompatibilityEnabledKey, true); }
set { EditorPrefs.SetBool(DebuggerCompatibilityEnabledKey, value); }
}
public static string ActiveLocale {
get { return EditorPrefs.GetString(ActiveLocaleKey, PackageConst.DefaultLocale); }
set { EditorPrefs.SetString(ActiveLocaleKey, value); }
}
#if UNITY_EDITOR_WIN
public static bool UseWatchman {
get { return EditorPrefs.GetBool(UseWatchmanKey, PackageConst.DefaultLocale == Locale.English); }
set { EditorPrefs.SetBool(UseWatchmanKey, value); }
}
#endif
public static bool DebuggerOnboardingShown {
get { return EditorPrefs.GetBool(DebuggerOnboardingShownKey); }
set { EditorPrefs.SetBool(DebuggerOnboardingShownKey, value); }
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96451431b50143944b85d4fbdde5f104
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,73 @@
using System.IO;
using UnityEditor;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor {
static class HotReloadSettingsEditor {
/// Ensure settings asset file is created and saved
public static void EnsureSettingsCreated(HotReloadSettingsObject asset) {
if (!SettingsExists()) {
CreateNewSettingsFile(asset, HotReloadSettingsObject.editorAssetPath);
}
}
/// Load existing settings asset or return the default settings
public static HotReloadSettingsObject LoadSettingsOrDefault() {
if (SettingsExists()) {
return AssetDatabase.LoadAssetAtPath<HotReloadSettingsObject>(HotReloadSettingsObject.editorAssetPath);
} else {
// create an instance with default values
return ScriptableObject.CreateInstance<HotReloadSettingsObject>();
}
}
/// <summary>
/// Create settings asset file
/// </summary>
/// <remarks>Assume that settings asset doesn't exist yet</remarks>
/// <returns>The settings asset</returns>
static void CreateNewSettingsFile(HotReloadSettingsObject asset, string editorAssetPath) {
// create new settings asset
// ReSharper disable once AssignNullToNotNullAttribute
Directory.CreateDirectory(Path.GetDirectoryName(editorAssetPath));
if (asset == null) {
asset = ScriptableObject.CreateInstance<HotReloadSettingsObject>();
}
AssetDatabase.CreateAsset(asset, editorAssetPath);
// Saving the asset isn't needed right after you created it. Unity will save it at the appropriate time.
// Troy: I tested in Unity 2018 LTS, first Android build creates the asset file and asset is included in the build.
}
#region include/exclude in build
private static bool SettingsExists() {
return AssetExists(HotReloadSettingsObject.editorAssetPath);
}
private static bool AssetExists(string assetPath) {
return AssetDatabase.GetMainAssetTypeAtPath(assetPath) != null;
}
public static void AddOrRemoveFromBuild(bool includeSettingsInBuild) {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
AssetDatabase.StartAssetEditing();
var so = LoadSettingsOrDefault();
try {
if (includeSettingsInBuild) {
// Note: don't need to force create settings because we know the defaults in player.
so.EnsurePrefabSetCorrectly();
EnsureSettingsCreated(so);
} else {
// this block shouldn't create the asset file, but it's also fine if it does
so.EnsurePrefabNotInBuild();
}
} finally {
AssetDatabase.StopAssetEditing();
}
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a0f4231ca4f63e54da0ecf87ab62c381
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,92 @@
using UnityEditor;
namespace SingularityGroup.HotReload.Editor {
internal static class HotReloadState {
private const string ServerPortKey = "HotReloadWindow.ServerPort";
private const string LastPatchIdKey = "HotReloadWindow.LastPatchId";
private const string ShowingRedDotKey = "HotReloadWindow.ShowingRedDot";
private const string ShowedEditorsWithoutHRKey = "HotReloadWindow.ShowedEditorWithoutHR";
private const string ShowedFieldInitializerWithSideEffectsKey = "HotReloadWindow.ShowedFieldInitializerWithSideEffects";
private const string ShowedAddMonobehaviourMethodsKey = "HotReloadWindow.ShowedAddMonobehaviourMethods";
private const string ShowedFieldInitializerExistingInstancesEditedKey = "HotReloadWindow.ShowedFieldInitializerExistingInstancesEdited";
private const string ShowedFieldInitializerExistingInstancesUneditedKey = "HotReloadWindow.ShowedFieldInitializerExistingInstancesUnedited";
private const string RecompiledUnsupportedChangesOnExitPlaymodeKey = "HotReloadWindow.RecompiledUnsupportedChangesOnExitPlaymode";
private const string RecompiledUnsupportedChangesInPlaymodeKey = "HotReloadWindow.RecompiledUnsupportedChangesInPlaymode";
private const string EditorCodePatcherInitKey = "HotReloadWindow.EditorCodePatcherInit";
private const string ShowedDebuggerCompatibilityKey = "HotReloadWindow.ShowedDebuggerCompatibility";
private const string DisallowedAutoRefreshKey = "HotReloadWindow.DisallowedAutoRefresh";
private const string WarnedDebuggerAttachedKey = "HotReloadWindow.WarnedDebuggerAttached";
public static int ServerPort {
get { return SessionState.GetInt(ServerPortKey, RequestHelper.defaultPort); }
set { SessionState.SetInt(ServerPortKey, value); }
}
public static string LastPatchId {
get { return SessionState.GetString(LastPatchIdKey, string.Empty); }
set { SessionState.SetString(LastPatchIdKey, value); }
}
public static bool ShowingRedDot {
get { return SessionState.GetBool(ShowingRedDotKey, false); }
set { SessionState.SetBool(ShowingRedDotKey, value); }
}
public static bool ShowedEditorsWithoutHR {
get { return SessionState.GetBool(ShowedEditorsWithoutHRKey, false); }
set { SessionState.SetBool(ShowedEditorsWithoutHRKey, value); }
}
public static bool ShowedFieldInitializerWithSideEffects {
get { return SessionState.GetBool(ShowedFieldInitializerWithSideEffectsKey, false); }
set { SessionState.SetBool(ShowedFieldInitializerWithSideEffectsKey, value); }
}
public static bool ShowedAddMonobehaviourMethods {
get { return SessionState.GetBool(ShowedAddMonobehaviourMethodsKey, false); }
set { SessionState.SetBool(ShowedAddMonobehaviourMethodsKey, value); }
}
public static bool ShowedFieldInitializerExistingInstancesEdited {
get { return SessionState.GetBool(ShowedFieldInitializerExistingInstancesEditedKey, false); }
set { SessionState.SetBool(ShowedFieldInitializerExistingInstancesEditedKey, value); }
}
public static bool ShowedFieldInitializerExistingInstancesUnedited {
get { return SessionState.GetBool(ShowedFieldInitializerExistingInstancesUneditedKey, false); }
set { SessionState.SetBool(ShowedFieldInitializerExistingInstancesUneditedKey, value); }
}
public static bool RecompiledUnsupportedChangesOnExitPlaymode {
get { return SessionState.GetBool(RecompiledUnsupportedChangesOnExitPlaymodeKey, false); }
set { SessionState.SetBool(RecompiledUnsupportedChangesOnExitPlaymodeKey, value); }
}
public static bool RecompiledUnsupportedChangesInPlaymode {
get { return SessionState.GetBool(RecompiledUnsupportedChangesInPlaymodeKey, false); }
set { SessionState.SetBool(RecompiledUnsupportedChangesInPlaymodeKey, value); }
}
public static bool EditorCodePatcherInit {
get { return SessionState.GetBool(EditorCodePatcherInitKey, false); }
set { SessionState.SetBool(EditorCodePatcherInitKey, value); }
}
public static bool ShowedDebuggerCompatibility {
get { return SessionState.GetBool(ShowedDebuggerCompatibilityKey, false); }
set { SessionState.SetBool(ShowedDebuggerCompatibilityKey, value); }
}
public static bool DisallowedAutoRefresh {
get { return SessionState.GetBool(DisallowedAutoRefreshKey, false); }
set { SessionState.SetBool(DisallowedAutoRefreshKey, value); }
}
public static bool WarnedDebuggerAttached {
get { return SessionState.GetBool(WarnedDebuggerAttachedKey, false); }
set { SessionState.SetBool(WarnedDebuggerAttachedKey, value); }
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 803347281bcf46b6b37d48231b8882be
timeCreated: 1694458889
@@ -0,0 +1,83 @@
#if UNITY_6000_3_OR_NEWER
using System.Collections.Generic;
using SingularityGroup.HotReload.Editor.Localization;
using UnityEditor;
using UnityEditor.Toolbars;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor {
internal static class HotReloadToolbar {
const string k_ElementId = "HotReload";
static readonly Spinner _spinner = new Spinner(100);
static HotReloadToolbar() {
bool _lastShowingRedDot = HotReloadState.ShowingRedDot;
EditorIndicationState.IndicationStatus _lastIndicationStatus = EditorIndicationState.CurrentIndicationStatus;
EditorApplication.update += () => {
if (_lastShowingRedDot == HotReloadState.ShowingRedDot && _lastIndicationStatus == EditorIndicationState.CurrentIndicationStatus) {
return;
}
_lastShowingRedDot = HotReloadState.ShowingRedDot;
_lastIndicationStatus = EditorIndicationState.CurrentIndicationStatus;
MainToolbar.Refresh(k_ElementId);
};
}
[MainToolbarElement(
k_ElementId,
defaultDockPosition = MainToolbarDockPosition.Right)]
static IEnumerable<MainToolbarElement> CreateToolbar() {
// ── Logo button ────────────────────────────────────────────────
yield return new MainToolbarButton(
new MainToolbarContent(
"",
GetLogoIcon(),
HotReloadState.ShowingRedDot ? $"Hot Realod\n{Translations.Timeline.OpenToViewNewEventsTooltip}" : "Hot Reload"),
OnLogoClick);
// ── Indication button ────────────────────────────────────────────
yield return new MainToolbarButton(
new MainToolbarContent(
"",
GetIndicationIcon(),
string.Format(Translations.Timeline.IndicationTooltip, EditorIndicationState.IndicationStatusText)),
OnIndicationClick);
// ── Recompile button ─────────────────────────────────────────────
yield return new MainToolbarButton(
new MainToolbarContent(
"",
GUIHelper.GetInvertibleIcon(InvertibleIcon.Recompile),
Translations.Miscellaneous.OverlayTooltipRecompile),
HotReloadRunTab.RecompileWithChecks);
}
static void OnLogoClick() {
HotReloadWindow.Open();
if (HotReloadWindow.Current) {
HotReloadWindow.Current.SelectTab(typeof(HotReloadRunTab));
}
}
static void OnIndicationClick() =>
HotReloadEventPopup.Open(PopupSource.Overlay, Event.current.mousePosition);
static Texture2D GetIndicationIcon() {
if (EditorIndicationState.IndicationIconPath == null || EditorIndicationState.SpinnerActive) {
return _spinner.GetIcon();
}
return GUIHelper.GetLocalIcon(EditorIndicationState.IndicationIconPath);
}
static Texture2D GetLogoIcon() => HotReloadState.ShowingRedDot ?
GUIHelper.GetInvertibleIcon(InvertibleIcon.LogoNew) :
GUIHelper.GetInvertibleIcon(InvertibleIcon.Logo);
}
}
#endif
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c403e42877d6481eb900306cf3f6bb51
timeCreated: 1774001943
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,147 @@
fileFormatVersion: 2
guid: 90cf8e542151548c6aa3cba26467e144
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMasterTextureLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: iPhone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,117 @@
using System.Reflection;
using SingularityGroup.HotReload.Editor;
using UnityEditor;
using UnityEngine;
[InitializeOnLoad]
public class InspectorFreezeFix
{
// Inspector window getting stuck is fixed by calling UnityEditor.InspectorWindow.RefreshInspectors()
// Below code subscribes to selection changed callback and calls the method if the inspector is actually stuck
static InspectorFreezeFix()
{
Selection.selectionChanged += OnSelectionChanged;
}
private static int _lastInitialEditorId;
private static void OnSelectionChanged() {
if (!EditorCodePatcher.config.enableInspectorFreezeFix) {
return;
}
try {
// Most of stuff is internal so we use reflection here
var inspectorType = typeof(Editor).Assembly.GetType("UnityEditor.InspectorWindow");
foreach (var inspector in Resources.FindObjectsOfTypeAll(inspectorType)) {
object isLockedValue = inspectorType.GetProperty("isLocked")?.GetValue(inspector);
if (isLockedValue == null) {
continue;
}
// If inspector window is locked deliberately by user (via the lock icon on top-right), we don't need to refresh
var isLocked = (bool)isLockedValue;
if (isLocked) {
continue;
}
// Inspector getting stuck is due to ActiveEditorTracker of that window getting stuck internally.
// The tracker starts returning same values from m_Tracker.activeEditors property.
// (Root of cause of this is out of my reach as the tracker code is mainly native code)
// We detect that by checking first element of activeEditors array
// We do the check because we don't want to RefreshInspectors every selection change, RefreshInspectors is expensive
var tracker = inspectorType.GetField("m_Tracker", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(inspector);
if (tracker == null) {
continue;
}
var activeEditors = tracker.GetType().GetProperty("activeEditors");
if (activeEditors == null) {
continue;
}
var editors = (Editor[])activeEditors.GetValue(tracker);
if (editors.Length == 0) {
continue;
}
var first = editors[0].GetInstanceID();
if (_lastInitialEditorId == first) {
// This forces the tracker to be rebuilt
var m = inspectorType.GetMethod("RefreshInspectors", BindingFlags.Static | BindingFlags.NonPublic);
if (m == null) {
// support for older versions
RefreshInspectors(inspectorType);
} else {
m.Invoke(null, null);
}
}
_lastInitialEditorId = first;
// Calling RefreshInspectors once refreshes all the editors
break;
}
} catch {
// ignore, we don't want to make user experience worse by displaying a warning in this case
}
}
static void RefreshInspectors(System.Type inspectorType) {
var allInspectorsField = inspectorType.GetField("m_AllInspectors", BindingFlags.NonPublic | BindingFlags.Static);
if (allInspectorsField == null) {
return;
}
var allInspectors = allInspectorsField.GetValue(null) as System.Collections.IEnumerable;
if (allInspectors == null) {
return;
}
foreach (var inspector in allInspectors) {
var trackerField = FindFieldInHierarchy(inspector.GetType(), "tracker");
if (trackerField == null) {
continue;
}
var tracker = trackerField.GetValue(inspector);
var forceRebuildMethod = tracker.GetType().GetMethod("ForceRebuild", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (forceRebuildMethod == null) {
continue;
}
forceRebuildMethod.Invoke(tracker, null);
}
}
static PropertyInfo FindFieldInHierarchy(System.Type type, string fieldName) {
PropertyInfo field = null;
while (type != null && field == null) {
field = type.GetProperty(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
type = type.BaseType;
}
return field;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 235343744f6348acb629d549ccafff0b
timeCreated: 1708187279
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 12e88a0f97924d18859867b0cc957d03
timeCreated: 1676802469
@@ -0,0 +1,102 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Editor.Cli;
using SingularityGroup.HotReload.Editor.Localization;
using SingularityGroup.HotReload.Localization;
using Translations = SingularityGroup.HotReload.Editor.Localization.Translations;
namespace SingularityGroup.HotReload.Editor {
static class DownloadUtility {
const string baseUrl = "https://cdn.hotreload.net";
public static async Task<DownloadResult> DownloadFile(string url, string targetFilePath, IProgress<float> progress, CancellationToken cancellationToken) {
var tmpDir = Path.GetDirectoryName(targetFilePath);
Directory.CreateDirectory(tmpDir);
using(var client = HttpClientUtils.CreateHttpClient()) {
client.Timeout = TimeSpan.FromMinutes(10);
return await client.DownloadAsync(url, targetFilePath, progress, cancellationToken).ConfigureAwait(false);
}
}
public static string GetPackagePrefix(string version, string locale) {
if (PackageConst.IsAssetStoreBuild) {
return $"releases/asset-store/{(locale == Locale.SimplifiedChinese ? "zh/" : "")}{version.Replace('.', '-')}";
}
return $"releases/{(locale == Locale.SimplifiedChinese ? "zh/" : "")}{version.Replace('.', '-')}";
}
public static string GetDownloadUrl(string key) {
return $"{baseUrl}/{key}";
}
public static async Task<DownloadResult> DownloadAsync(this HttpClient client, string requestUri, string destinationFilePath, IProgress<float> progress, CancellationToken cancellationToken = default(CancellationToken)) {
// Get the http headers first to examine the content length
using (var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false)) {
if (response.StatusCode != HttpStatusCode.OK) {
throw new DownloadException(string.Format(Translations.Errors.ExceptionDownloadFailed, response.StatusCode, response.ReasonPhrase));
}
var contentLength = response.Content.Headers.ContentLength;
if (!contentLength.HasValue) {
throw new DownloadException(Translations.Errors.ExceptionDownloadContentLengthUnknown);
}
using (var fs = new FileStream(destinationFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
using (var download = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) {
// Ignore progress reporting when no progress reporter was
if (progress == null) {
await download.CopyToAsync(fs).ConfigureAwait(false);
} else {
// Convert absolute progress (bytes downloaded) into relative progress (0% - 99.9%)
var relativeProgress = new Progress<long>(totalBytes => progress.Report(Math.Min(99.9f, (float)totalBytes / contentLength.Value)));
// Use extension method to report progress while downloading
await download.CopyToAsync(fs, 81920, relativeProgress, cancellationToken).ConfigureAwait(false);
}
await fs.FlushAsync().ConfigureAwait(false);
if (fs.Length != contentLength.Value) {
throw new DownloadException(Translations.Errors.ExceptionDownloadFileCorrupted);
}
return new DownloadResult(HttpStatusCode.OK, null);
}
}
}
static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress, CancellationToken cancellationToken) {
if (source == null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new ArgumentException(Translations.Utility.StreamHasToBeReadable, nameof(source));
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new ArgumentException(Translations.Utility.StreamHasToBeWritable, nameof(destination));
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) {
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
}
[Serializable]
public class DownloadException : ApplicationException {
public DownloadException(string message)
: base(message) {
}
public DownloadException(string message, Exception innerException)
: base(message, innerException) {
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2a7a39befa1f455cb21fcad46513b6e5
timeCreated: 1676973096
@@ -0,0 +1,18 @@
using System;
namespace SingularityGroup.HotReload.Editor {
static class ExponentialBackoff {
public static TimeSpan GetTimeout(int attempt, int minBackoff = 250, int maxBackoff = 60000, int deltaBackoff = 400) {
attempt = Math.Min(25, attempt); // safety to avoid overflow below
var delta = (uint)(
(Math.Pow(2.0, attempt) - 1.0)
* deltaBackoff
);
var interval = Math.Min(checked(minBackoff + delta), maxBackoff);
return TimeSpan.FromMilliseconds(interval);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5329de48151140eb871721ae80f925cd
timeCreated: 1676908147
@@ -0,0 +1,63 @@
using System;
using System.IO;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Editor.Cli;
using SingularityGroup.HotReload.EditorDependencies;
using UnityEditor;
using UnityEngine;
#if UNITY_2019_4_OR_NEWER
using System.Reflection;
using Unity.CodeEditor;
#endif
namespace SingularityGroup.HotReload.Editor {
static class InstallUtility {
static string installFlagPath = PackageConst.LibraryCachePath + "/installFlag.txt";
public static void DebugClearInstallState() {
File.Delete(installFlagPath);
}
// HandleEditorStart is only called on editor start, not on domain reload
public static void HandleEditorStart(string updatedFromVersion) {
var showOnStartup = HotReloadPrefs.ShowOnStartup;
if (showOnStartup == ShowOnStartupEnum.Always || (showOnStartup == ShowOnStartupEnum.OnNewVersion && !String.IsNullOrEmpty(updatedFromVersion))) {
if (!HotReloadPrefs.DeactivateHotReload) {
HotReloadWindow.Open();
}
}
if (HotReloadPrefs.LaunchOnEditorStart && !HotReloadPrefs.DeactivateHotReload) {
EditorCodePatcher.DownloadAndRun().Forget();
}
EditorCodePatcher.SendEditorTelemetryIfEnabled(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Editor, StatEventType.Start));
}
public static void CheckForNewInstall() {
if(File.Exists(installFlagPath) || MultiplayerPlaymodeHelper.IsClone) {
return;
}
Directory.CreateDirectory(Path.GetDirectoryName(installFlagPath));
using(File.Create(installFlagPath)) { }
//Avoid opening the window on domain reload
EditorApplication.delayCall += HandleNewInstall;
}
static void HandleNewInstall() {
if (EditorCodePatcher.licenseType == UnityLicenseType.UnityPro) {
RedeemLicenseHelper.I.StartRegistration();
}
HotReloadPrefs.AllowDisableUnityAutoRefresh = true;
HotReloadPrefs.AllAssetChanges = true;
HotReloadPrefs.AutoRecompileUnsupportedChanges = true;
HotReloadPrefs.AutoRecompileUnsupportedChangesOnExitPlayMode = true;
#if UNITY_EDITOR_WIN
HotReloadPrefs.UseWatchman = false;
#endif
if (HotReloadCli.CanOpenInBackground) {
HotReloadPrefs.DisableConsoleWindow = true;
}
HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.TelemetryCollection);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ee93b2c98bc7d8f4bb38bbbd5961d354
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Editor.Cli;
using SingularityGroup.HotReload.Localization;
using SingularityGroup.HotReload.Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
using Translations = SingularityGroup.HotReload.Editor.Localization.Translations;
namespace SingularityGroup.HotReload.Editor {
internal class ServerDownloader : IProgress<float> {
public float Progress {get; private set;}
public bool Started {get; private set;}
public int Attempts { get; private set; }
class Config {
public Dictionary<string, string> customServerExecutables;
}
public string GetExecutablePath(ICliController cliController) {
var targetDir = CliUtils.GetExecutableTargetDir();
var targetPath = Path.Combine(targetDir, cliController.BinaryFileName);
return targetPath;
}
public bool IsDownloaded(ICliController cliController) {
return File.Exists(GetExecutablePath(cliController));
}
public string GetBinaryPath(ICliController cliController) {
var defaultExecutablePath = CliUtils.GetExecutableTargetDir();
var config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFilePath));
var customExecutables = config?.customServerExecutables;
if (customExecutables == null) {
return defaultExecutablePath;
}
string customBinaryPath;
if (!customExecutables.TryGetValue(cliController.PlatformName, out customBinaryPath)) {
return defaultExecutablePath;
}
return Path.GetDirectoryName(customBinaryPath);
}
public bool CheckIfDownloaded(ICliController cliController) {
if(TryUseUserDefinedBinaryPath(cliController, GetExecutablePath(cliController))) {
Started = true;
Progress = 1f;
return true;
} else if(IsDownloaded(cliController)) {
Started = true;
Progress = 1f;
return true;
} else {
Started = false;
Progress = 0f;
return false;
}
}
public async Task<bool> EnsureDownloaded(ICliController cliController, CancellationToken cancellationToken, int maxAttempts = -1) {
var targetDir = CliUtils.GetExecutableTargetDir();
var targetPath = Path.Combine(targetDir, cliController.BinaryFileName);
Started = true;
Progress = 0f;
Attempts = 0;
if(File.Exists(targetPath)) {
Progress = 1f;
return true;
}
await ThreadUtility.SwitchToThreadPool(cancellationToken);
Directory.CreateDirectory(targetDir);
if(TryUseUserDefinedBinaryPath(cliController, targetPath, true)) {
Progress = 1f;
return true;
}
var tmpPath = CliUtils.GetTempDownloadFilePath("Server.tmp");
bool sucess = false;
HashSet<string> errors = null;
while(!sucess) {
try {
if (File.Exists(targetPath)) {
Progress = 1f;
Attempts = 0;
return true;
}
// Note: we are writing to temp file so if downloaded file is corrupted it will not cause issues until it's copied to target location
var result = await DownloadUtility.DownloadFile(GetDownloadUrl(cliController), tmpPath, this, cancellationToken).ConfigureAwait(false);
sucess = result.statusCode == HttpStatusCode.OK;
} catch (OperationCanceledException) {
Progress = 0;
Started = false;
Attempts = 0;
throw;
} catch (Exception e) {
var error = $"{e.GetType().Name}: {e.Message}";
errors = (errors ?? new HashSet<string>());
if (errors.Add(error)) {
Log.Warning(Translations.Errors.ErrorDownloadFailed, error);
}
}
if (!sucess) {
if (maxAttempts > 0 && Attempts + 1 >= maxAttempts) {
Progress = 0;
Attempts = 0;
Started = false;
Log.Warning(Translations.Errors.ErrorDownloadFailedMaxAttempts);
return false;
}
await Task.Delay(ExponentialBackoff.GetTimeout(Attempts++), cancellationToken).ConfigureAwait(false);
Progress = 0;
}
}
if (errors?.Count > 0) {
var data = new EditorExtraData {
{ StatKey.Errors, new List<string>(errors) },
};
// sending telemetry requires server to be running so we only attempt after server is downloaded
EditorCodePatcher.SendEditorTelemetryIfEnabled(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Editor, StatEventType.Download), data);
Log.Info(Translations.Errors.ErrorDownloadSucceeded);
}
const int ERROR_ALREADY_EXISTS = 0xB7;
try {
File.Move(tmpPath, targetPath);
} catch(IOException ex) when((ex.HResult & 0x0000FFFF) == ERROR_ALREADY_EXISTS) {
//another downloader came first
try {
File.Delete(tmpPath);
} catch {
//ignored
}
}
Progress = 1f;
Attempts = 0;
return true;
}
static bool TryUseUserDefinedBinaryPath(ICliController cliController, string targetPath, bool logNotFoundWarning = false) {
if (!File.Exists(PackageConst.ConfigFilePath)) {
return false;
}
var config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFilePath));
var customExecutables = config?.customServerExecutables;
if (customExecutables == null) {
return false;
}
string customBinaryPath;
if(!customExecutables.TryGetValue(cliController.PlatformName, out customBinaryPath)) {
return false;
}
if (!File.Exists(customBinaryPath)) {
if (logNotFoundWarning) {
Log.Warning(Translations.Errors.ErrorServerBinaryNotFound, cliController.PlatformName, customBinaryPath);
}
return false;
}
try {
var targetFile = new FileInfo(targetPath);
bool copy = true;
if (targetFile.Exists) {
copy = File.GetLastWriteTimeUtc(customBinaryPath) > targetFile.LastWriteTimeUtc;
}
if (copy) {
Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
File.Copy(customBinaryPath, targetPath, true);
}
return true;
} catch(IOException ex) {
Log.Warning(Translations.Errors.ErrorCopyingServerBinary, customBinaryPath, ex);
return false;
}
}
public static string GetDownloadUrl(ICliController cliController) {
const string version = PackageConst.ServerVersion;
// NOTE: server is not translated at the moment so we always use english
var key = $"{DownloadUtility.GetPackagePrefix(version, Locale.English)}/server/{cliController.PlatformName}/{cliController.BinaryFileName}";
return DownloadUtility.GetDownloadUrl(key);
}
void IProgress<float>.Report(float value) {
Progress = value;
}
public async Task<bool> PromptForDownload(CancellationToken manualDownloadCancelationToken) {
if (EditorUtility.DisplayDialog(
title: Translations.Dialogs.DialogTitleInstallComponents,
message: Translations.Dialogs.DialogMessageInstallComponents,
ok: Translations.Dialogs.DialogButtonInstall,
cancel: Translations.Dialogs.DialogButtonMoreInfo)
) {
try {
return await EnsureDownloaded(HotReloadCli.controller, manualDownloadCancelationToken);
} catch (Exception ex) {
if (ex is OperationCanceledException || manualDownloadCancelationToken.IsCancellationRequested) {
// binary was downloaded manually. Retry once to detect the binary.
return await EnsureDownloaded(HotReloadCli.controller, CancellationToken.None, 1);
}
Log.Exception(ex);
}
}
return false;
}
}
class DownloadResult {
public readonly HttpStatusCode statusCode;
public readonly string error;
public DownloadResult(HttpStatusCode statusCode, string error) {
this.statusCode = statusCode;
this.error = error;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f076514e142a4915ab2676a9ca6d884a
timeCreated: 1676802482
@@ -0,0 +1,95 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SingularityGroup.HotReload.Editor.Cli;
using SingularityGroup.HotReload.Editor.Localization;
using SingularityGroup.HotReload.RuntimeDependencies;
using UnityEditor;
#if UNITY_EDITOR_WIN
using System.Diagnostics;
using Debug = UnityEngine.Debug;
#endif
namespace SingularityGroup.HotReload.Editor {
static class UpdateUtility {
public static async Task<string> Update(string version, IProgress<float> progress, CancellationToken cancellationToken) {
await ThreadUtility.SwitchToThreadPool();
string serverDir;
if(!CliUtils.TryFindServerDir(out serverDir)) {
progress?.Report(1);
return Translations.Utility.UnableToLocateHotReloadPackage;
}
var packageDir = Path.GetDirectoryName(Path.GetFullPath(serverDir));
var cacheDir = Path.GetFullPath(PackageConst.LibraryCachePath);
if(Path.GetPathRoot(packageDir) != Path.GetPathRoot(cacheDir)) {
progress?.Report(1);
return Translations.Utility.UnableToUpdatePackageDifferentDrive;
}
var updatedPackageCopy = BackupPackage(packageDir, version);
var key = $"{DownloadUtility.GetPackagePrefix(version, PackageConst.DefaultLocale)}/HotReload.zip";
var url = DownloadUtility.GetDownloadUrl(key);
var targetFileName = $"HotReload{version.Replace('.', '-')}.zip";
var targetFilePath = CliUtils.GetTempDownloadFilePath(targetFileName);
var proxy = new Progress<float>(f => progress?.Report(f * 0.7f));
var result = await DownloadUtility.DownloadFile(url, targetFilePath, proxy, cancellationToken).ConfigureAwait(false);
if(result.error != null) {
progress?.Report(1);
return result.error;
}
PackageUpdater.UpdatePackage(targetFilePath, updatedPackageCopy);
progress?.Report(0.8f);
var packageRecycleBinDir = PackageConst.LibraryCachePath + $"/PackageArchived-{version}-{Guid.NewGuid():N}";
try {
Directory.Move(packageDir, packageRecycleBinDir);
Directory.Move(updatedPackageCopy, packageDir);
} catch {
// fallback to replacing files individually if access to the folder is denied
PackageUpdater.UpdatePackage(targetFilePath, packageDir);
}
try {
Directory.Delete(packageRecycleBinDir, true);
} catch (IOException) {
//ignored
}
progress?.Report(1);
return null;
}
static string BackupPackage(string packageDir, string version) {
var backupPath = PackageConst.LibraryCachePath + $"/PackageBackup-{version}";
if(Directory.Exists(backupPath)) {
Directory.Delete(backupPath, true);
}
DirectoryCopy(packageDir, backupPath);
return backupPath;
}
static void DirectoryCopy(string sourceDirPath, string destDirPath) {
var rootSource = new DirectoryInfo(sourceDirPath);
var sourceDirs = rootSource.GetDirectories();
// ensure destination directory exists
Directory.CreateDirectory(destDirPath);
// Get the files in the directory and copy them to the new destination
var files = rootSource.GetFiles();
foreach (var file in files) {
string temppath = Path.Combine(destDirPath, file.Name);
var copy = file.CopyTo(temppath);
copy.LastWriteTimeUtc = file.LastWriteTimeUtc;
}
// copying subdirectories, and their contents to destination
foreach (var subdir in sourceDirs) {
string subDirDestPath = Path.Combine(destDirPath, subdir.Name);
DirectoryCopy(subdir.FullName, subDirDestPath);
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d8485ce38122465e9e70d5992d9ae7ed
timeCreated: 1676966641
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c856b3b3ce5f41c7aaebf7b543be697a
timeCreated: 1759652305
@@ -0,0 +1,103 @@
namespace SingularityGroup.HotReload.Editor.Localization {
internal static partial class Translations {
public static class About {
// Log Messages
public static string LogNoLogsFound;
public static string LogFailedOpeningLogFile;
public static string LogBuildTargetSwitching;
// About/Help Tab
public static string AboutTitle;
public static string AboutDescription;
public static string AboutVersionInfo;
public static string AboutChangelog;
public static string AboutFeatures;
public static string AboutImprovements;
public static string AboutFixes;
public static string AboutToday;
public static string AboutYesterday;
public static string AboutDaysAgo;
public static string AboutOneMonthAgo;
public static string AboutMonthsAgo;
public static string AboutOneYearAgo;
public static string AboutYearsAgo;
// About Tab Buttons
public static string ButtonDocumentation;
public static string ButtonContact;
public static string ButtonUnityForum;
public static string ButtonReportIssue;
public static string ButtonJoinDiscord;
public static string ButtonSeeMore;
public static string ButtonManageLicense;
public static string ButtonManageAccount;
public static void LoadEnglish() {
// Log Messages
LogNoLogsFound = "No logs found";
LogFailedOpeningLogFile = "Failed opening log file.";
LogBuildTargetSwitching = "Build target is switching to {0}.";
// About/Help Tab
AboutTitle = "Help";
AboutDescription = "Info and support for Hot Reload for Unity.";
AboutVersionInfo = " Hot Reload version {0}. ";
AboutChangelog = "Changelog";
AboutFeatures = "Features:";
AboutImprovements = "Improvements:";
AboutFixes = "Fixes:";
AboutToday = "Today";
AboutYesterday = "Yesterday";
AboutDaysAgo = "{0} days ago";
AboutOneMonthAgo = "one month ago";
AboutMonthsAgo = "{0} months ago";
AboutOneYearAgo = "one year ago";
AboutYearsAgo = "{0} years ago";
// About Tab Buttons
ButtonDocumentation = "Documentation";
ButtonContact = "Contact";
ButtonUnityForum = "Unity Forum";
ButtonReportIssue = "Report issue";
ButtonJoinDiscord = "Join Discord";
ButtonSeeMore = "See More";
ButtonManageLicense = "Manage License";
ButtonManageAccount = "Manage Account";
}
public static void LoadSimplifiedChinese() {
// Log Messages
LogNoLogsFound = "未找到日志";
LogFailedOpeningLogFile = "打开日志文件失败。";
LogBuildTargetSwitching = "构建目标正在切换到 {0}。";
// About/Help Tab
AboutTitle = "帮助";
AboutDescription = "Unity Hot Reload 的信息和支持。";
AboutVersionInfo = " Hot Reload 版本 {0}。";
AboutChangelog = "更新日志";
AboutFeatures = "新功能:";
AboutImprovements = "改进:";
AboutFixes = "修复:";
AboutToday = "今天";
AboutYesterday = "昨天";
AboutDaysAgo = "{0} 天前";
AboutOneMonthAgo = "一个月前";
AboutMonthsAgo = "{0} 个月前";
AboutOneYearAgo = "一年前";
AboutYearsAgo = "{0} 年前";
// About Tab Buttons
ButtonDocumentation = "文档";
ButtonContact = "联系我们";
ButtonUnityForum = "Unity 论坛";
ButtonReportIssue = "报告问题";
ButtonJoinDiscord = "加入 Discord";
ButtonSeeMore = "查看更多";
ButtonManageLicense = "管理许可证";
ButtonManageAccount = "管理帐户";
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 155269da640bb89428bf5b3d415878c9
@@ -0,0 +1,190 @@
namespace SingularityGroup.HotReload.Editor.Localization {
internal static partial class Translations {
public static class BugReport {
// Window titles
public static string WindowTitleBugReport;
public static string WindowTitleFeedback;
public static string TabBugReport;
public static string TabFeedback;
// Form section labels
public static string LabelTitle;
public static string LabelIssueSeverity;
public static string LabelHowOften;
public static string LabelContactEmail;
public static string LabelContactEmailFinePrint;
public static string LabelDetails;
public static string LabelFeedback;
// Severity choices
public static string SeverityNormal;
public static string SeverityLow;
public static string SeverityHigh;
// Frequency choices
public static string FrequencyFirstTime;
public static string FrequencySometimes;
public static string FrequencyAlways;
// Submit button
public static string ButtonSubmit;
public static string ButtonSubmitting;
// Validation
public static string ValidationTitleRequired;
// Success screen
public static string SuccessTitle;
public static string SuccessMessage;
// Failure screen
public static string FailureTitle;
public static string FailureMessage;
// Shared buttons
public static string ButtonJoinDiscord;
public static string ButtonContactUs;
// Discard dialog
public static string DiscardDialogTitle;
public static string DiscardDialogMessage;
public static string DiscardDialogConfirm;
public static string DiscardDialogCancel;
// Misc
public static string NoSubmitHandlerError;
// Exceptions
public static string SubmitHandlerError;
public static string PlaceholderDetails;
public static string LicenseActivationFailed;
public static string LicenseActivationFailedWithError;
public static void LoadEnglish() {
WindowTitleBugReport = "Bug Report (Hot Reload)";
WindowTitleFeedback = "Feedback (Hot Reload)";
TabBugReport = "Bug Report";
TabFeedback = "Feedback";
LabelTitle = "Title";
LabelIssueSeverity = "Issue Severity";
LicenseActivationFailed = "License activation failed";
LicenseActivationFailedWithError = "License activation failed with error: {0}";
LabelHowOften = "How often does it happen?";
LabelContactEmail = "Contact Email (optional)";
LabelContactEmailFinePrint = "This email will only be used to follow up on this bug report and will not be stored or shared for any other purpose.";
LabelDetails = "Details";
LabelFeedback = "Feedback";
SeverityNormal = "Normal";
SeverityLow = "Low";
SeverityHigh = "High";
FrequencyFirstTime = "This is the first time";
FrequencySometimes = "Sometimes but not always";
FrequencyAlways = "Always";
ButtonSubmit = "Submit";
ButtonSubmitting = "Submitting...";
ValidationTitleRequired = "You must provide a title to be able to send the report.";
SuccessTitle = "Report Submitted";
SuccessMessage = "Thank you! We've accepted your report and are processing it.";
FailureTitle = "Submission Failed";
FailureMessage = "Please reach out to us directly so we can help resolve your issue.";
ButtonJoinDiscord = "Join Our Discord";
ButtonContactUs = "Contact Us";
DiscardDialogTitle = "Discard Report?";
DiscardDialogMessage = "You have unsaved content in this report. Discard changes and close?";
DiscardDialogConfirm = "Discard";
DiscardDialogCancel = "Cancel";
NoSubmitHandlerError = "No way to handle bug report";
SubmitHandlerError = "Failure occurred while handling bug report: {0}";
PlaceholderDetails = @"## Steps to reproduce:
1.
2.
3.
## What happened?
## What should have happened?
";
}
public static void LoadSimplifiedChinese() {
WindowTitleBugReport = "错误报告 (Hot Reload)";
WindowTitleFeedback = "反馈 (Hot Reload)";
TabBugReport = "错误报告";
TabFeedback = "反馈";
LabelTitle = "标题";
LabelIssueSeverity = "问题严重性";
LabelHowOften = "发生频率如何?";
LabelContactEmail = "联系邮箱(可选)";
LabelContactEmailFinePrint = "此邮箱仅用于跟进本次错误报告,不会被存储或用于其他任何目的。";
LabelDetails = "详情";
LabelFeedback = "反馈";
SeverityNormal = "普通";
SeverityLow = "低";
SeverityHigh = "高";
FrequencyFirstTime = "这是第一次发生";
FrequencySometimes = "偶尔发生";
FrequencyAlways = "总是发生";
ButtonSubmit = "提交";
ButtonSubmitting = "提交中…";
ValidationTitleRequired = "必须填写标题才能发送报告。";
SuccessTitle = "报告已提交";
SuccessMessage = "感谢您!我们已收到您的报告并正在处理中。";
FailureTitle = "提交失败";
FailureMessage = "请直接联系我们,以便我们协助解决您的问题。";
ButtonJoinDiscord = "加入我们的 Discord";
ButtonContactUs = "联系我们";
DiscardDialogTitle = "放弃报告?";
DiscardDialogMessage = "此报告中有未保存的内容。是否放弃更改并关闭?";
DiscardDialogConfirm = "放弃";
DiscardDialogCancel = "取消";
NoSubmitHandlerError = "无法处理错误报告";
SubmitHandlerError = "处理错误报告时发生错误:{0}";
LicenseActivationFailed = "许可证激活失败";
LicenseActivationFailedWithError = "许可证激活失败,错误信息:{0}";
PlaceholderDetails = @"## 复现步骤:
1.
2.
3.
## 实际发生了什么?
## 预期应发生什么?
";
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d6519b7661b44ec98e29b09e511f8ac7
timeCreated: 1773936808

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