Update to 0.3.1

This commit is contained in:
wsycarlos 2026-04-28 19:57:30 +08:00
parent 93d16fb3da
commit d0ec488a30
28 changed files with 905 additions and 7 deletions

View File

@ -7,12 +7,31 @@ namespace Byway.Quality
public class BywaySystemMemoryRuleMatcher : SystemMemoryRuleMatcherBase<BywayQualityLevel> { }
public class BywayMaliGenerationRuleMatcher : MaliGenerationRuleMatcherBase<BywayQualityLevel> { }
public class BywayEmulatorDetectionRuleMatcher : EmulatorDetectionRuleMatcherBase<BywayQualityLevel> { }
public class BywayGraphicsApiRuleMatcher : GraphicsApiRuleMatcherBase<BywayQualityLevel> { }
public class BywaySocNameRuleMatcher : SocNameRuleMatcherBase<BywayQualityLevel> { }
public class BywayAndroidApiLevelRuleMatcher : AndroidApiLevelRuleMatcherBase<BywayQualityLevel> { }
public class BywayScreenResolutionRuleMatcher : ScreenResolutionRuleMatcherBase<BywayQualityLevel> { }
public class BywayCompositeAndRuleMatcher : CompositeAndRuleMatcherBase<BywayQualityLevel> { }
public class BywayCompositeOrRuleMatcher : CompositeOrRuleMatcherBase<BywayQualityLevel> { }
public class BywayDeviceNameContainsRuleMatcher : DeviceNameContainsRuleMatcherBase<BywayQualityLevel> { }
public enum BywayQualityLevel
{
Lowest,
VeryLow,
Low,
Medium,
High,
Highest
VeryHigh,
Ultra
}
}
}

View File

@ -0,0 +1,16 @@
namespace Byway.Quality
{
/// <summary>
/// Graphics API category, used by <c>GraphicsApiRuleMatcherBase</c> to gate quality on the actual
/// rendering backend selected by Unity at runtime (Vulkan vs OpenGLES3 vs OpenGLES2).
/// </summary>
public enum GraphicsApi
{
/// <summary>Match any graphics API. Effectively skips the API check.</summary>
Any = 0,
Vulkan = 1,
OpenGLES3 = 2,
OpenGLES2 = 3
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b0e8c2f4d3a5758d9c0f1a2e3b4c5d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -8,6 +8,8 @@ namespace Byway.Quality
{
// Build.VERSION_CODES.S (Android 12)
private const int ANDROID_VERSION_CODES_S = 31;
// Build.VERSION_CODES.Q (Android 10) — getCurrentThermalStatus available
private const int ANDROID_VERSION_CODES_Q = 29;
private static int _sdkVersion;
@ -30,6 +32,17 @@ namespace Byway.Quality
_ => (GpuMinorSeries.Unknown, 0)
};
stats.SocName = GetSocName();
stats.AndroidSdkInt = GetAndroidSdkVersion();
stats.BuildManufacturer = GetBuildString("MANUFACTURER");
stats.BuildBrand = GetBuildString("BRAND");
stats.BuildModel = GetBuildString("MODEL");
stats.BuildHardware = GetBuildString("HARDWARE");
stats.BuildProduct = GetBuildString("PRODUCT");
stats.BuildSocManufacturer = GetBuildSocManufacturer();
stats.MaliGeneration = DeriveMaliGeneration(stats.GpuMajorSeries, stats.GpuMinorSeries, stats.GpuSeriesNumber, gpuName);
stats.IsEmulator = DetectEmulator(stats);
stats.ThermalStatus = GetCurrentThermalStatus();
}
public static GpuMajorSeries ParseGpuMajorSeries(string gpuName)
@ -175,7 +188,159 @@ namespace Byway.Quality
}
}
private static int GetAndroidSdkVersion()
// ---------------------------------------------------------------------
// v0.3.0 additions
// ---------------------------------------------------------------------
public static string GetBuildSocManufacturer()
{
if (GetAndroidSdkVersion() < ANDROID_VERSION_CODES_S)
return "";
try
{
using var buildClass = new AndroidJavaClass("android.os.Build");
var socManufacturer = buildClass.GetStatic<string>("SOC_MANUFACTURER");
return socManufacturer ?? "";
}
catch (Exception e)
{
Debug.LogException(e);
return "";
}
}
private static string GetBuildString(string fieldName)
{
#if UNITY_EDITOR
if (Application.isEditor)
return "";
#endif
#if UNITY_ANDROID
try
{
using var buildClass = new AndroidJavaClass("android.os.Build");
var value = buildClass.GetStatic<string>(fieldName);
return value ?? "";
}
catch (Exception e)
{
Debug.LogException(e);
return "";
}
#else
return "";
#endif
}
public static MaliGeneration DeriveMaliGeneration(GpuMajorSeries major, GpuMinorSeries minor, int seriesNumber, string gpuName)
{
if (major == GpuMajorSeries.Immortalis)
return MaliGeneration.Immortalis;
if (major != GpuMajorSeries.Mali)
return MaliGeneration.Unknown;
// Utgard: "Mali-400/450/470" — minor parsed as Mali (no letter)
if (minor == GpuMinorSeries.Mali)
return MaliGeneration.Utgard;
// Midgard: Mali-T6xx/T7xx/T8xx
if (minor == GpuMinorSeries.MaliT)
return MaliGeneration.Midgard;
if (minor != GpuMinorSeries.MaliG)
return MaliGeneration.Unknown;
// Mali-G family disambiguation by series number band:
// G31/G52/G57/G68 → BifrostLegacy (low-end Bifrost+early Valhall, all <70)
// G71/G72/G76/G77/G78 → Valhall1 (legacy Valhall, 70-99)
// G310/G510/G610/G615/G710/G715/G720/G725 → Valhall2 (modern, >=100)
if (seriesNumber >= 100)
return MaliGeneration.Valhall2;
if (seriesNumber >= 70)
return MaliGeneration.Valhall1;
if (seriesNumber > 0)
return MaliGeneration.BifrostLegacy;
return MaliGeneration.Unknown;
}
public static bool DetectEmulator(HardwareStats stats)
{
// x86/x86_64 CPU on Android is the strongest single signal — no shipping
// Android phone uses x86 in 2024+. Check first.
var cpu = stats.ProcessorType ?? "";
if (cpu.IndexOf("x86", StringComparison.OrdinalIgnoreCase) >= 0)
return true;
var hardware = (stats.BuildHardware ?? "").ToLowerInvariant();
if (hardware == "goldfish" || hardware == "ranchu" ||
hardware.Contains("vbox") || hardware.Contains("ttvm") ||
hardware.Contains("nox") || hardware == "intel")
return true;
var manufacturer = stats.BuildManufacturer ?? "";
if (manufacturer.Equals("unknown", StringComparison.OrdinalIgnoreCase) ||
manufacturer.Equals("Genymotion", StringComparison.OrdinalIgnoreCase) ||
manufacturer.Equals("BlueStacks", StringComparison.OrdinalIgnoreCase) ||
manufacturer.Equals("ldplayer", StringComparison.OrdinalIgnoreCase) ||
manufacturer.Equals("Netease", StringComparison.OrdinalIgnoreCase))
return true;
var product = (stats.BuildProduct ?? "").ToLowerInvariant();
if (product.StartsWith("sdk_", StringComparison.Ordinal) ||
product.Contains("vbox") || product.Contains("ldplayer") ||
product == "nox" || product.Contains("ttvm"))
return true;
var model = stats.BuildModel ?? "";
if (model.IndexOf("sdk", StringComparison.OrdinalIgnoreCase) >= 0 ||
model.IndexOf("Emulator", StringComparison.OrdinalIgnoreCase) >= 0 ||
model.IndexOf("Android SDK built for", StringComparison.OrdinalIgnoreCase) >= 0 ||
model.IndexOf("BlueStacks", StringComparison.OrdinalIgnoreCase) >= 0 ||
model.IndexOf("MuMu", StringComparison.Ordinal) >= 0)
return true;
return false;
}
public static int GetCurrentThermalStatus()
{
#if UNITY_EDITOR
if (Application.isEditor)
return -1;
#endif
#if UNITY_ANDROID
if (GetAndroidSdkVersion() < ANDROID_VERSION_CODES_Q)
return -1;
try
{
using var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
using var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
if (activity == null)
return -1;
using var pmObj = activity.Call<AndroidJavaObject>("getSystemService", "power");
if (pmObj == null)
return -1;
return pmObj.Call<int>("getCurrentThermalStatus");
}
catch (Exception e)
{
Debug.LogException(e);
return -1;
}
#else
return -1;
#endif
}
// ---------------------------------------------------------------------
public static int GetAndroidSdkVersion()
{
#if UNITY_EDITOR
if (Application.isEditor)

View File

@ -1,4 +1,5 @@
using UnityEngine;
using UnityEngine.Rendering;
namespace Byway.Quality
{
@ -53,6 +54,64 @@ namespace Byway.Quality
/// </summary>
public int SystemMemorySizeMb { get; internal set; }
// ---------------------------------------------------------------------
// v0.3.0 additions (additive only; existing matchers/assets unaffected).
// ---------------------------------------------------------------------
/// <summary>Android <c>Build$VERSION.SDK_INT</c>; 0 on non-Android / editor.</summary>
public int AndroidSdkInt { get; internal set; }
/// <summary>True when the device is detected as an Android emulator (BlueStacks/MuMu/LDPlayer/AVD/etc.). False on iOS and physical Android.</summary>
public bool IsEmulator { get; internal set; }
/// <summary><see cref="UnityEngine.SystemInfo.graphicsDeviceType" />.</summary>
public GraphicsDeviceType GraphicsDeviceType { get; internal set; }
/// <summary><see cref="UnityEngine.SystemInfo.graphicsShaderLevel" />.</summary>
public int GraphicsShaderLevel { get; internal set; }
/// <summary><see cref="UnityEngine.Screen.width" /> at hardware-stats capture time.</summary>
public int ScreenWidth { get; internal set; }
/// <summary><see cref="UnityEngine.Screen.height" /> at hardware-stats capture time.</summary>
public int ScreenHeight { get; internal set; }
/// <summary><see cref="UnityEngine.Screen.dpi" /> at hardware-stats capture time. May be 0 if unknown.</summary>
public float ScreenDpi { get; internal set; }
/// <summary><see cref="UnityEngine.SystemInfo.processorCount" />.</summary>
public int ProcessorCount { get; internal set; }
/// <summary><see cref="UnityEngine.SystemInfo.processorFrequency" /> in MHz.</summary>
public int ProcessorFrequencyMHz { get; internal set; }
/// <summary><see cref="UnityEngine.SystemInfo.processorType" />. Strong x86 emulator signal on Android.</summary>
public string ProcessorType { get; internal set; }
/// <summary>Android <c>Build.MANUFACTURER</c>; empty on non-Android.</summary>
public string BuildManufacturer { get; internal set; }
/// <summary>Android <c>Build.BRAND</c>; empty on non-Android.</summary>
public string BuildBrand { get; internal set; }
/// <summary>Android <c>Build.MODEL</c>; empty on non-Android. Distinct from <see cref="DeviceModel" /> which is Unity's composed string.</summary>
public string BuildModel { get; internal set; }
/// <summary>Android <c>Build.HARDWARE</c>; empty on non-Android. Useful emulator signal (<c>goldfish</c>/<c>ranchu</c>).</summary>
public string BuildHardware { get; internal set; }
/// <summary>Android <c>Build.PRODUCT</c>; empty on non-Android. Useful emulator signal (<c>sdk_*</c>/<c>vbox*</c>).</summary>
public string BuildProduct { get; internal set; }
/// <summary>Android <c>Build.SOC_MANUFACTURER</c> (API 31+); empty otherwise.</summary>
public string BuildSocManufacturer { get; internal set; }
/// <summary>Derived Mali architectural generation; <see cref="Quality.MaliGeneration.Unknown" /> on non-Mali / non-Android.</summary>
public MaliGeneration MaliGeneration { get; internal set; }
/// <summary>Android <c>PowerManager.getCurrentThermalStatus()</c> (API 29+); -1 if unsupported / not Android.</summary>
public int ThermalStatus { get; internal set; }
internal static HardwareStats CreateDefault()
{
return new HardwareStats
@ -64,7 +123,26 @@ namespace Byway.Quality
GpuMinorSeries = GpuMinorSeries.Unknown,
GpuSeriesNumber = 0,
SocName = "",
SystemMemorySizeMb = SystemInfo.systemMemorySize
SystemMemorySizeMb = SystemInfo.systemMemorySize,
AndroidSdkInt = 0,
IsEmulator = false,
GraphicsDeviceType = SystemInfo.graphicsDeviceType,
GraphicsShaderLevel = SystemInfo.graphicsShaderLevel,
ScreenWidth = Screen.width,
ScreenHeight = Screen.height,
ScreenDpi = Screen.dpi,
ProcessorCount = SystemInfo.processorCount,
ProcessorFrequencyMHz = SystemInfo.processorFrequency,
ProcessorType = SystemInfo.processorType,
BuildManufacturer = "",
BuildBrand = "",
BuildModel = "",
BuildHardware = "",
BuildProduct = "",
BuildSocManufacturer = "",
MaliGeneration = MaliGeneration.Unknown,
ThermalStatus = -1
};
}
}

View File

@ -15,6 +15,7 @@ namespace Byway.Quality
stats.GpuMajorSeries = GpuMajorSeries.Apple;
stats.GpuMinorSeries = ParseGpuMinorSeries(stats.GpuName);
stats.GpuSeriesNumber = ParseAppleGpuSeriesNumber(stats.GpuName);
stats.IsEmulator = false;
}
public static GpuMinorSeries ParseGpuMinorSeries(string gpuName)

View File

@ -0,0 +1,30 @@
namespace Byway.Quality
{
/// <summary>
/// Generation taxonomy for ARM Mali / Immortalis GPUs.
/// Used by <c>MaliGenerationRuleMatcherBase</c> to disambiguate the legacy/modern Mali numeric collision
/// (e.g. Mali-G77 vs Mali-G310 — both <c>MaliG</c>, but very different architectures).
/// </summary>
public enum MaliGeneration
{
Unknown = 0,
/// <summary>Mali-400 / 450 / 470 (no letter suffix).</summary>
Utgard = 1,
/// <summary>Mali-T6xx / T7xx / T8xx.</summary>
Midgard = 2,
/// <summary>Mali-G31 / G52 / G57 / G68 (low-end Bifrost / early Valhall band).</summary>
BifrostLegacy = 3,
/// <summary>Mali-G71 / G72 / G76 / G77 / G78 (Valhall first wave, legacy numbering &lt; 100).</summary>
Valhall1 = 4,
/// <summary>Mali-G310 / G510 / G610 / G615 / G710 / G715 / G720 / G725 (modern Valhall numbering &gt;= 100).</summary>
Valhall2 = 5,
/// <summary>ARM Immortalis-G715 / G720 / G725 / G925 (top-tier ray-tracing line).</summary>
Immortalis = 6
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4f8c6a9d2e1b4536b7a8d0e1c2f3a4b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7a4c3e9d2b1f4a5e8c6d0f2b3a4e5c7d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Byway.Quality
{
[Serializable]
public class AndroidApiLevelRuleMatcherBase<T> : RuleMatcherBase<T>
{
public Rule[] rules;
protected override IEnumerable<IMatcher<T>> Rules => rules;
[Serializable]
public sealed class Rule : IMatcher<T>
{
[Tooltip("Minimum Android SDK_INT (inclusive); 0 = no min")]
public int apiLevelMin;
[Tooltip("Maximum Android SDK_INT (inclusive); 0 = no max → treated as 999")]
public int apiLevelMax;
[Tooltip("Quality level for the device that match")]
public T qualityLevel;
public bool TryMatch(HardwareStats stats, out T matchedQualityLevel)
{
matchedQualityLevel = default;
if (stats.AndroidSdkInt <= 0)
return false;
var max = apiLevelMax <= 0 ? 999 : apiLevelMax;
if (stats.AndroidSdkInt < apiLevelMin || stats.AndroidSdkInt > max)
return false;
matchedQualityLevel = qualityLevel;
return true;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c0513c6d8e7f9a6cb1f2c3d4e5f60718
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Byway.Quality
{
[Serializable]
public class CompositeAndRuleMatcherBase<T> : RuleMatcherBase<T>
{
public Rule[] rules;
protected override IEnumerable<IMatcher<T>> Rules => rules;
[Serializable]
public sealed class Rule : IMatcher<T>
{
[Tooltip("All children must match for this rule to fire. Children's own quality levels are discarded.")]
[SerializeReference]
[SelectableSerializeReference]
public IMatcher[] children;
[Tooltip("Quality level returned when all children match")]
public T qualityLevel;
public bool TryMatch(HardwareStats stats, out T matchedQualityLevel)
{
matchedQualityLevel = default;
if (children == null || children.Length == 0)
return false;
foreach (var child in children)
{
if (child == null)
return false;
if (!child.TryMatch<T>(stats, out _))
return false;
}
matchedQualityLevel = qualityLevel;
return true;
}
}
}
[Serializable]
public class CompositeOrRuleMatcherBase<T> : RuleMatcherBase<T>
{
public Rule[] rules;
protected override IEnumerable<IMatcher<T>> Rules => rules;
[Serializable]
public sealed class Rule : IMatcher<T>
{
[Tooltip("Any one child matching causes this rule to fire. Children's own quality levels are discarded.")]
[SerializeReference]
[SelectableSerializeReference]
public IMatcher[] children;
[Tooltip("Quality level returned when any child matches")]
public T qualityLevel;
public bool TryMatch(HardwareStats stats, out T matchedQualityLevel)
{
matchedQualityLevel = default;
if (children == null || children.Length == 0)
return false;
foreach (var child in children)
{
if (child == null)
continue;
if (child.TryMatch<T>(stats, out _))
{
matchedQualityLevel = qualityLevel;
return true;
}
}
return false;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e2735e8fa09b1c8ed3f4e5f60718293a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Byway.Quality
{
[Serializable]
public class DeviceNameContainsRuleMatcherBase<T> : RuleMatcherBase<T>
{
public Rule[] rules;
protected override IEnumerable<IMatcher<T>> Rules => rules;
[Serializable]
public sealed class Rule : IMatcher<T>
{
[Tooltip("Pattern to compare against SystemInfo.deviceModel")]
public string deviceModelPattern;
[Tooltip("How to compare the pattern against deviceModel")]
public StringMatchMode matchMode;
[Tooltip("Quality level for the device that match")]
public T qualityLevel;
public bool TryMatch(HardwareStats stats, out T matchedQualityLevel)
{
matchedQualityLevel = default;
if (string.IsNullOrEmpty(stats.DeviceModel))
return false;
if (string.IsNullOrEmpty(deviceModelPattern))
return false;
if (!StringMatchModeUtility.IsMatch(stats.DeviceModel, deviceModelPattern, matchMode))
return false;
matchedQualityLevel = qualityLevel;
return true;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3846f90b1ac2d9fe4f5061728293a4b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Byway.Quality
{
[Serializable]
public class EmulatorDetectionRuleMatcherBase<T> : RuleMatcherBase<T>
{
public Rule[] rules;
protected override IEnumerable<IMatcher<T>> Rules => rules;
[Serializable]
public sealed class Rule : IMatcher<T>
{
[Tooltip("true: match if emulator detected; false: match if NOT emulator")]
public bool matchEmulator;
[Tooltip("Quality level for the device that match")]
public T qualityLevel;
public bool TryMatch(HardwareStats stats, out T matchedQualityLevel)
{
matchedQualityLevel = default;
if (stats.IsEmulator != matchEmulator)
return false;
matchedQualityLevel = qualityLevel;
return true;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9d2e0f3a5b4c6749e8d9f0a1b2c3d4e5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace Byway.Quality
{
[Serializable]
public class GraphicsApiRuleMatcherBase<T> : RuleMatcherBase<T>
{
public Rule[] rules;
protected override IEnumerable<IMatcher<T>> Rules => rules;
[Serializable]
public sealed class Rule : IMatcher<T>
{
[Tooltip("Required graphics API. Any = skip API check.")]
public GraphicsApi requiredApi;
[Tooltip("Minimum SystemInfo.graphicsShaderLevel (inclusive); 0 = no min")]
public int requiredShaderLevelMin;
[Tooltip("Quality level for the device that match")]
public T qualityLevel;
public bool TryMatch(HardwareStats stats, out T matchedQualityLevel)
{
matchedQualityLevel = default;
if (requiredApi != GraphicsApi.Any && !ApiMatches(requiredApi, stats.GraphicsDeviceType))
return false;
if (stats.GraphicsShaderLevel < requiredShaderLevelMin)
return false;
matchedQualityLevel = qualityLevel;
return true;
}
private static bool ApiMatches(GraphicsApi required, GraphicsDeviceType actual)
{
return required switch
{
GraphicsApi.Vulkan => actual == GraphicsDeviceType.Vulkan,
GraphicsApi.OpenGLES3 => actual == GraphicsDeviceType.OpenGLES3,
GraphicsApi.OpenGLES2 => actual == GraphicsDeviceType.OpenGLES2,
_ => true
};
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ae3f1a4b6c5d784af9e0a1b2c3d4e5f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Byway.Quality
{
[Serializable]
public class MaliGenerationRuleMatcherBase<T> : RuleMatcherBase<T>
{
public Rule[] rules;
protected override IEnumerable<IMatcher<T>> Rules => rules;
[Serializable]
public sealed class Rule : IMatcher<T>
{
[Tooltip("Mali architectural generation that match")]
public MaliGeneration generation;
[Tooltip("Minimum of GPU series number that match (inclusive); 0 = no min")]
public int gpuSeriesNumberMin;
[Tooltip("Maximum of GPU series number that match (inclusive); 0 = no max → treated as int.MaxValue")]
public int gpuSeriesNumberMax;
[Tooltip("Quality level for the device that match")]
public T qualityLevel;
public bool TryMatch(HardwareStats stats, out T matchedQualityLevel)
{
matchedQualityLevel = default;
if (stats.MaliGeneration == MaliGeneration.Unknown)
return false;
if (stats.MaliGeneration != generation)
return false;
var max = gpuSeriesNumberMax <= 0 ? int.MaxValue : gpuSeriesNumberMax;
if (stats.GpuSeriesNumber < gpuSeriesNumberMin || stats.GpuSeriesNumber > max)
return false;
matchedQualityLevel = qualityLevel;
return true;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c1d9e2f4a3b5648d7c8e9f0a1b2c3d4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Byway.Quality
{
[Serializable]
public class ScreenResolutionRuleMatcherBase<T> : RuleMatcherBase<T>
{
public Rule[] rules;
protected override IEnumerable<IMatcher<T>> Rules => rules;
[Serializable]
public sealed class Rule : IMatcher<T>
{
[Tooltip("Minimum total pixel count (width*height), inclusive; 0 = no min")]
public int pixelCountMin;
[Tooltip("Maximum total pixel count (width*height), inclusive; 0 = no max → unbounded")]
public int pixelCountMax;
[Tooltip("Minimum DPI, inclusive; 0 = no min (DPI also skipped if Screen.dpi reports 0)")]
public float dpiMin;
[Tooltip("Maximum DPI, inclusive; 0 = no max → unbounded")]
public float dpiMax;
[Tooltip("Quality level for the device that match")]
public T qualityLevel;
public bool TryMatch(HardwareStats stats, out T matchedQualityLevel)
{
matchedQualityLevel = default;
var pixelCount = stats.ScreenWidth * stats.ScreenHeight;
var pxMax = pixelCountMax <= 0 ? int.MaxValue : pixelCountMax;
if (pixelCount < pixelCountMin || pixelCount > pxMax)
return false;
if (stats.ScreenDpi > 0f)
{
var dMax = dpiMax <= 0f ? float.MaxValue : dpiMax;
if (stats.ScreenDpi < dpiMin || stats.ScreenDpi > dMax)
return false;
}
else if (dpiMin > 0f)
{
return false;
}
matchedQualityLevel = qualityLevel;
return true;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1624d7e9f8a0b7dc2f3d4e5f6071829
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
namespace Byway.Quality
{
[Serializable]
public class SocNameRuleMatcherBase<T> : RuleMatcherBase<T>
{
public Rule[] rules;
protected override IEnumerable<IMatcher<T>> Rules => rules;
[Serializable]
public sealed class Rule : IMatcher<T>
{
[Tooltip("Pattern to compare against HardwareStats.SocName (Build.SOC_MODEL on Android 12+)")]
public string socNamePattern;
[Tooltip("How to compare the pattern against SocName")]
public StringMatchMode matchMode;
[Tooltip("Quality level for the device that match")]
public T qualityLevel;
public bool TryMatch(HardwareStats stats, out T matchedQualityLevel)
{
matchedQualityLevel = default;
if (string.IsNullOrEmpty(stats.SocName))
return false;
if (string.IsNullOrEmpty(socNamePattern))
return false;
if (!StringMatchModeUtility.IsMatch(stats.SocName, socNamePattern, matchMode))
return false;
matchedQualityLevel = qualityLevel;
return true;
}
}
}
internal static class StringMatchModeUtility
{
private static bool _regexFailureLogged;
public static bool IsMatch(string input, string pattern, StringMatchMode mode)
{
return mode switch
{
StringMatchMode.Exact => input.Equals(pattern, StringComparison.Ordinal),
StringMatchMode.StartsWith => input.StartsWith(pattern, StringComparison.Ordinal),
StringMatchMode.Contains => input.IndexOf(pattern, StringComparison.Ordinal) >= 0,
StringMatchMode.Regex => TryRegex(input, pattern),
_ => false
};
}
private static bool TryRegex(string input, string pattern)
{
try
{
return Regex.IsMatch(input, pattern);
}
catch (ArgumentException e)
{
if (!_regexFailureLogged)
{
_regexFailureLogged = true;
Debug.LogWarning($"[QualityTuner] invalid regex pattern '{pattern}': {e.Message}");
}
return false;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bf402b5c7d6e895ba0f1b2c3d4e5f607
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,20 @@
namespace Byway.Quality
{
/// <summary>
/// String matching mode shared by <c>SocNameRuleMatcherBase</c> and <c>DeviceNameContainsRuleMatcherBase</c>.
/// </summary>
public enum StringMatchMode
{
/// <summary>Exact ordinal match.</summary>
Exact = 0,
/// <summary>Ordinal StartsWith match.</summary>
StartsWith = 1,
/// <summary>Ordinal Contains match.</summary>
Contains = 2,
/// <summary>.NET regex match. Invalid patterns return false (logged once).</summary>
Regex = 3
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a9d7b1e3c2f4647c8b9e0f1d2a3b4c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,11 +1,11 @@
{
"name": "com.bywaystudios.qualitytuner",
"displayName": "Quality Tuner",
"version": "0.1.0",
"version": "0.3.1",
"description": "Tools to support deciding quality level by hardware spec of mobile devices.",
"repository": {
"url": "git@gitea.bywaystudios.com:wangshiyao/QualitySettingsPackage.git",
"type": "git",
"revision": null
}
}
}