CodeVetter · Repo Unpack · Contributor handoff
2dust/v2rayN
All logic changes belong in ServiceLib; the two shells should stay thin views over shared ViewModels. Read the order below before editing: Global.cs constants, ConfigHandler, CoreConfigContextBuilder, the relevant CoreConfig service, FmtHandler, and the target ViewModel. Build requires .NET 10 SDK (global.json) and a recursive submodule checkout for the Desktop project; test via 'dotnet test v2rayN/ServiceLib.Tests'. Respect the ConfigVersion migration chain, the {0} argument-template convention, embedded Sample resource names, and the LoadCore ordering (main config -> stop -> remove tun -> start -> wait for pre-socks port -> start pre-service).
- Read-first files for any change: v2rayN/ServiceLib/Global.cs (all constants/tag names/URL templates), ServiceLib/Handler/ConfigHandler.cs (config + profile persistence), ServiceLib/Manager/AppManager.cs (singleton state + SQLite query helpers), and ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs (node resolution pipeline).
- Conventions: managers are 'public sealed class X { private static readonly Lazy<X> _instance = new(() => new()); public static X Instance => _instance.Value; }'; handlers are static classes; string checks use IsNullOrEmpty/IsNotEmpty/TrimEx extension methods; user-facing text must come from ResUI.* generated from resx.
- ReactiveUI conventions: ViewModels derive from MyReactiveObject, commands via ReactiveCommand.CreateFromTask, properties via [Reactive] partial properties from ReactiveUI.SourceGenerators, and UI-marshaling via RxSchedulers.MainThreadScheduler.Schedule or Signal.FromAsync(...).SubscribeOn(...).ToTask().
- Never persist mutable config references into CoreConfigContext — it is an immutable record snapshot so the generated config and the process launch mode (e.g. sudo/TUN decision in ShouldRunAsSudo) agree even if settings change mid-reload; clone via JsonUtils.DeepCopy before mutating.
- ProfileItem carries legacy flat fields plus JSON 'extra' blobs accessed via GetProtocolExtra/SetProtocolExtra/GetTransportExtra/SetTransportExtra; new fields must respect the ConfigVersion migration ladder (v2->v3 moved protocol extras, v3->v4 moved transport extras) — add a new migration step rather than reparsing inline.
- Build commands: 'dotnet build v2rayN/v2rayN.slnx' or per-project; WPF project targets net10.0-windows10.0.19041.0 and needs Windows or EnableWindowsTargeting; Desktop needs 'git submodule update --init --recursive' first or GlobalHotKeys.csproj is missing; tests: 'dotnet test v2rayN/ServiceLib.Tests -c Release'.
- When changing share-URI parse/export, update both directions (Fmt Resolve + ToUri) and add a factory in FmtHandlerTests.ShareProfileFactories — the suite asserts parity with Global.ProtocolShares so a one-sided change fails CI.
- Safe edit boundaries: new ViewModels/services in ServiceLib; per-shell code limited to Views, Converters, platform utils (WindowsUtils, MacAppUtils) and Managers like HotkeyManager/WindowDialog; do not add UI dependencies to ServiceLib (it must stay runnable by the test project on Linux CI). (inferred)
- Known traps: generated config filenames are constants (config.json, configPre.json, configTest{0}.json) under binConfigs — TaskManager deletes files matching 'Test' older than 1h there; ports are derived as socksPort + (int)EInboundProtocol; sing-box updates are version-locked at <=1.14.x.
- Core exe argument strings are string.Format templates where {0} is the config path (AbsolutePath flag controls quoting via GetBinConfigPath); keep the format-string contract when adding cores and provide env vars via CoreInfo.Environment (e.g. XRAY_LOCATION_ASSET).
- Logging goes through Logging.SaveLog (NLog, guiLogs/) gated by config.GuiItem.EnableLog set in InitComponents; updateFunc callbacks carry (notify:bool, msg) where notify=true means surface a snackbar via NoticeManager.Enqueue.
- Do not call ViewModel constructors expecting side effects in design mode: MainWindowViewModel.Init short-circuits on DesignMode, and design-time data lives in v2rayN.Desktop/DesignData; both shells share the same ServiceLib ViewModels so VM changes must remain toolkit-agnostic (use WindowDialog/Interactions, not dialogs).