From 493f35d842972af9e2c5e8cd404fa4a4856b981c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=E9=B8=BD?= <43724908+Akarinnnnn@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:59:53 +0800 Subject: [PATCH 1/2] Add DLL version checker on Windows Improve performance of NativeMethods.PtrToStringUTF8(), by using spans to vectorize NUL('\0') scanning. --- CodeGen/templates/header.txt | 2 +- Standalone3.0/CONTRIBUTING.md | 289 ++++++++++++++++++ Standalone3.0/ICallbackIdentity.cs | 13 - Standalone3.0/Steamworks.NET.csproj | 9 +- Standalone3.0/anycpu/ICallbackIdentity.cs | 7 + Standalone3.0/anycpu/InteropHelp.Unsafe.cs | 19 ++ .../{ => anycpu}/SteamMarshallerTable.cs | 6 +- .../{ => anycpu}/SteamMarshallerTable.g.cs | 0 Standalone3.0/anycpu/VersionChecker.cs | 80 +++++ .../autogen/NativeMethods.AnyCPU.cs | 7 +- .../Runtime/InteropHelp.cs | 165 +++++----- .../Runtime/Version.cs | 8 +- 12 files changed, 500 insertions(+), 105 deletions(-) create mode 100644 Standalone3.0/CONTRIBUTING.md delete mode 100644 Standalone3.0/ICallbackIdentity.cs create mode 100644 Standalone3.0/anycpu/ICallbackIdentity.cs create mode 100644 Standalone3.0/anycpu/InteropHelp.Unsafe.cs rename Standalone3.0/{ => anycpu}/SteamMarshallerTable.cs (83%) rename Standalone3.0/{ => anycpu}/SteamMarshallerTable.g.cs (100%) create mode 100644 Standalone3.0/anycpu/VersionChecker.cs rename Standalone3.0/{ => anycpu}/autogen/NativeMethods.AnyCPU.cs (88%) diff --git a/CodeGen/templates/header.txt b/CodeGen/templates/header.txt index bd364c77..e32ae492 100644 --- a/CodeGen/templates/header.txt +++ b/CodeGen/templates/header.txt @@ -1,5 +1,5 @@ // This file is provided under The MIT License as part of Steamworks.NET. -// Copyright (c) 2013-2022 Riley Labrecque +// Copyright (c) 2013-2026 Riley Labrecque // Please see the included LICENSE.txt for additional information. // This file is automatically generated. diff --git a/Standalone3.0/CONTRIBUTING.md b/Standalone3.0/CONTRIBUTING.md new file mode 100644 index 00000000..84ae8dcd --- /dev/null +++ b/Standalone3.0/CONTRIBUTING.md @@ -0,0 +1,289 @@ +# Steamworks.NET.AnyCPU Contributing Guidelines + +## Table of Contents +- [Introduction](#introduction) +- [Getting Started](#getting-started) +- [Project Structure](#project-structure) +- [Development Workflow](#development-workflow) +- [Code Generation](#code-generation) +- [Testing](#testing) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Community](#community) + +## Introduction + +### Project Overview +Steamworks.NET.AnyCPU is a fork of Steamworks.NET that provides AnyCPU support, allowing the library to work seamlessly across different processor architectures (x86, x64, ARM, etc.). + +### Relations between `Akarinnnnn/Steamworks.NET.AnyCPU` and `rlabrecque/Steamworks.NET` + +Steamworks.NET.AnyCPU originated as a patch for Steamworks.NET. However, due to its extensive modifications to `CodeGen`, the resulting commit volume became too large, far exceeding that of the branch that upgraded the target SDK from 1.62 to 1.63. Therefore, in the short term, AnyCPU will remain as an independent fork. + +**Main Contributors:** +- @Akarinnnnn Any CPU rewrite +- @Chicken-Bones ~~Vibe code the guy above~~AnyCPU idea initiator, DLL resolve hook and conditional marshal prototype provider +- @ryan-linehan CI/CD in NuGet packaging + +**Credits** +- @rlabrecque + +## Getting Started + +### Prerequisites +- **Python 3.13** (required for CodeGen) +- **.NET SDK** (version specified in project files) +- **Git** +- **Visual Studio 2022** or **Visual Studio Code** (using VSC for Python, VS for C# is recommended) +- **Rider** and **PyCharm** (if you prefer JetBrains tools, but currently `.gitignore` isn't ready for them) + +### Setting Up Development Environment +1. Clone the repository: + ```bash + git clone https://github.com/Akarinnnnn/Steamworks.NET.AnyCPU.git + cd Steamworks.NET.AnyCPU + ``` + +2. Verify Python installation: + ```bash + python --version + # Should output: Python 3.13.x + # or python3 if your python command is bound to 2.x + ``` + +3. Restore .NET dependencies: + ```bash + dotnet restore Standalone3.0/Steamworks.NET.sln + ``` + +4. Build the project: + ```bash + dotnet build Standalone3.0/Steamworks.NET.sln + ``` + +5. Create your `Steamworks.NET.AnyCPU` package + ```bash + dotnet pack --version $SEMVER Standalone3.0/Steamworks.NET.sln + ``` + +## Project Structure + +### 1. Solution View of `Steamworks.NET` +- **`src/`**: Shared source between Steamworks.NET.AnyCPU and Unity Package Manager. Linked from `$REPOSITORY_ROOT/com.rlabrecque.steamworks.net/Runtime` + - **Important**: Any edits without `#if STEAMWORKS_ANYCPU` should be carefully considered as they will affect Unity, which targeting to `netstandard2.0` or `netstandard2.1` depends on Unity version. + - `src/autogen`: First type of generated source files. Generator is located at `$REPOSITORY_ROOT/CodeGen`, @Akarinnnnn added AnyCPU modifications. + - `src/types`: Second type of generated source files. Generator is located at `$REPOSITORY_ROOT/CodeGen`, mostly done by original author @rlabrecque. This type is mostly from templates. + +- **`native/`**: Steamworks native dependencies for supported platforms. Linked from `$REPOSITORY_ROOT/com.rlabrecque.steamworks.net/Plugin` + - **Important**: These are provided by Valve, bundled with the current version, and should not be edited. + - If you need to update them for upgrading target SDK version, ask the whole Steamworks.NET community. + +- **`anycpu/`**: The AnyCPU specific source code. This is where you should make your edits for the AnyCPU version of Steamworks.NET. + - The sub-folder structure is mirrored with `src/`. + - **Important**: Files prefixed with `*.g.cs` are **generated** and should not be edited directly. If you need to edit them, edit the Generator in `$REPOSITORY_ROOT/CodeGen` and regenerate the code. + +### 2. `$REPOSITORY_ROOT/CodeGen` +Python project, C# binding generator(Binder). This repository uses Python 3.13 to run the code. Previously contained the submodule `CodeGen/SteamworksParser/`, later integrated into this repository via `git subtree`. + +- **`templates/`**: Stores templates for generated files, including generated file headers, `Steamworks.NET/src/types`, and partial `*.g.cs` files. +- **`SteamworksParser/`**: Parser that scans Steamworks SDK C++ header files line by line, scanning types, fields, interface member functions, and type aliases. @Akarinnnnn added type size analysis, field size analysis, object layout analysis, and structure cross-platform compatibility analysis features on top of the original. Also includes some debugging scripts. +- **`Steam/`**: Stores Steamworks SDK C++ header files for the corresponding version, used for binding generation. +- **`src/`**: Binding generator. + +## Development Workflow + +### Branch Strategy +- Stable release branch doesn't exist currently, production-ready code is hold in tags. +- **`anycpu-2`**: Development branch for upcoming features +- **Feature branches**: `feature/description` (e.g., `feature/add-new-interface`) +- **Bug fix branches**: `fix/issue-description` (e.g., `fix/memory-leak-issue123`) +- **Hotfix branches**: `hotfix/critical-issue` (for urgent fixes to production) + +### Commit Guidelines +- Use descriptive commit messages in the present tense +- Reference issue numbers when applicable (e.g., `Fixes #123`) + +### Coding Standards +- Enable `.editorconfig` support to your editor, most coding standards are defined there +- Use meaningful variable and method names +- Add XML documentation comments for public APIs +- Keep methods focused and concise (single responsibility principle) + +## Code Generation + +### When to Regenerate Code +You should regenerate code when: +1. Steamworks SDK headers are updated +2. Templates are modified +3. Binder logic is changed +4. New types need to be added +5. Fixing issues in generated code + +### Regeneration Process +1. Navigate to `CodeGen/` directory: + ```bash + cd CodeGen + ``` + +2. Run the generator script: + ```bash + python Steamworks.NET_CodeGen.py + ``` + +3. Verify generated files: + - Check for compilation errors + - Verify type mappings are correct + - Ensure no breaking changes + +4. Test the changes: + In another terminal do: + ```bash + cd Standalone3.0 + dotnet build + ``` + +### Common Issues and Solutions +- **Missing Python modules**: Run `pip install -r requirements.txt` (if available) +- **Parser errors**: Navigate to corresponding parse logic in `SteamworksParser.py` and fix it +- **Other binder errors**: Navigate to corresponding bind logic in `src/.py` and fix it +- **Generation failures**: Review template files for syntax errors + +## Testing + +### Testing Requirements +- All changes should be tested before submission +- Verify generated code compiles correctly +- Test with sample applications when possible +- Ensure cross-platform compatibility (x86, x64, ARM) if possible +- Run existing unit tests (if available) + +### Testing Strategy +1. **Compilation Testing**: Ensure the project builds without errors +2. **Runtime Testing**: Test basic Steamworks functionality, `SteamAPI.Init()` and `SteamAPI.Shutdown()` +3. **Cross-Platform Testing**: Verify on different architectures, do your best +4. **Integration Testing**: Test with Unity projects (if applicable) + +### Creating Tests +When adding new features or fixing bugs: +1. When creating tests, add documentation describing the expected behavior +2. Include setup instructions for the test environment + +## Pull Request Guidelines + +### Before Submitting a PR +1. Ensure your code follows the project's coding standards, use `dotnet format` or format hotkey first +2. Run any existing tests +3. Update documentation if needed +4. Rebase onto the latest target branch +5. Squash commits when appropriate +6. Ensure commit messages are clear and descriptive + +### PR Description Template +```markdown +## Description +Brief description of the changes. Explain what problem this solves or what feature this adds. + +## Related Issues +Fixes #issue_number +Closes #issue_number +Related to #issue_number + +## Changes Made +- List specific changes made +- Include technical details if relevant +- Mention any breaking changes + +## Testing +Describe how you tested the changes: +- [ ] Compilation passes +- [ ] Basic functionality works +- [ ] (Optional, if possible) Cross-platform compatibility verified +- [ ] No regressions in existing features + +## Checklist +- [ ] Code follows project standards +- [ ] Tests pass (or N/A if no tests exist) +- [ ] Documentation updated +- [ ] No breaking changes (or clearly documented if breaking) +- [ ] PR targets the correct branch +``` + +### PR Review Process +1. **Initial Review**: Maintainers are busy to fight for life, but your PR will be reviewed sometime +2. **Feedback**: Address any feedback or requested changes +3. **Approval**: Once approved, the PR will be merged +4. **Merge Strategy**: Plain Merge for history clear + +## Community + +### Communication Channels +- **GitHub Issues**: For bug reports and feature requests +- **GitHub Discussions**: For questions and general discussions +- **Pull Requests**: For code contributions +- **Discord Chat**: We don't have that yet + +### How to Report Issues +When reporting issues, please include: +1. Clear description of the problem +2. Steps to reproduce +3. Expected vs actual behavior +4. Environment details (OS, CPU architecture, .NET version, Unity version if applicable) +5. Relevant code snippets or error messages + +### How to Request Features +When requesting features: +1. Describe the use case +2. Explain the benefits +3. Provide examples of how it would be used +4. Consider if it aligns with project goals + +### Code of Conduct +Please be respectful and constructive in all interactions. We follow these principles: +- Be welcoming and inclusive +- Be respectful of different viewpoints +- Focus on what is best for the community +- Show empathy towards other community members + +Violations of the code of conduct should be reported to the maintainers. + +### Recognition +Contributors will be: +- Acknowledged in release notes for significant contributions +- Given credit in relevant documentation + +--- + +## Additional Resources + +### Documentation +- [Steamworks Documentation](https://partner.steamgames.com/doc/sdk) +- [Original Steamworks.NET Documentation](https://steamworks.github.io/) + +### Useful Commands +```bash +# Build the project +dotnet build Standalone3.0/Steamworks.NET.sln + +# Clean build artifacts +dotnet clean Standalone3.0/Steamworks.NET.sln + +# Run code generator +cd CodeGen +python Steamworks.NET_CodeGen.py + +# Check for compilation errors +dotnet build --no-restore --verbosity minimal + +# Create NuGet package yourself +cd Standalone3.0 +dotnet pack --version $SEMVER -o path/to/your/test/packages/registry +``` + +### Troubleshooting +- **Build failures**: Check .NET SDK version compatibility +- **Missing dependencies**: `dotnet restore` and `python -m pip install` +- **Python errors**: Verify Python 3.13 is installed correctly + +--- + +*Thank you for contributing to Steamworks.NET.AnyCPU! Your contributions help make this project better for everyone.* +Heart from Cyberstan, no AI was hurt during the writing process🤖. \ No newline at end of file diff --git a/Standalone3.0/ICallbackIdentity.cs b/Standalone3.0/ICallbackIdentity.cs deleted file mode 100644 index 31e520dd..00000000 --- a/Standalone3.0/ICallbackIdentity.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Steamworks -{ - internal interface ICallbackIdentity - { - public static int CallbackIdentity { get; } - } -} diff --git a/Standalone3.0/Steamworks.NET.csproj b/Standalone3.0/Steamworks.NET.csproj index 2a4db5c5..cbe8b9a4 100644 --- a/Standalone3.0/Steamworks.NET.csproj +++ b/Standalone3.0/Steamworks.NET.csproj @@ -4,7 +4,10 @@ net8.0 Steamworks git - STEAMWORKS_LIN_OSX;STEAMWORKS_X64;STEAMWORKS_ANYCPU + STEAMWORKS_LIN_OSX;STEAMWORKS_X64;STEAMWORKS_ANYCPU;STEAMWORKS_UNSAFE_ENABLED + disable + + true @@ -41,10 +44,10 @@ runtimes/linux-x64/native - + runtimes/android-arm64/native - + runtimes/linux-arm64/native diff --git a/Standalone3.0/anycpu/ICallbackIdentity.cs b/Standalone3.0/anycpu/ICallbackIdentity.cs new file mode 100644 index 00000000..39c86d5e --- /dev/null +++ b/Standalone3.0/anycpu/ICallbackIdentity.cs @@ -0,0 +1,7 @@ +namespace Steamworks +{ + internal interface ICallbackIdentity + { + public static int CallbackIdentity { get; } + } +} diff --git a/Standalone3.0/anycpu/InteropHelp.Unsafe.cs b/Standalone3.0/anycpu/InteropHelp.Unsafe.cs new file mode 100644 index 00000000..6077145e --- /dev/null +++ b/Standalone3.0/anycpu/InteropHelp.Unsafe.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Steamworks +{ + public static partial class InteropHelp + { + public static unsafe string PtrToStringUTF8(IntPtr nativeUtf8) { + byte* cStr = (byte*)nativeUtf8; + ReadOnlySpan spanUnchecked = new(cStr, int.MaxValue); + ReadOnlySpan stringSpan = spanUnchecked.Slice(0, spanUnchecked.IndexOf((byte)0)); + + return Encoding.UTF8.GetString(stringSpan); + } + } +} diff --git a/Standalone3.0/SteamMarshallerTable.cs b/Standalone3.0/anycpu/SteamMarshallerTable.cs similarity index 83% rename from Standalone3.0/SteamMarshallerTable.cs rename to Standalone3.0/anycpu/SteamMarshallerTable.cs index 1f603836..443fb1d9 100644 --- a/Standalone3.0/SteamMarshallerTable.cs +++ b/Standalone3.0/anycpu/SteamMarshallerTable.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.InteropServices; namespace Steamworks { @@ -16,9 +15,8 @@ private static partial class Impl // partial, in generated file // static ConditionalMarshallerTable(); - public static T Marshal(IntPtr unmanagetype) - { + public static T Marshal(IntPtr unmanagetype) { return Impl.Marshaller(unmanagetype); - } + } } } diff --git a/Standalone3.0/SteamMarshallerTable.g.cs b/Standalone3.0/anycpu/SteamMarshallerTable.g.cs similarity index 100% rename from Standalone3.0/SteamMarshallerTable.g.cs rename to Standalone3.0/anycpu/SteamMarshallerTable.g.cs diff --git a/Standalone3.0/anycpu/VersionChecker.cs b/Standalone3.0/anycpu/VersionChecker.cs new file mode 100644 index 00000000..9eb53ea7 --- /dev/null +++ b/Standalone3.0/anycpu/VersionChecker.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Steamworks +{ + // another part is generated by python script, which contains the expected version. + internal static partial class VersionChecker + { + public const string ExpectedVersion = Version.SteamAPIDLLVersion; + + // loaded file might from a different path than the one we expect, so we need to get the actual path of the loaded module and check its version. + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true)] + private static extern int GetModuleFileName( + IntPtr hModule, + StringBuilder lpFilename, + int nSize + ); + + internal static void CheckedGoodOrThrow(nint loadedWindowsSteamModule) { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + // other platforms is not possible to check, their versioning method is too various + return; + + StringBuilder sbModulePath = new(65535); + if (GetModuleFileName(loadedWindowsSteamModule, sbModulePath, sbModulePath.Capacity) != 0) + throw new ArgumentException("Module path is longer than excepted(65535), the hard path length limit as of 2026/3/10, ", + nameof(loadedWindowsSteamModule)); + + string modulePath = sbModulePath.ToString(); + FileVersionInfo ver = FileVersionInfo.GetVersionInfo(modulePath); + + if (ver.FileVersion == VersionChecker.ExpectedVersion) { + Debug.WriteLine($"Steamworks DLL version check passed: {ver.FileVersion}"); + } else { + throw new SteamworksDllMismatchException($"Steamworks DLL version mismatch: expected {VersionChecker.ExpectedVersion}," + + $" but found {ver.FileVersion}" + + $"{Environment.NewLine}Loaded Steamworks DLL path: {modulePath}") { + ModulePath = modulePath, + ActualVersion = ver.FileVersion, + }; + } + } + + /// + /// Represents an exception that is thrown when the Steamworks DLL version does not match the expected version required + /// by the application. + /// + /// + /// This exception typically indicates that the Steamworks native library present at runtime is + /// incompatible with the managed code, which may result from game engine's native plugin resolve logic error, + /// version mismatches, incorrect deployment, or pirateware. To resolve + /// this issue, ensure .NET SDK's NuGet functionality is used to manage the Steamworks.NET.AnyCPU dependency, + /// and that the correct version of the Steamworks DLL is included in the application's output directory. + /// Additionally, verify that the application is not being run in an environment where a different version of the Steamworks DLL is present, + /// such as a modded game directory or a pirated copy of the game. + /// + [Serializable] + public class SteamworksDllMismatchException : Exception + { + public SteamworksDllMismatchException() { } + public SteamworksDllMismatchException(string message) : base(message) { } + public SteamworksDllMismatchException(string message, Exception inner) : base(message, inner) { } + [Obsolete] + protected SteamworksDllMismatchException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// Programmitaclly access the expected version of the Steamworks DLL that is required by the Steamworks.NET. + /// + public string ExpectedVersion => VersionChecker.ExpectedVersion; + + public string ActualVersion { get; init; } + + public string ModulePath { get; init; } + } + } +} diff --git a/Standalone3.0/autogen/NativeMethods.AnyCPU.cs b/Standalone3.0/anycpu/autogen/NativeMethods.AnyCPU.cs similarity index 88% rename from Standalone3.0/autogen/NativeMethods.AnyCPU.cs rename to Standalone3.0/anycpu/autogen/NativeMethods.AnyCPU.cs index 92fce1e3..2a124516 100644 --- a/Standalone3.0/autogen/NativeMethods.AnyCPU.cs +++ b/Standalone3.0/anycpu/autogen/NativeMethods.AnyCPU.cs @@ -19,6 +19,8 @@ private static IntPtr DllImportResolver(string libraryName, System.Reflection.As if (libraryName == NativeLibraryName || libraryName == NativeLibrary_SDKEncryptedAppTicket) { // check are we on win64, the special case we are going to handle if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.Is64BitProcess) { + // *nix Godot handling on another branch. first branch is enough for Godot on Windows though, see next branch for details. + // check who is requesting steam native if (assembly.GetName().Name != "Steamworks.NET") { // Unmanaged libraries(steam native dll) will be cached(probably by name), @@ -54,13 +56,16 @@ private static IntPtr DllImportResolver(string libraryName, System.Reflection.As string searchDirectory = Path.GetDirectoryName(assembly.Location); if (string.IsNullOrEmpty(searchDirectory)) { + // basiclly Godot will fall here, it loads assemblies from memory. + // but on Windows it doesn't seem need, because Windows searchs dlls next to MainProcess.exe by default System.Diagnostics.Debug.WriteLine("It seems you are loading Steamworks.NET.AnyCPU from memory," + " auto-detect steam native location is not possible," + " now trying to load from AppDomain.BaseDirectory." + " If still fails, please call" + " NativeLibrary.SetDllImporterResplver(typeof(Steamworks.SteamAPI).Assembly, YourResolver) manually."); - searchDirectory = AppDomain.CurrentDomain.BaseDirectory; + // Godot sets AppContext.BaseDirectory for managed plugin loads, we can use it here. + searchDirectory = AppContext.BaseDirectory; } string path = Path.Combine(searchDirectory, Path.ChangeExtension(nixPrefix + libraryName, extension)); diff --git a/com.rlabrecque.steamworks.net/Runtime/InteropHelp.cs b/com.rlabrecque.steamworks.net/Runtime/InteropHelp.cs index 4ec706cc..17bff9df 100644 --- a/com.rlabrecque.steamworks.net/Runtime/InteropHelp.cs +++ b/com.rlabrecque.steamworks.net/Runtime/InteropHelp.cs @@ -1,4 +1,4 @@ -// This file is provided under The MIT License as part of Steamworks.NET. +// This file is provided under The MIT License as part of Steamworks.NET. // Copyright (c) 2013-2022 Riley Labrecque // Please see the included LICENSE.txt for additional information. @@ -11,13 +11,20 @@ #if !DISABLESTEAMWORKS +#if STEAMWORKS_ANYCPU +#nullable enable +#endif + +using System; using System.Runtime.InteropServices; using IntPtr = System.IntPtr; using System.Text; -namespace Steamworks { - public class InteropHelp { +namespace Steamworks +{ + public partial class InteropHelp + { public static void TestIfPlatformSupported() { #if !UNITY_EDITOR && !UNITY_STANDALONE && !UNITY_ANDROID && !STEAMWORKS_WIN && !STEAMWORKS_LIN_OSX throw new System.InvalidOperationException("Steamworks functions can only be called on platforms that Steam is available on."); @@ -43,6 +50,7 @@ public static void TestIfAvailableGameServer() { } // This continues to exist for both 'out string' and strings returned by Steamworks functions. +#if !STEAMWORKS_UNSAFE_ENABLED public static string PtrToStringUTF8(IntPtr nativeUtf8) { if (nativeUtf8 == IntPtr.Zero) { return null; @@ -62,7 +70,7 @@ public static string PtrToStringUTF8(IntPtr nativeUtf8) { Marshal.Copy(nativeUtf8, buffer, 0, buffer.Length); return Encoding.UTF8.GetString(buffer); } - +#endif public static string ByteArrayToStringUTF8(byte[] buffer) { int length = 0; while (length < buffer.Length && buffer[length] != 0) { @@ -72,17 +80,29 @@ public static string ByteArrayToStringUTF8(byte[] buffer) { return Encoding.UTF8.GetString(buffer, 0, length); } - public static void StringToByteArrayUTF8(string str, byte[] outArrayBuffer, int outArrayBufferSize) - { - outArrayBuffer = new byte[outArrayBufferSize]; + public static void StringToByteArrayUTF8(string str, byte[] outArrayBuffer, int outArrayBufferSize) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(outArrayBuffer); +#else + if (outArrayBuffer == null) { + throw new ArgumentNullException(nameof(outArrayBuffer)); + } +#endif + int length = Encoding.UTF8.GetBytes(str, 0, str.Length, outArrayBuffer, 0); + + // do buffer capacity check + if (length >= outArrayBuffer.Length - 1) { + throw new ArgumentOutOfRangeException(nameof(str), length, "Encoded string length is longer than 'outArrayByffer' can hold"); + } outArrayBuffer[length] = 0; } - // This is for 'const char *' arguments which we need to ensure do not get GC'd while Steam is using them. + // This is for 'const char *' and `char *` arguments which we need to ensure do not get GC'd while Steam is using them. // We can't use an ICustomMarshaler because Unity crashes when a string between 96 and 127 characters long is defined/initialized at the top of class scope... #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_ANDROID || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX - public class UTF8StringHandle : Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid { + public class UTF8StringHandle : Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid + { public UTF8StringHandle(string str) : base(true) { if (str == null) { @@ -99,6 +119,21 @@ public UTF8StringHandle(string str) SetHandle(buffer); } + public UTF8StringHandle(int bufferSize) : base(true) { + SetHandle(Marshal.AllocHGlobal(bufferSize + 1)); + } + +#if STEAMWORKS_ANYCPU + public string? BackToString() { +#else + public string BackToString() { +#endif + if (IsInvalid) + return null; + + return PtrToStringUTF8(handle); + } + protected override bool ReleaseHandle() { if (!IsInvalid) { Marshal.FreeHGlobal(handle); @@ -113,15 +148,20 @@ public void Dispose() {} } #endif - // TODO - Should be IDisposable // We can't use an ICustomMarshaler because Unity dies when MarshalManagedToNative() gets called with a generic type. - public class SteamParamStringArray { + public class SteamParamStringArray : System.IDisposable + { // The pointer to each AllocHGlobal() string +#if STEAMWORKS_ANYCPU + IntPtr[]? m_Strings; +#else IntPtr[] m_Strings; +#endif // The pointer to the condensed version of m_Strings IntPtr m_ptrStrings; // The pointer to the StructureToPtr version of SteamParamStringArray_t that will get marshaled IntPtr m_pSteamParamStringArray; + private bool _disposedValue; public SteamParamStringArray(System.Collections.Generic.IList strings) { if (strings == null) { @@ -148,31 +188,51 @@ public SteamParamStringArray(System.Collections.Generic.IList strings) { Marshal.StructureToPtr(stringArray, m_pSteamParamStringArray, false); } - ~SteamParamStringArray() { - if (m_Strings != null) { - foreach (IntPtr ptr in m_Strings) { - Marshal.FreeHGlobal(ptr); + public static implicit operator IntPtr(SteamParamStringArray that) { + return that.m_pSteamParamStringArray; + } + + protected virtual void Dispose(bool disposing) { + if (!_disposedValue) { + if (disposing) { + // Part of dispose pattern. No managed resources to dispose. } - } - if (m_ptrStrings != IntPtr.Zero) { - Marshal.FreeHGlobal(m_ptrStrings); - } + // cleanup unmanaged resources + if (m_Strings != null) { + foreach (IntPtr ptr in m_Strings) { + Marshal.FreeHGlobal(ptr); + } + } - if (m_pSteamParamStringArray != IntPtr.Zero) { - Marshal.FreeHGlobal(m_pSteamParamStringArray); + if (m_ptrStrings != IntPtr.Zero) { + Marshal.FreeHGlobal(m_ptrStrings); + } + + if (m_pSteamParamStringArray != IntPtr.Zero) { + Marshal.FreeHGlobal(m_pSteamParamStringArray); + } + + _disposedValue = true; } } - public static implicit operator IntPtr(SteamParamStringArray that) { - return that.m_pSteamParamStringArray; + ~SteamParamStringArray() { + Dispose(disposing: false); + } + + public void Dispose() { + // Do not change this code. Put cleanup code into the "Dispose(bool disposing)" method. + Dispose(disposing: true); + GC.SuppressFinalize(this); } } } // TODO - Should be IDisposable // MatchMaking Key-Value Pair Marshaller - public class MMKVPMarshaller { + public class MMKVPMarshaller + { private IntPtr m_pNativeArray; private IntPtr m_pArrayEntries; @@ -205,63 +265,6 @@ public static implicit operator IntPtr(MMKVPMarshaller that) { return that.m_pNativeArray; } } - - public class DllCheck { -#if DISABLED - [DllImport("kernel32.dll")] - public static extern IntPtr GetModuleHandle(string lpModuleName); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto)] - extern static int GetModuleFileName(IntPtr hModule, StringBuilder strFullPath, int nSize); -#endif - - /// - /// This is an optional runtime check to ensure that the dlls are the correct version. Returns false only if the steam_api.dll is found and it's the wrong size or version number. - /// - public static bool Test() { -#if DISABLED - bool ret = CheckSteamAPIDLL(); -#endif - return true; - } - -#if DISABLED - private static bool CheckSteamAPIDLL() { - string fileName; - int fileBytes; - if (IntPtr.Size == 4) { - fileName = "steam_api.dll"; - fileBytes = Version.SteamAPIDLLSize; - } - else { - fileName = "steam_api64.dll"; - fileBytes = Version.SteamAPI64DLLSize; - } - - IntPtr handle = GetModuleHandle(fileName); - if (handle == IntPtr.Zero) { - return true; - } - - StringBuilder filePath = new StringBuilder(256); - GetModuleFileName(handle, filePath, filePath.Capacity); - string file = filePath.ToString(); - - // If we can not find the file we'll just skip it and let the DllNotFoundException take care of it. - if (System.IO.File.Exists(file)) { - System.IO.FileInfo fInfo = new System.IO.FileInfo(file); - if (fInfo.Length != fileBytes) { - return false; - } - - if (System.Diagnostics.FileVersionInfo.GetVersionInfo(file).FileVersion != Version.SteamAPIDLLVersion) { - return false; - } - } - return true; - } -#endif - } } #endif // !DISABLESTEAMWORKS diff --git a/com.rlabrecque.steamworks.net/Runtime/Version.cs b/com.rlabrecque.steamworks.net/Runtime/Version.cs index a275aff8..27a5b6ba 100644 --- a/com.rlabrecque.steamworks.net/Runtime/Version.cs +++ b/com.rlabrecque.steamworks.net/Runtime/Version.cs @@ -13,11 +13,15 @@ namespace Steamworks { public static class Version { + // TODO: MANUAL edit this file on each targeting Steamworks API update public const string SteamworksNETVersion = "2025.163.0"; public const string SteamworksSDKVersion = "1.63"; - public const string SteamAPIDLLVersion = "10.24.16.75"; + // used to for Steamworks.NET.AnyCPU to check whether the loaded native is compatible + public const string SteamAPIDLLVersion = "10.24.16.75"; + [System.Obsolete("SteamAPIDLLVersion is more reliable to check versions", DiagnosticId = "SNET4001")] public const int SteamAPIDLLSize = 274072; - public const int SteamAPI64DLLSize = 317080; + [System.Obsolete("SteamAPIDLLVersion is more reliable to check versions", DiagnosticId = "SNET4001")] + public const int SteamAPI64DLLSize = 317080; } } From 424293f40ff8ac3b648ab73d41caf49ab2841d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=E9=B8=BD?= <43724908+Akarinnnnn@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:22:57 +0800 Subject: [PATCH 2/2] Enable version check while resolving DLL --- Standalone3.0/anycpu/VersionChecker.cs | 4 ++-- Standalone3.0/anycpu/autogen/NativeMethods.AnyCPU.cs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Standalone3.0/anycpu/VersionChecker.cs b/Standalone3.0/anycpu/VersionChecker.cs index 9eb53ea7..8b5c7d41 100644 --- a/Standalone3.0/anycpu/VersionChecker.cs +++ b/Standalone3.0/anycpu/VersionChecker.cs @@ -24,14 +24,14 @@ internal static void CheckedGoodOrThrow(nint loadedWindowsSteamModule) { return; StringBuilder sbModulePath = new(65535); - if (GetModuleFileName(loadedWindowsSteamModule, sbModulePath, sbModulePath.Capacity) != 0) + if (GetModuleFileName(loadedWindowsSteamModule, sbModulePath, sbModulePath.Capacity) == 0) throw new ArgumentException("Module path is longer than excepted(65535), the hard path length limit as of 2026/3/10, ", nameof(loadedWindowsSteamModule)); string modulePath = sbModulePath.ToString(); FileVersionInfo ver = FileVersionInfo.GetVersionInfo(modulePath); - if (ver.FileVersion == VersionChecker.ExpectedVersion) { + if (ver.FileVersion == ExpectedVersion) { Debug.WriteLine($"Steamworks DLL version check passed: {ver.FileVersion}"); } else { throw new SteamworksDllMismatchException($"Steamworks DLL version mismatch: expected {VersionChecker.ExpectedVersion}," + diff --git a/Standalone3.0/anycpu/autogen/NativeMethods.AnyCPU.cs b/Standalone3.0/anycpu/autogen/NativeMethods.AnyCPU.cs index 2a124516..3a697905 100644 --- a/Standalone3.0/anycpu/autogen/NativeMethods.AnyCPU.cs +++ b/Standalone3.0/anycpu/autogen/NativeMethods.AnyCPU.cs @@ -38,6 +38,10 @@ private static IntPtr DllImportResolver(string libraryName, System.Reflection.As string x64LibName = $"{libraryName}64"; NativeLibrary.TryLoad(x64LibName, assembly, searchPath, out nint lib); + if (lib != 0) { + VersionChecker.CheckedGoodOrThrow(lib); + } + return lib; } else { // first chance search, if failed or specified, check the assembly directory for steam natives