Files
ADOFAIRenderer/UI/RenderConfigUIManager.cs
T
2026-06-13 21:40:27 +09:00

354 lines
16 KiB
C#

using System.Text;
using ADOFAIRenderer.Core;
using ADOFAIRenderer.Data;
using ADOFAIRenderer.Util.FFmpeg;
using MelonLoader;
using UnityEngine;
using UnityEngine.UIElements;
namespace ADOFAIRenderer.UI
{
public class RenderConfigUIManager : MonoBehaviour
{
[SerializeField]
private UIDocument uiDocument = null!;
private VisualElement _root = null!;
private IntegerInputValidator _intValidator = null!;
private DoubleInputValidator _doubleValidator = null!;
private EnumDropdownBinder<Codec> _codecBinder = null!;
private Button _goRenderButton = null!;
private Button _encodingConfigButton = null!;
private VisualElement _encodingConfigContainer = null!;
private void Awake()
{
_root = uiDocument.rootVisualElement;
RenderManager.Instance.Configuration = new RenderConfig();
_intValidator = new IntegerInputValidator(_root);
_doubleValidator = new DoubleInputValidator(_root);
}
private void Start()
{
Melon<RenderMod>.Logger.Msg("[UI] Start 진입");
// Init Validators
_intValidator
.AddLimits("RenderWidth", new PositiveOnlyIntegerLimit(), new RangeIntegerLimit(1, 7680))
.AddLimits("RenderHeight", new PositiveOnlyIntegerLimit(), new RangeIntegerLimit(1, 4320));
_doubleValidator
.AddLimits("RenderFPS", new PositiveOnlyDoubleLimit(), new RangeDoubleLimit(0, 9600))
.AddLimits("VideoFPS", new PositiveOnlyDoubleLimit(), new RangeDoubleLimit(0, 4800))
.AddLimits("DelayAfterLevelEnd", new PositiveOnlyDoubleLimit(), new RangeDoubleLimit(0, 86400));
_intValidator.SetupValidation();
_doubleValidator.SetupValidation();
Melon<RenderMod>.Logger.Msg("[UI] Validator 설정 완료");
// Init Enum Binders & Event Listeners
_codecBinder = new EnumDropdownBinder<Codec>(_root, "CodecDropdown", Codec.H264);
Melon<RenderMod>.Logger.Msg("[UI] CodecBinder 생성 완료");
_codecBinder.OnValueChanged(value => RenderManager.Instance.Configuration.Codec = value);
Melon<RenderMod>.Logger.Msg("[UI] CodecBinder 콜백 등록 완료");
// Bind Data
_root.dataSource = RenderManager.Instance.Configuration;
// Attach Button Event Listener
_goRenderButton = _root.Q<Button>("GoRenderButton");
if (_goRenderButton != null)
{
Melon<RenderMod>.Logger.Msg($"[UI] GoRenderButton 발견: pickingMode={_goRenderButton.pickingMode}, enabledSelf={_goRenderButton.enabledSelf}");
_goRenderButton.RegisterCallback<ClickEvent>(_ => TryGoRender());
}
else
{
Melon<RenderMod>.Logger.Error("Button 'GoRenderButton'을(를) 찾을 수 없습니다.");
}
_encodingConfigButton = _root.Q<Button>("EncodingSettingsButton");
if (_encodingConfigButton != null)
{
Melon<RenderMod>.Logger.Msg($"[UI] EncodingSettingsButton 발견: pickingMode={_encodingConfigButton.pickingMode}, enabledSelf={_encodingConfigButton.enabledSelf}");
_encodingConfigButton.RegisterCallback<ClickEvent>(_ => OpenEncodingConfig());
}
else
{
Melon<RenderMod>.Logger.Error("Button 'EncodingSettingsButton'을(를) 찾을 수 없습니다.");
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
}
}
private void OpenEncodingConfig()
{
Melon<RenderMod>.Logger.Msg("[UI] OpenEncodingConfig 호출됨");
var config = RenderManager.Instance.Configuration;
config.EncodingConfig ??= GenerateEncodingConfig(_codecBinder.Value);
var tempConfig = new EncodingConfig
{
RateControlMode = config.EncodingConfig.RateControlMode,
Bitrate = config.EncodingConfig.Bitrate,
MaxBitrate = config.EncodingConfig.MaxBitrate,
QP = config.EncodingConfig.QP
};
if (_root.Contains(_encodingConfigContainer))
{
_root.Remove(_encodingConfigContainer);
}
// 1. 전체 화면 컨테이너 생성 및 배경 적용
_encodingConfigContainer = new VisualElement { name = "EncodingSettingsContainer" };
_encodingConfigContainer.style.position = Position.Absolute;
_encodingConfigContainer.style.width = Length.Percent(100);
_encodingConfigContainer.style.height = Length.Percent(100);
_encodingConfigContainer.style.left = 0;
_encodingConfigContainer.style.top = 0;
_encodingConfigContainer.style.backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f, 1.0f));
_encodingConfigContainer.style.justifyContent = Justify.Center;
_encodingConfigContainer.style.alignItems = Align.Center;
// 로우 생성 헬퍼 (UXML의 ui-row 속성 복제)
VisualElement CreateRow()
{
var row = new VisualElement();
row.AddToClassList("ui-row");
row.style.flexDirection = FlexDirection.Row;
row.style.height = 130;
row.style.maxHeight = 130;
row.style.minHeight = 80;
row.style.justifyContent = Justify.Center;
row.style.alignItems = Align.Center;
return row;
}
// 라벨 생성 헬퍼 (우측 정렬 적용하여 인풋과의 인접성 향상)
Label CreateFormLabel(string text)
{
var label = new Label(text);
label.style.unityTextAlign = TextAnchor.MiddleRight;
label.style.color = new StyleColor(Color.white);
return label;
}
// 2. 타이틀 로우
var titleRow = CreateRow();
var title = new Label("인코딩 설정");
title.style.fontSize = 36;
title.style.unityFontStyleAndWeight = FontStyle.Bold;
title.style.color = new StyleColor(Color.white);
title.style.unityTextAlign = TextAnchor.MiddleCenter;
title.style.width = 770; // 360 * 2 + 50 (여백)에 맞춤
title.style.height = StyleKeyword.Auto;
titleRow.Add(title);
_encodingConfigContainer.Add(titleRow);
// 3. 전송률 모드 로우 (라벨 + 드롭다운)
var modeRow = CreateRow();
var modeLabel = CreateFormLabel("전송률 제어 모드");
var modeDropdown = new DropdownField { label = "" };
foreach (RateControlMode modeVal in System.Enum.GetValues(typeof(RateControlMode)))
{
modeDropdown.choices.Add(modeVal.ToString());
}
modeDropdown.value = tempConfig.RateControlMode.ToString();
modeRow.Add(modeLabel);
modeRow.Add(modeDropdown);
_encodingConfigContainer.Add(modeRow);
// 4. Bitrate 로우 (라벨 + 입력)
var bitrateRow = CreateRow();
var bitrateLabel = CreateFormLabel("대상 비트레이트 (kbps)");
var bitrateField = new IntegerField { name = "EncodingBitrate", label = "", value = tempConfig.Bitrate };
bitrateRow.Add(bitrateLabel);
bitrateRow.Add(bitrateField);
_encodingConfigContainer.Add(bitrateRow);
// 5. Max Bitrate 로우 (라벨 + 입력)
var maxBitrateRow = CreateRow();
var maxBitrateLabel = CreateFormLabel("최대 비트레이트 (kbps)");
var maxBitrateField = new IntegerField { name = "EncodingMaxBitrate", label = "", value = tempConfig.MaxBitrate };
maxBitrateRow.Add(maxBitrateLabel);
maxBitrateRow.Add(maxBitrateField);
_encodingConfigContainer.Add(maxBitrateRow);
// 6. QP 로우 (라벨 + 입력)
var qpRow = CreateRow();
var qpLabel = CreateFormLabel("QP 품질 값 (0 ~ 51)");
var qpField = new IntegerField { name = "EncodingQP", label = "", value = tempConfig.QP };
qpRow.Add(qpLabel);
qpRow.Add(qpField);
_encodingConfigContainer.Add(qpRow);
// 7. QP = 0 (무손실) 경고 라벨 로우 생성 (ui-row 클래스를 상속받아 NanumSquareNeo 폰트가 적용되도록 처리)
var warningRow = CreateRow();
warningRow.style.height = StyleKeyword.Auto;
warningRow.style.maxHeight = StyleKeyword.Null;
warningRow.style.minHeight = StyleKeyword.Null;
var warningLabel = new Label("※ QP 0 (무손실)은 일부 하드웨어/코덱에서 지원되지 않을 수 있으며, 인코딩 실패 시 CPU 모드로 폴백됩니다.");
warningLabel.style.fontSize = 20; // 30px에서 20px로 오버라이드
warningLabel.style.color = new StyleColor(new Color(0.9f, 0.6f, 0.2f));
warningLabel.style.whiteSpace = WhiteSpace.Normal;
warningLabel.style.width = 720; // 360px에서 720px로 오버라이드
warningLabel.style.height = StyleKeyword.Auto;
warningLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
warningLabel.style.marginTop = 10;
warningLabel.style.marginBottom = 10;
warningRow.Add(warningLabel);
_encodingConfigContainer.Add(warningRow);
// 7.5. 유효성 검사기 설정 (기존에 작성된 validator 재사용 및 이전 값 복원)
var encodingValidator = new IntegerInputValidator(_encodingConfigContainer);
encodingValidator
.AddLimits("EncodingBitrate", new RangeIntegerLimit(100, 1000000))
.AddLimits("EncodingMaxBitrate", new RangeIntegerLimit(100, 1000000))
.AddLimits("EncodingQP", new RangeIntegerLimit(0, 51));
encodingValidator.SetupValidation();
// 가시성 필터링 업데이트 (행 단위로 보임/숨김 제어)
void UpdateVisibility(string selectedModeStr)
{
if (System.Enum.TryParse<RateControlMode>(selectedModeStr, out var mode))
{
bitrateRow.style.display = (mode == RateControlMode.CBR || mode == RateControlMode.VBR) ? DisplayStyle.Flex : DisplayStyle.None;
maxBitrateRow.style.display = (mode == RateControlMode.VBR) ? DisplayStyle.Flex : DisplayStyle.None;
qpRow.style.display = (mode == RateControlMode.CQP) ? DisplayStyle.Flex : DisplayStyle.None;
warningRow.style.display = (mode == RateControlMode.CQP && qpField.value == 0) ? DisplayStyle.Flex : DisplayStyle.None;
}
}
modeDropdown.RegisterValueChangedCallback(evt => UpdateVisibility(evt.newValue));
qpField.RegisterValueChangedCallback(evt =>
{
warningRow.style.display = (modeDropdown.value == "CQP" && evt.newValue == 0) ? DisplayStyle.Flex : DisplayStyle.None;
});
UpdateVisibility(modeDropdown.value);
// 화면 전환 제어
void ToggleScreens(bool showEncoding)
{
foreach (var child in _root.Children())
{
if (child == _encodingConfigContainer) continue;
child.style.display = showEncoding ? DisplayStyle.None : DisplayStyle.Flex;
}
_encodingConfigContainer.style.display = showEncoding ? DisplayStyle.Flex : DisplayStyle.None;
}
// 8. 제어 버튼 로우 (취소 / 저장)
var buttonRow = CreateRow();
var cancelBtn = new Button { text = "취소" };
var saveBtn = new Button { text = "저장" };
buttonRow.Add(cancelBtn);
buttonRow.Add(saveBtn);
_encodingConfigContainer.Add(buttonRow);
cancelBtn.RegisterCallback<ClickEvent>(evt =>
{
ToggleScreens(false);
_root.Remove(_encodingConfigContainer);
_encodingConfigContainer = null!;
});
saveBtn.RegisterCallback<ClickEvent>(evt =>
{
if (System.Enum.TryParse<RateControlMode>(modeDropdown.value, out var mode))
{
tempConfig.RateControlMode = mode;
}
tempConfig.Bitrate = bitrateField.value;
tempConfig.MaxBitrate = maxBitrateField.value;
tempConfig.QP = qpField.value;
if (tempConfig.RateControlMode == RateControlMode.VBR && tempConfig.MaxBitrate < tempConfig.Bitrate)
{
tempConfig.MaxBitrate = tempConfig.Bitrate;
}
config.EncodingConfig = tempConfig;
ToggleScreens(false);
_root.Remove(_encodingConfigContainer);
_encodingConfigContainer = null!;
Melon<RenderMod>.Logger.Msg($"[UI] EncodingConfig 저장 완료");
});
_root.Add(_encodingConfigContainer);
// 화면 전환 활성화
ToggleScreens(true);
}
private void TryGoRender()
{
Melon<RenderMod>.Logger.Msg("[UI] TryGoRender 호출됨");
var widthField = _root.Q<IntegerField>("RenderWidth");
var heightField = _root.Q<IntegerField>("RenderHeight");
var randomSeedField = _root.Q<IntegerField>("RandomSeed");
var renderFpsField = _root.Q<DoubleField>("RenderFPS");
var videoFpsField = _root.Q<DoubleField>("VideoFPS");
var delayField = _root.Q<DoubleField>("DelayAfterLevelEnd");
var showJudgementText = _root.Q<Toggle>("ShowJudgementText");
var processAudio = _root.Q<Toggle>("ProcessAudioToggle");
if (string.IsNullOrEmpty(widthField.text) ||
string.IsNullOrEmpty(heightField.text) ||
string.IsNullOrEmpty(renderFpsField.text) ||
string.IsNullOrEmpty(videoFpsField.text) ||
string.IsNullOrEmpty(delayField.text))
{
Melon<RenderMod>.Logger.Error("필수 입력 값이 비어 있습니다. 모든 설정을 입력해 주세요.");
return;
}
// Copy validated values to Configuration manually
var config = RenderManager.Instance.Configuration;
config.RenderWidth = widthField.value;
config.RenderHeight = heightField.value;
config.RenderFPS = renderFpsField.value;
config.VideoFPS = videoFpsField.value;
config.DelayAfterLevelEnd = delayField.value;
config.ShowJudgementText = showJudgementText.value;
config.ProcessAudio = processAudio.value;
if (!string.IsNullOrEmpty(randomSeedField.text))
config.RandomSeed = randomSeedField.value;
RenderManager.Instance.Configuration.EncodingConfig ??= GenerateEncodingConfig(_codecBinder.Value);
RenderManager.Instance.SetupCaptureDirectory();
RenderManager.Instance.SetupGameScene();
}
private EncodingConfig GenerateEncodingConfig(Codec codec)
{
return new EncodingConfig
{
RateControlMode = RateControlMode.CBR,
Bitrate = 30000, MaxBitrate = 50000,
QP = 18
};
}
}
}