分析UBT中EULA的内容分发限制

Analyzing the content distribution restrictions of EULA in UBT

UE的EULA License Grant/(A) 中明确说明了,使用UE开发并再分发的内容不得包含 未Cook的源格式 和基于 引擎工具 开发的付费内容,本篇文章研究一下EULA里对内容分发的具体内容和从技术上怎么绕过这个限制。

There is no restriction on your Distribution of a Product made using the Licensed Technology that does not include any Engine Code or any Paid Content Distributed in uncooked source format (in each case, including as modified by you under the License) and does not require any Licensed Technology (including as modified by you under the License) to run (“Unrestricted Products”). For clarity, the foregoing does not constitute a license under any patents, copyrights, trademarks, trade secrets or other intellectual property rights, whether by implication, estoppel or otherwise.

而EULA中对于Engine Tools的定义是:

“Engine Tools” means (a) editors and other tools included in the Engine Code; (b) any code and modules in either the Developer or Editor folders, including in object code format, whether statically or dynamically linked; and (c) other software that may be used to develop standalone products based on the Licensed Technology.

这意味着开发者无法在游戏内容中使用EditorDeveloper下的任何模块。
用户协议是具有法律效力的协议文件,我本着要折腾一下的精神并且想顺便看一下UE是怎么实现这个限制的,仅作记录分析用,并不会对内容进行收费的二次分发或用在正式的项目中。我读了一下UBT的处理逻辑,UBT的代码只需要简单地改几处。

注意:在正式的发行版本中这么做是违反UE的EULA的,强烈不建议在正式项目中这么做。
本文代码和编译均基于UE_4.18版本(本地通过源码编译出的引擎),本文介绍的操作不适用从EpicLauncher安装的引擎,文末会写原因。

在UE中,当打包的游戏(Target is Game)为Shipping的时候,如果工程中中包含了Editor/Developer的模块,会报下列错误:

1
UATHelper: Packaging (Windows (64-bit)):   ERROR: ERROR: Non-editor build cannot depend on non-redistributable modules. C:\Users\imzlp\Documents\Unreal Projects\EULA418\Binaries\Win64\EULA418-Win64-Shipping.exe depends on 'DesktopPlatform'.

意思就是DesktopPlatform是不可再发行的模块,你不能在Game中用,通过查看UBT的代码发现这个异常是在UBT的CheckForEULAViolation中抛出的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Source/Programs/UnrealBuildTools/Configuration/UEBuildTarget.cs
private void CheckForEULAViolation()
{
if (TargetType != TargetType.Editor && TargetType != TargetType.Program && Configuration == UnrealTargetConfiguration.Shipping &&
Rules.bCheckLicenseViolations)
{
bool bLicenseViolation = false;
foreach (UEBuildBinary Binary in AppBinaries)
{
List<UEBuildModule> AllDependencies = Binary.GetAllDependencyModules(true, false);
IEnumerable<UEBuildModule> NonRedistModules = AllDependencies.Where((DependencyModule) =>
!IsRedistributable(DependencyModule) && DependencyModule.Name != AppName
);

if (NonRedistModules.Count() != 0)
{
IEnumerable<UEBuildModule> NonRedistDeps = AllDependencies.Where((DependantModule) =>
DependantModule.GetDirectDependencyModules().Intersect(NonRedistModules).Any()
);
string Message = string.Format("Non-editor build cannot depend on non-redistributable modules. {0} depends on '{1}'.", Binary.ToString(), string.Join("', '", NonRedistModules));
if (NonRedistDeps.Any())
{
Message = string.Format("{0}\nDependant modules '{1}'", Message, string.Join("', '", NonRedistDeps));
}
if(Rules.bBreakBuildOnLicenseViolation)
{
Log.TraceError("ERROR: {0}", Message);
}
else
{
Log.TraceWarning("WARNING: {0}", Message);
}
bLicenseViolation = true;
}
}
if (Rules.bBreakBuildOnLicenseViolation && bLicenseViolation)
{
throw new BuildException("Non-editor build cannot depend on non-redistributable modules.");
}
}
}

关键的判断是IsRedistributable这个方法(概念等同于C++里的成员函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Source/Programs/UnrealBuildTools/Configuration/UEBuildTarget.cs
public static bool IsRedistributable(UEBuildModule Module)
{
if(Module.Rules != null && Module.Rules.IsRedistributableOverride.HasValue)
{
return Module.Rules.IsRedistributableOverride.Value;
}
if(Module.RulesFile != null)
{
return !Module.RulesFile.IsUnderDirectory(UnrealBuildTool.EngineSourceDeveloperDirectory) && !Module.RulesFile.IsUnderDirectory(UnrealBuildTool.EngineSourceEditorDirectory);
}

return true;
}

这里做的判断是模块是不是属于Editor/Developer的,是就返回true,从而进行下面的异常抛出。为了测试我将这里直接修改为return true;

只是改了上面之后还是打包不成功,不过现在的错误变成了链接错误。继续看UBT的代码后发现还有一个地方对打包是Shipping的模式做了判断(前后的// ...表示省略了不相关代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Source/Programs/UnrealBuildTools/Configuration/UEBuildTarget.cs
protected void AddPrecompiledModules()
{
// ...
bool bAllowDeveloperModules = false;
if(Configuration != UnrealTargetConfiguration.Shipping)
{
Directories.Add(UnrealBuildTool.EngineSourceDeveloperDirectory);
bAllowDeveloperModules = true;
}

// Find all the modules that are not part of the standard set
HashSet<string> FilteredModuleNames = new HashSet<string>();
foreach (string ModuleName in ModuleNames)
{
FileReference ModuleFileName = RulesAssembly.GetModuleFileName(ModuleName);
if (Directories.Any(x => ModuleFileName.IsUnderDirectory(x)))
{
string RelativeFileName = ModuleFileName.MakeRelativeTo(UnrealBuildTool.EngineSourceDirectory);
if (ExcludeFolders.All(x => RelativeFileName.IndexOf(x, StringComparison.InvariantCultureIgnoreCase) == -1) && !PrecompiledModules.Any(x => x.Name == ModuleName))
{
FilteredModuleNames.Add(ModuleName);
}
}
}

// Add all the plugins which aren't already being built
foreach(UEBuildPlugin PrecompilePlugin in PrecompilePlugins)
{
foreach (ModuleDescriptor ModuleDescriptor in PrecompilePlugin.Descriptor.Modules)
{
if (ModuleDescriptor.IsCompiledInConfiguration(Platform, TargetType, bAllowDeveloperModules && Rules.bBuildDeveloperTools, Rules.bBuildEditor, Rules.bBuildRequiresCookedData))
{
string RelativeFileName = RulesAssembly.GetModuleFileName(ModuleDescriptor.Name).MakeRelativeTo(UnrealBuildTool.EngineDirectory);
if (!ExcludeFolders.Any(x => RelativeFileName.Contains(x)) && !PrecompiledModules.Any(x => x.Name == ModuleDescriptor.Name))
{
FilteredModuleNames.Add(ModuleDescriptor.Name);
}
}
}
}
// ...
}

将上面关于bAllowDeveloperModules的部分注释掉,然后直接:

1
2
Directories.Add(UnrealBuildTool.EngineSourceDeveloperDirectory);
bool bAllowDeveloperModules=true;

重新编译UBT即可。

然后可以写个简单的例子测试了,比如使用Developer下的DesktopPlatform来调用系统的选择文件。
首先在*.build.cs中添加DesktopPlatform模块依赖:

1
2
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" ,"DesktopPlatform"});
PrivateDependencyModuleNames.AddRange(new string[] { "DesktopPlatform" });

然后写一个简单的函数暴露给蓝图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// .h
UCLASS()
class EULA418_API UFileOperatorFlib : public UBlueprintFunctionLibrary
{
GENERATED_BODY()

UFUNCTION(BlueprintCallable)
static FString ExtendGetOpenFileName();

};
// .cpp
#include "FileOperatorFlib.h"
#include "Developer/DesktopPlatform/Public/DesktopPlatformModule.h"
#include "Paths.h"

FString UFileOperatorFlib::ExtendGetOpenFileName()
{
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();

if (DesktopPlatform)
{
TArray<FString> OpenFilenames;
const bool bOpened = DesktopPlatform->OpenFileDialog(
nullptr,
TEXT("OpenFileDialog"),
FString(TEXT("")),
TEXT(""),
TEXT("All Files (*.*)"),
EFileDialogFlags::None,
OpenFilenames
);

if (OpenFilenames.Num() > 0)
{
return FPaths::ConvertRelativePathToFull(OpenFilenames[0]);
}
}
return TEXT("");
}

正常的情况下,将上面的代码打包Shipping会产生文章最开始的错误,但是我们改过之后可以打包成功并且可以正确执行,这个简单的工程和打包的内容可以在这里下载。
启动之后按下数字键1(不是Num 1)即可打开windows的选择文件窗口,选择之后会将选择的文件的路径显示到屏幕上。

注意:需要使用源码版引擎编译项目,如果编译时具有noexcept的错误,则可以在项目的*.target.cs中添加bForceEnableExceptions = true;
重新编译项目会编译引擎内的模块(打开从EpicLauncher安装的引擎文件夹就可以知道,默认Editor/Developer的模块是没有编译出静态链接库的,这也是不能用从EpicLauncher安装的引擎来执行本文的操作的原因)。

全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:分析UBT中EULA的内容分发限制
文章作者:查利鹏
发布时间:2019/08/24 14:15
本文字数:2k 字
原始链接:https://imzlp.com/posts/9050/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!