Files
Ez2Lazer/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShift.cs
LA 0b9f9f70d6 主要为代码质量更新
1. 匹配新版按钮控件的自动宽度写法

2. 统一Ez日志写入方向

3.移除历史修改:缓存启用mod列表,切换mod时保持通用mod开启状态

4.代码格式化、

5.修改文件名称表意,更直观
2026-03-12 19:29:55 +08:00

742 lines
35 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.LAsEzExtensions.Localization;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods
{
public class ManiaModPatternShift : Mod, IApplicableAfterBeatmapConversion, IApplicableToBeatmapConverter, IHasSeed, IHasApplyOrder
{
private const double min_column_spacing_ms = 8;
public override string Name => "Pattern Shift";
public override string Acronym => "PS";
public override double ScoreMultiplier => 1;
public override LocalisableString Description => PatternShiftStrings.PATTERN_SHIFT_DESCRIPTION;
public override IconUsage? Icon => FontAwesome.Solid.Magic;
public override ModType Type => ModType.LA_Mod;
public override bool Ranked => false;
public override bool ValidForMultiplayer => true;
public override bool ValidForFreestyleAsRequiredMod => false;
[SettingSource(typeof(PatternShiftStrings), nameof(PatternShiftStrings.PATTERN_SHIFT_KEY_COUNT_LABEL), nameof(PatternShiftStrings.PATTERN_SHIFT_KEY_COUNT_DESCRIPTION))]
public BindableNumber<int> KeyCount { get; } = new BindableInt(8)
{
MinValue = 2,
MaxValue = ManiaRuleset.MAX_STAGE_KEYS,
Precision = 1
};
[SettingSource(typeof(PatternShiftStrings), nameof(PatternShiftStrings.PATTERN_SHIFT_DENSITY_LABEL), nameof(PatternShiftStrings.PATTERN_SHIFT_DENSITY_DESCRIPTION))]
public BindableNumber<int> Density { get; } = new BindableInt(7)
{
MinValue = 1,
MaxValue = 10,
Precision = 1
};
[SettingSource(typeof(PatternShiftStrings), nameof(PatternShiftStrings.PATTERN_SHIFT_MAX_CHORD_LABEL), nameof(PatternShiftStrings.PATTERN_SHIFT_MAX_CHORD_DESCRIPTION))]
public BindableNumber<int> MaxChord { get; } = new BindableInt(5)
{
MinValue = 1,
MaxValue = ManiaRuleset.MAX_STAGE_KEYS,
Precision = 1
};
[SettingSource(typeof(PatternShiftStrings), nameof(PatternShiftStrings.PATTERN_SHIFT_ALIGN_DIVISOR_LABEL), nameof(PatternShiftStrings.PATTERN_SHIFT_ALIGN_DIVISOR_DESCRIPTION))]
public BindableNumber<int> AlignDivisor { get; } = new BindableInt
{
MinValue = 0,
MaxValue = 16,
Precision = 1
};
[SettingSource(typeof(PatternShiftStrings), nameof(PatternShiftStrings.PATTERN_SHIFT_DELAY_LEVEL_LABEL), nameof(PatternShiftStrings.PATTERN_SHIFT_DELAY_LEVEL_DESCRIPTION))]
public BindableNumber<int> DelayLevel { get; } = new BindableInt
{
MinValue = 0,
MaxValue = 10,
Precision = 1
};
[SettingSource(typeof(PatternShiftStrings), nameof(PatternShiftStrings.PATTERN_SHIFT_REGENERATE_LABEL), nameof(PatternShiftStrings.PATTERN_SHIFT_REGENERATE_DESCRIPTION))]
public BindableBool Regenerate { get; } = new BindableBool();
[SettingSource(typeof(PatternShiftStrings), nameof(PatternShiftStrings.PATTERN_SHIFT_REGENERATE_DIFFICULTY_LABEL), nameof(PatternShiftStrings.PATTERN_SHIFT_REGENERATE_DIFFICULTY_DESCRIPTION))]
public BindableNumber<int> RegenerateDifficulty { get; } = new BindableInt(5)
{
MinValue = 2,
MaxValue = 10,
Precision = 1
};
[SettingSource(typeof(EzCommonModStrings), nameof(EzCommonModStrings.SEED_LABEL), nameof(EzCommonModStrings.SEED_DESCRIPTION), SettingControlType = typeof(SettingsNumberBox))]
public Bindable<int?> Seed { get; } = new Bindable<int?>();
[SettingSource(typeof(EzCommonModStrings), nameof(EzCommonModStrings.APPLY_ORDER_LABEL), nameof(EzCommonModStrings.APPLY_ORDER_DESCRIPTION))]
public BindableNumber<int> ApplyOrderIndex { get; } = new BindableInt
{
MinValue = 0,
MaxValue = 100
};
public int ApplyOrder => ApplyOrderIndex.Value;
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
yield return ("Key Count", $"{KeyCount.Value}");
yield return ("Density", $"{Density.Value}");
yield return ("Max Chord", $"{MaxChord.Value}");
yield return ("Align Divisor", AlignDivisor.Value == 0 ? "Off" : $"1/{AlignDivisor.Value}");
yield return ("Delay Level", $"{DelayLevel.Value}");
yield return ("Regenerate", Regenerate.Value ? "On" : "Off");
yield return ("Regenerate Difficulty", $"{RegenerateDifficulty.Value}");
yield return ("Seed", Seed.Value?.ToString() ?? "Random");
}
}
public void ApplyToBeatmapConverter(IBeatmapConverter converter)
{
if (converter is ManiaBeatmapConverter maniaConverter)
maniaConverter.TargetColumns = Math.Clamp(KeyCount.Value, 2, ManiaRuleset.MAX_STAGE_KEYS);
}
public void ApplyToBeatmap(IBeatmap beatmap)
{
applyToBeatmapInternal((ManiaBeatmap)beatmap);
}
private static List<PatternShiftChord> buildChords(List<PatternShiftNote> notes)
{
var chords = new List<PatternShiftChord>();
PatternShiftChord? current = null;
foreach (var note in notes)
{
if (current == null || note.StartTime != current.Time)
{
current = new PatternShiftChord(note.StartTime);
chords.Add(current);
}
current.Notes.Add(note);
}
return chords;
}
private static void reduceAllChords(List<PatternShiftChord> chordList, int maxChord, int difficulty)
{
int[] chordTimeLimits = { 200, 100, 50, 25, 12 };
int[] chordNeighborLimits = { 1, 1, 1, 1, 1 };
for (int i = 0; i < chordTimeLimits.Length; i++)
chordTimeLimits[i] = chordTimeLimits[i] * 100 / difficulty / difficulty;
foreach (var chord in chordList)
reduceChordSize(chord, maxChord);
for (int i = 1; i < chordList.Count - 1; i++)
{
for (int j = 0; j < chordTimeLimits.Length; j++)
{
double spacing = chordList[i + 1].Time - chordList[i - 1].Time;
int neighborSize = chordList[i - 1].Notes.Count + chordList[i + 1].Notes.Count;
if (spacing < chordTimeLimits[j] && neighborSize > chordNeighborLimits[j])
reduceChordSize(chordList[i], Math.Min(maxChord, 5 - j));
}
}
}
private static void reduceChordSize(PatternShiftChord chord, int newSize)
{
if (chord.Notes.Count <= newSize)
return;
chord.Notes = chord.Notes.OrderBy(n => n.SourceColumn).ToList();
while (chord.Notes.Count > newSize)
chord.Notes.RemoveAt(0);
}
private static void applyDelay(List<PatternShiftChord> chords, ControlPointInfo controlPoints, int delayLevel, Random rng)
{
if (delayLevel <= 0)
return;
foreach (var chord in chords)
{
int noteCount = chord.Notes.Count;
int maxShift = ManiaKeyPatternHelp.GetDelayMaxShiftCount(delayLevel, noteCount);
if (maxShift <= 0)
continue;
double beatLength = controlPoints.TimingPointAt(chord.Time).BeatLength;
double offsetAmount = beatLength * ManiaKeyPatternHelp.GetDelayBeatFraction(delayLevel);
var indexes = Enumerable.Range(0, noteCount).OrderBy(_ => rng.Next()).Take(maxShift).ToList();
foreach (int index in indexes)
{
var note = chord.Notes[index];
double direction = rng.NextDouble() < 0.5 ? -1 : 1;
double offset = direction * offsetAmount;
note.StartTime = Math.Max(0, note.StartTime + offset);
note.EndTime = Math.Max(note.StartTime, note.EndTime + offset);
}
}
}
private static void assignColumns(List<PatternShiftChord> chords, int keyCount, Random rng)
{
double[] lastColumnTime = new double[keyCount];
var placedNotes = new List<PatternShiftNote>();
for (int i = 0; i < keyCount; i++)
lastColumnTime[i] = -1000;
int lastNote = 0;
foreach (var chord in chords)
{
chord.Notes = chord.Notes.OrderBy(n => n.SourceColumn).ToList();
var usedColumns = new HashSet<int>();
var assigned = new List<PatternShiftNote>();
foreach (var note in chord.Notes)
{
int column = chooseColumn(keyCount, lastColumnTime, lastNote, rng, note.StartTime, min_column_spacing_ms);
if (column < 0)
continue;
if (usedColumns.Contains(column))
continue;
if (hasAssignedNoteAtTime(placedNotes, column, note.StartTime, min_column_spacing_ms))
continue;
note.AssignedColumn = column;
lastNote = column;
lastColumnTime[column] = note.IsHold ? note.EndTime : note.StartTime;
usedColumns.Add(column);
assigned.Add(note);
placedNotes.Add(note);
}
chord.Notes = assigned;
}
}
private static int chooseColumn(int keys, double[] lastUsedTime, int lastNote, Random rng, double currentTime, double minSpacingMs)
{
var candidates = new List<int>();
double safeTime = currentTime - Math.Max(0, minSpacingMs);
for (int i = 0; i < keys; i++)
{
if (lastUsedTime[i] <= safeTime)
candidates.Add(i);
}
// 若严格最小间隔下无可用列,退化到旧逻辑,避免过多丢 note。
if (candidates.Count == 0)
{
for (int i = 0; i < keys; i++)
{
if (lastUsedTime[i] <= currentTime)
candidates.Add(i);
}
}
if (candidates.Count == 0)
return -1;
double minTime = double.MaxValue;
var minIndexList = new List<int>();
for (int i = 0; i < candidates.Count; i++)
{
int index = candidates[i];
if (lastUsedTime[index] < minTime)
{
minIndexList.Clear();
minTime = lastUsedTime[index];
minIndexList.Add(index);
}
else if (lastUsedTime[index] <= minTime + 24 && lastUsedTime[index] >= minTime - 24) minIndexList.Add(index);
}
int noteLeft = minIndexList.Count(i => i < keys / 2);
int noteRight = minIndexList.Count(i => i >= (keys + 1) / 2);
if (noteRight > 0 && noteLeft > 0)
{
bool lastOnLeft = lastNote < keys / 2;
minIndexList = minIndexList.Where(i => lastOnLeft ? i >= (keys + 1) / 2 : i < keys / 2).ToList();
}
return minIndexList.Count > 0 ? minIndexList[rng.Next(minIndexList.Count)] : -1;
}
private static bool hasAssignedNoteAtTime(List<PatternShiftNote> notes, int column, double time, double tolerance = 0.5)
{
for (int i = 0; i < notes.Count; i++)
{
var note = notes[i];
if (note.AssignedColumn != column)
continue;
if (!note.IsHold && Math.Abs(note.StartTime - time) <= tolerance)
return true;
}
return false;
}
private List<PatternShiftNote> modifyNotesByDifficulty(List<PatternShiftNote> originalNotes, ManiaBeatmap beatmap, int targetColumns, int stars, Random rng)
{
// Default behavior: stars==5 => minimal change. stars<5 remove notes; stars>5 add notes.
var notes = new List<PatternShiftNote>(originalNotes);
const int center = 5;
int delta = stars - center;
int oscSeed = Seed.Value ?? RNG.Next();
var osc = new EzOscillator(oscSeed);
if (notes.Count == 0)
{
if (delta <= 0)
return notes;
// If there are no notes but user asked to increase difficulty, generate a few seed notes
var tp0 = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault();
double beatLen0 = tp0?.BeatLength ?? 500;
int initialAdd = Math.Min(8, Math.Max(1, (int)Math.Round(3 * (delta / (double)center))));
var seedList = new List<PatternShiftNote>();
for (int i = 0; i < initialAdd; i++)
{
double t = i * beatLen0 * (1.0 + 0.25 * osc.Next());
int col = rng.Next(0, Math.Max(1, targetColumns));
seedList.Add(new PatternShiftNote(Math.Max(0, t), Math.Max(0, t), Array.Empty<HitSampleInfo>(), col, false));
}
return seedList.OrderBy(n => n.StartTime).ThenBy(n => n.SourceColumn).ToList();
}
if (delta < 0)
{
double removalRatio = Math.Min(0.95, -delta / (double)(center - 1));
int targetRemove = (int)Math.Round(notes.Count * removalRatio);
var remaining = new List<PatternShiftNote>();
int removed = 0;
for (int i = 0; i < notes.Count; i++)
{
double oscVal = osc.Next(); // 0..1
double p = removalRatio * (0.6 + 0.8 * (1.0 - oscVal));
if (removed < targetRemove && rng.NextDouble() < p)
{
removed++;
continue;
}
remaining.Add(notes[i]);
}
// if still need to remove, remove from tail
while (removed < targetRemove && remaining.Count > 0)
{
remaining.RemoveAt(remaining.Count - 1);
removed++;
}
return remaining.OrderBy(n => n.StartTime).ThenBy(n => n.SourceColumn).ToList();
}
if (delta > 0)
{
double insertRatio = Math.Min(2.0, delta / (double)center); // allow up to doubling local density
int targetAdd = (int)Math.Round(notes.Count * insertRatio * 0.25); // conservative additions
var result = new List<PatternShiftNote>(notes);
// First: conservative local additions around existing notes (as before)
for (int i = 0; i < targetAdd; i++)
{
int idx = rng.Next(0, notes.Count);
var anchor = notes[idx];
var tp = beatmap.ControlPointInfo.TimingPointAt(anchor.StartTime);
double beatLength = tp.BeatLength;
int[] allowedSubdiv = new[] { 2, 4, 8, 16 };
int subdiv = allowedSubdiv[rng.Next(allowedSubdiv.Length)];
double offset = (rng.NextDouble() - 0.5) * (beatLength / subdiv) * (1.0 + 0.5 * (1.0 - osc.Next()));
double newTime = Math.Max(0, anchor.StartTime + offset);
int col = rng.Next(0, Math.Max(1, targetColumns));
// avoid duplicate at same time+col within tolerance
bool exists = result.Any(n => n.SourceColumn == col && Math.Abs(n.StartTime - newTime) <= 48);
if (exists)
{
// try different column
for (int t = 0; t < 6 && exists; t++)
{
col = (col + 1) % targetColumns;
exists = result.Any(n => n.SourceColumn == col && Math.Abs(n.StartTime - newTime) <= 48);
}
if (exists) continue;
}
result.Add(new PatternShiftNote(newTime, newTime, Array.Empty<HitSampleInfo>(), col, false));
}
// Second: fill larger gaps between existing notes to create notes in previously empty regions
int remainingGapAdds = targetAdd; // allow similar amount for gaps
for (int i = 0; i < notes.Count - 1 && remainingGapAdds > 0; i++)
{
double left = notes[i].StartTime;
double right = notes[i + 1].StartTime;
double gap = right - left;
var tpLeft = beatmap.ControlPointInfo.TimingPointAt(left);
double localBeat = tpLeft.BeatLength;
// consider gaps larger than ~1.5 beats
if (gap > Math.Max(300, localBeat * 1.5))
{
// number of potential inserts proportional to gap size and difficulty
int inserts = Math.Min(2, Math.Max(1, (int)Math.Floor(gap / (localBeat * 2.0))));
for (int j = 0; j < inserts && remainingGapAdds > 0; j++)
{
double t = left + (osc.Next() * 0.75 + 0.125) * gap; // biased away from edges
// snap to a subdivision of local beat
int[] allowedSubdiv = new[] { 2, 4, 8 };
int subdiv = allowedSubdiv[rng.Next(allowedSubdiv.Length)];
double step = localBeat / subdiv;
double snapped = Math.Round(t / step) * step;
int col = rng.Next(0, Math.Max(1, targetColumns));
bool exists = result.Any(n => n.SourceColumn == col && Math.Abs(n.StartTime - snapped) <= 48);
if (exists)
{
// try other columns quickly
for (int c = 0; c < Math.Min(6, targetColumns) && exists; c++)
{
col = (col + 1) % targetColumns;
exists = result.Any(n => n.SourceColumn == col && Math.Abs(n.StartTime - snapped) <= 48);
}
if (exists) continue;
}
result.Add(new PatternShiftNote(Math.Max(0, snapped), Math.Max(0, snapped), Array.Empty<HitSampleInfo>(), col, false));
remainingGapAdds--;
}
}
}
return result.OrderBy(n => n.StartTime).ThenBy(n => n.SourceColumn).ToList();
}
return notes;
}
// In-place Cooley-Tukey FFT (radix-2)
private static void fft(Complex[] buffer, bool inverse)
{
int n = buffer.Length;
int bits = (int)Math.Log(n, 2);
// bit-reverse permutation
for (int i = 0; i < n; i++)
{
int j = 0;
int x = i;
for (int k = 0; k < bits; k++)
{
j = (j << 1) | (x & 1);
x >>= 1;
}
if (j > i)
{
(buffer[i], buffer[j]) = (buffer[j], buffer[i]);
}
}
for (int len = 2; len <= n; len <<= 1)
{
double angle = 2 * Math.PI / len * (inverse ? 1 : -1);
var wlen = new Complex(Math.Cos(angle), Math.Sin(angle));
for (int i = 0; i < n; i += len)
{
var w = Complex.One;
for (int j = 0; j < len / 2; j++)
{
var u = buffer[i + j];
var v = buffer[i + j + len / 2] * w;
buffer[i + j] = u + v;
buffer[i + j + len / 2] = u - v;
w *= wlen;
}
}
}
if (inverse)
{
for (int i = 0; i < n; i++)
buffer[i] /= n;
}
}
private void applyToBeatmapInternal(ManiaBeatmap maniaBeatmap)
{
Seed.Value ??= RNG.Next();
var rng = new Random(Seed.Value.Value);
int targetColumns = Math.Clamp(KeyCount.Value, 2, ManiaRuleset.MAX_STAGE_KEYS);
int maxChord = Math.Clamp(MaxChord.Value, 1, targetColumns);
int difficulty = Math.Clamp(Density.Value, 1, 10);
if (maniaBeatmap.HitObjects.Count == 0 && !Regenerate.Value)
return;
maniaBeatmap.Stages.Clear();
maniaBeatmap.Stages.Add(new StageDefinition(targetColumns));
maniaBeatmap.Difficulty.CircleSize = targetColumns;
double snap(double time)
{
if (AlignDivisor.Value <= 0)
return time;
return maniaBeatmap.ControlPointInfo.GetClosestSnappedTime(time, AlignDivisor.Value);
}
var notes = maniaBeatmap.HitObjects.Select(h =>
{
if (h is HoldNote hold)
{
double start = snap(hold.StartTime);
double end = snap(hold.EndTime);
if (end < start)
end = start;
return new PatternShiftNote(start, end, hold.GetNodeSamples(0), hold.Column, end > start);
}
double time = snap(h.StartTime);
return new PatternShiftNote(time, time, h.Samples, h.Column, false);
}).OrderBy(n => n.StartTime).ThenBy(n => n.SourceColumn).ToList();
if (Regenerate.Value)
notes = modifyNotesByDifficulty(notes, maniaBeatmap, targetColumns, RegenerateDifficulty.Value, rng);
var chords = buildChords(notes);
applyDelay(chords, maniaBeatmap.ControlPointInfo, DelayLevel.Value, rng);
reduceAllChords(chords, maxChord, difficulty);
assignColumns(chords, targetColumns, rng);
var newObjects = new List<ManiaHitObject>(notes.Count);
foreach (var chord in chords)
{
foreach (var note in chord.Notes)
{
if (note.IsHold && note.EndTime > note.StartTime)
{
newObjects.Add(new HoldNote
{
Column = note.AssignedColumn,
StartTime = note.StartTime,
Duration = note.EndTime - note.StartTime,
NodeSamples = new List<IList<HitSampleInfo>> { note.Samples, Array.Empty<HitSampleInfo>() }
});
}
else
{
newObjects.Add(new Note
{
Column = note.AssignedColumn,
StartTime = note.StartTime,
Samples = note.Samples
});
}
}
}
maniaBeatmap.HitObjects = newObjects.OrderBy(h => h.StartTime).ThenBy(h => h.Column).ToList();
ManiaNoteCleanupTool.EnforceHoldReleaseGap(maniaBeatmap);
}
private class PatternShiftNote
{
public IList<HitSampleInfo> Samples { get; }
public int SourceColumn { get; }
public bool IsHold { get; }
public double StartTime { get; set; }
public double EndTime { get; set; }
public int AssignedColumn { get; set; }
public PatternShiftNote(double startTime, double endTime, IList<HitSampleInfo> samples, int sourceColumn, bool isHold)
{
StartTime = startTime;
EndTime = endTime;
Samples = samples;
SourceColumn = sourceColumn;
IsHold = isHold;
}
}
private class PatternShiftChord
{
public double Time { get; }
public List<PatternShiftNote> Notes { get; set; } = new List<PatternShiftNote>();
public PatternShiftChord(double time)
{
Time = time;
}
}
}
public static class PatternShiftStrings
{
public static readonly LocalisableString PATTERN_SHIFT_DESCRIPTION =
new EzLocalizationManager.EzLocalisableString("重构谱面列数、密度与多压", "Rebuild the beatmap with new column count, density and chord limit.");
public static readonly LocalisableString PATTERN_SHIFT_DESCRIPTION_BRACKET =
new EzLocalizationManager.EzLocalisableString("修改/补充键型:切叉", "Rebuild the beatmap with new column count, density and chord limit (with bracket).");
public static readonly LocalisableString PATTERN_SHIFT_DESCRIPTION_CHORD =
new EzLocalizationManager.EzLocalisableString("修改/补充键型:拍", "Rebuild the beatmap with new column count, density and chord limit (chord limit).");
public static readonly LocalisableString PATTERN_SHIFT_DESCRIPTION_DELAY =
new EzLocalizationManager.EzLocalisableString("修改/补充键型:偏移", "Rebuild the beatmap with new column count, density and chord limit (with delay).");
public static readonly LocalisableString PATTERN_SHIFT_DESCRIPTION_DUMP =
new EzLocalizationManager.EzLocalisableString("修改/补充键型:楼梯", "Rebuild the beatmap with new column count, density and chord limit (full).");
public static readonly LocalisableString PATTERN_SHIFT_DESCRIPTION_JACK =
new EzLocalizationManager.EzLocalisableString("修改/补充键型:叠/子弹", "Rebuild the beatmap with new column count, density and chord limit (with Jack).");
public static readonly LocalisableString PATTERN_SHIFT_KEY_COUNT_LABEL = new EzLocalizationManager.EzLocalisableString("目标列数", "Target Columns");
public static readonly LocalisableString PATTERN_SHIFT_KEY_COUNT_DESCRIPTION = new EzLocalizationManager.EzLocalisableString("设置生成后的列数", "Set the output column count.");
public static readonly LocalisableString PATTERN_SHIFT_DENSITY_LABEL = new EzLocalizationManager.EzLocalisableString("密度", "Density");
public static readonly LocalisableString PATTERN_SHIFT_DENSITY_DESCRIPTION = new EzLocalizationManager.EzLocalisableString("密度强度1-10", "Density strength (1-10).");
public static readonly LocalisableString PATTERN_SHIFT_MAX_CHORD_LABEL = new EzLocalizationManager.EzLocalisableString("和弦上限", "Max Chord");
public static readonly LocalisableString PATTERN_SHIFT_MAX_CHORD_DESCRIPTION = new EzLocalizationManager.EzLocalisableString("每一排最多保留的note数量", "Maximum notes per row.");
public static readonly LocalisableString PATTERN_SHIFT_ALIGN_DIVISOR_LABEL = new EzLocalizationManager.EzLocalisableString("对齐", "Align");
public static readonly LocalisableString PATTERN_SHIFT_ALIGN_DIVISOR_DESCRIPTION = new EzLocalizationManager.EzLocalisableString(
"对齐到节拍网格0=关闭1=1/12=1/24=1/48=1/816=1/16",
"Snap to beat grid. 0=off, 1=1/1, 2=1/2, 4=1/4, 8=1/8, 16=1/16.");
public static readonly LocalisableString PATTERN_SHIFT_DELAY_LEVEL_LABEL = new EzLocalizationManager.EzLocalisableString("Delay", "Delay");
public static readonly LocalisableString PATTERN_SHIFT_DELAY_LEVEL_DESCRIPTION = new EzLocalizationManager.EzLocalisableString(
"随机将部分note前后偏移0=关闭。\n1-3: 偏移1/16、3/32、1/8偏移数量<=等级,且至少保留等级数量不偏移。\n4-6: 同样偏移至少保留1个不偏移。\n7-10: 偏移1/16、1/12、5/48、1/8偏移数量<=等级,不强制保留。",
"Randomly offset some notes. 0=off.\n1-3: offsets 1/16, 3/32, 1/8; shift up to level, keep at least level unshifted.\n4-6: same offsets; keep at least 1 unshifted.\n7-10: offsets 1/16, 1/12, 5/48, 1/8; shift up to level, no minimum unshifted.");
public static readonly LocalisableString PATTERN_SHIFT_JACK_LEVEL_DESCRIPTION = new EzLocalizationManager.EzLocalisableString(
"Jack 等级说明:\n1: 1/2 源,移动,单侧列。\n2: 1/2 源,移动,仅双侧列(失败时回退到较低等级)。\n3: 1/4 源,移动,单侧列。\n4: 1/4 源,移动,仅双侧列(失败时回退)。\n5: 1/2 源,添加,单侧列。\n6: 1/2 源,添加,仅双侧列(失败时回退)。\n7: 1/4 源,添加,单侧列。\n8: 1/4 源,添加,仅双侧列(失败时回退)。\n9: 等级5 + 等级7。\n10: 等级6 + 等级8.",
"Jack level description:\n1: 1/2 sources, move, one-side columns.\n2: 1/2 sources, move, both-sides only (fallback to lower levels on fail).\n3: 1/4 sources, move, one-side columns.\n4: 1/4 sources, move, both-sides only (fallback).\n5: 1/2 sources, add, one-side columns.\n6: 1/2 sources, add, both-sides only (fallback).\n7: 1/4 sources, add, one-side columns.\n8: 1/4 sources, add, both-sides only (fallback).\n9: level 5 + level 7.\n10: level 6 + level 8.");
public static readonly LocalisableString PATTERN_SHIFT_WINDOW_MAX_ITERATIONS_LABEL = new EzLocalizationManager.EzLocalisableString("窗口最大迭代", "Window Max Iterations");
public static readonly LocalisableString PATTERN_SHIFT_WINDOW_MAX_ITERATIONS_DESCRIPTION = new EzLocalizationManager.EzLocalisableString("每个窗口的最大迭代次数", "Max iterations per window.");
public static readonly LocalisableString PATTERN_SHIFT_WAVEFORM_LABEL = new EzLocalizationManager.EzLocalisableString("波形", "Waveform");
public static readonly LocalisableString PATTERN_SHIFT_WAVEFORM_DESCRIPTION =
new EzLocalizationManager.EzLocalisableString("振荡器波形,影响键型局部处理时的循环周期", "Oscillator waveform used to vary pattern intensity.");
public static readonly LocalisableString PATTERN_SHIFT_LEVEL_LABEL = new EzLocalizationManager.EzLocalisableString("等级", "Level");
public static readonly LocalisableString PATTERN_SHIFT_LEVEL_DESCRIPTION =
new EzLocalizationManager.EzLocalisableString("0=off, 1-10。控制每个窗口生成的音符数量", "0=off, 1-10. Controls how many notes are generated per window.");
public static readonly LocalisableString PATTERN_SHIFT_OSCILLATION_BEATS_LABEL = new EzLocalizationManager.EzLocalisableString("振荡节拍", "Oscillation Beats");
public static readonly LocalisableString PATTERN_SHIFT_OSCILLATION_BEATS_DESCRIPTION =
new EzLocalizationManager.EzLocalisableString("振荡器变化的节拍间隔。1=每拍。", "Beat interval for oscillator changes. 1=every beat.");
public static readonly LocalisableString PATTERN_SHIFT_WINDOW_INTERVAL_LABEL = new EzLocalizationManager.EzLocalisableString("窗口间隔", "Window Interval");
public static readonly LocalisableString PATTERN_SHIFT_WINDOW_INTERVAL_DESCRIPTION =
new EzLocalizationManager.EzLocalisableString("每N半拍处理一次。1=每半拍。", "Process every N half-beats. 1=every half-beat.");
public static readonly LocalisableString PATTERN_SHIFT_WINDOW_START_OFFSET_LABEL = new EzLocalizationManager.EzLocalisableString("窗口起始偏移", "Window Start Offset");
public static readonly LocalisableString PATTERN_SHIFT_WINDOW_START_OFFSET_DESCRIPTION = new EzLocalizationManager.EzLocalisableString("1-4第一到第四个半拍。", "1-4: first to fourth half-beat.");
public static readonly LocalisableString PATTERN_SHIFT_SKIP_FINE_THRESHOLD_LABEL = new EzLocalizationManager.EzLocalisableString("Bypass高分节拍跳过阈值", "Bypass high-density beat skip threshold");
public static readonly LocalisableString PATTERN_SHIFT_SKIP_FINE_THRESHOLD_DESCRIPTION = new EzLocalizationManager.EzLocalisableString(
"在1/2节拍之间出现1/4以上不含1/4线的note数量阈值超过则跳过处理。高星图需适当增加。",
"Threshold for notes finer than 1/4 within a half-window; exceeding this skips processing. Default = 2.");
public static readonly LocalisableString PATTERN_SHIFT_SKIP_QUARTER_DIVISOR_LABEL =
new EzLocalizationManager.EzLocalisableString("Bypass 1/4线跳过阈值 (1/n)", "Bypass Quarter-line threshold divisor (1/n)");
public static readonly LocalisableString PATTERN_SHIFT_SKIP_QUARTER_DIVISOR_DESCRIPTION = new EzLocalizationManager.EzLocalisableString(
"1/4线上的note数量阈值按谱面总列数的1/n计算默认 n=2即 k/2 阈值。",
"Quarter-line note threshold is totalColumns / n. Default n=2 (k/2).");
public static readonly LocalisableString PATTERN_SHIFT_REGENERATE_LABEL = new EzLocalizationManager.EzLocalisableString("重生成", "Regenerate");
public static readonly LocalisableString PATTERN_SHIFT_REGENERATE_DESCRIPTION = new EzLocalizationManager.EzLocalisableString(
"在转换时根据音频重新生成完整谱面,而不是使用原始音符",
"Regenerate the full beatmap from audio during conversion instead of using original hit objects.");
public static readonly LocalisableString PATTERN_SHIFT_REGENERATE_DIFFICULTY_LABEL = new EzLocalizationManager.EzLocalisableString("重生成难度", "Regenerate Difficulty");
public static readonly LocalisableString PATTERN_SHIFT_REGENERATE_DIFFICULTY_DESCRIPTION = new EzLocalizationManager.EzLocalisableString(
"控制重生成的目标难度(星级),范围 2-10",
"Target difficulty (stars) for regeneration, range 2-10.");
}
}