Flutter未知功能插件gangplank的潜在用途
Flutter未知功能插件gangplank的潜在用途
Gangplank 是一个用于简化使用 LCU(League Client Update)API 的插件。它提供了多种功能,如监听 League 客户端的启动和关闭、WebSocket 连接、HTTP 请求等。本插件目前仅支持 Windows 和 macOS。
示例应用
功能特性
- LCUWatcher:监听你的 League 客户端并通知你客户端的启动和关闭。
- LCUSocket:负责 WebSocket 连接。它连接到 League 客户端,你可以订阅你想监听的事件。
- LCUHttpClient:提供最常用的 HTTP 方法,用于向 League 客户端发送 HTTP 请求(例如创建大厅、开始匹配等)。
- LCULiveGameWatcher:监听你的游戏客户端并以多种方式通知你(见下文)。
使用方法
预备工作
LCUWatcher
, LCUSocket
, LCUHttpClient
和 LCULiveGameWatcher
应该通过 Gangplank
类来实例化,以确保一切正常运行。如果你尝试多次使用同一个 Gangplank
实例来创建服务实例,则会抛出断言错误。
final gp = Gangplank(
// 禁用整个包的日志记录
disableLogging: true,
);
final watcher = gp.createLCUWatcher(
config: LCUWatcherConfig(
// 仅禁用 LCUWatcher 的日志记录
disableLogging: true,
// 检查 League of Legends 进程是否存在的时间间隔
processCheckerInterval: const Duration(seconds: 2),
)
);
final socket = gp.createLCUSocket();
final httpClient = gp.createLCUHttpClient();
final liveGameWatcher = gp.createLCULiveGameWatcher();
每项服务都可以接受一个配置对象,允许你更改其行为。
全局覆盖 HTTP
在使用此插件之前,请确保全局覆盖 HTTP 以防止连接到 LCU 时出现握手异常。
void main() {
HttpOverrides.global = GangplankHttpOverrides();
runApp(const GangplankExampleApp());
}
LCUWatcher 和 LCUSocket
LCUWatcher
是其他服务正确工作的第一个实例。它监视你的 League 客户端并提取所需的凭证以连接到 League 客户端。在 LCUWatcher
触发 onClientStarted
事件之前,你不能使用 LCUSocket
或 LCUHttpClient
。当 LCUWatcher
触发 onClientStarted
事件后,你可以连接到 WebSocket。当 WebSocket 成功连接时,将触发 onConnect
事件。如果 League 客户端关闭并重新打开,所有服务将自然恢复工作,无需自行处理任何事情。
final gp = Gangplank();
final watcher = gp.createLCUWatcher();
final socket = gp.createLCUSocket();
watcher.onClientStarted.listen((credentials) {
// 当 LCUWatcher 找到正在运行的 League 客户端实例时调用
// 现在你可以安全地使用 LCUHTTPCLIENT
/* 客户端已启动,你现在可以开始连接到 League 客户端暴露的 WebSocket */
// 当 WebSocket 成功连接时,将触发 onConnect 事件
socket.connect();
});
watcher.onClientClosed.listen((_) {
// League 客户端已关闭
});
socket.onConnect.listen((_) {
// WebSocket 现在已连接
});
socket.onDisconnect.listen((_) {
// WebSocket 现在已断开连接
});
// 开始监视
watcher.watch();
你也可以手动停止监视 LCUWatcher
或断开 LCUSocket
的连接。
watcher.stopWatching();
socket.disconnect();
订阅 WebSocket 事件
你可以在应用程序的不同小部件/组件/服务中多次订阅相同的事件。如果 LCUSocket
断开并重新连接,订阅将保持不变,不会被处置。这意味着只需订阅一次即可。
socket.subscribe('*', (event) {
// 在这里接收 League 客户端发出的所有事件
});
socket.subscribe('/lol-lobby/v2/*', (event) {
// 可以在给定路径的末尾使用通配符
});
socket.subscribe('*/v2/lobby', (event) {
// 可以在给定路径的开头使用通配符
});
socket.subscribe('/lol-chat/v1/conversations/*/messages', (event) {
// 可以在给定路径中使用通配符
});
socket.subscribe('/lol-chat/*/conversations/*/messages', (event) {
// 可以使用多个通配符
});
socket.subscribe('/lol-lobby/v2/lobby', (event) {
// 也可以完全匹配路径
});
// 取消订阅特定事件
socket.unsubscribe('/lol-lobby/v2/lobby');
// 通过函数订阅和取消订阅特定事件(无匿名函数)
socket.subscribe('/lol-lobby/v2/lobby', onLobbyEvent);
socket.unsubscribeSpecific(onLobbyEvent);
void onLobbyEvent(EventResponse data) => print(data);
// 手动触发事件以测试和模拟事件监听器
socket.fireEvent(
'/lol-lobby/v2/lobby',
ManualEventResponse(
uri: '/lol-lobby/v2/lobby',
data: { 'mockedData': true }
),
);
// 上述操作将导致此事件监听器触发并发出上述数据
socket.subscribe('/lol-lobby/v2/lobby', (event) {
// EVENT.uri = '/lol-lobby/v2/lobby';
// EVENT.data = {'mockedData': true };
});
LCUHttpClient(执行 HTTP 请求)
如上所述,只有在 onClientStarted
事件触发后才能安全执行 HTTP 请求。否则,将抛出断言错误。LCUHttpClient
使用自己的异常类。此异常类称为 LCUHttpClientException
,包括错误消息、HTTP 状态和由 League 客户端提供的错误代码。
创建或离开大厅
final gp = Gangplank();
// 当然,仅在观察者触发 onClientStarted 事件后才可用
// 你可以传递将要缓存的端点路由及其过期时间
// 你还可以提供通配符进行匹配
// 缓存过期时间是可选的,如果没有提供则默认为全局缓存过期时间
final httpClient = gp.createLCUHttpClient(
config: LCUHttpClientConfig(
getRoutesToCache: [
LCUGetRouteToCache(
route: '/lol-summoner/v1/*',
cacheExpiration: const Duration(minutes: 120),
),
],
cacheExpiration: const Duration(minutes: 20),
),
);
try {
// QUEUEID 440 将导致一个灵活排名的大厅
await httpClient.post('/lol-lobby/v2/lobby', body: { 'queueId': 440 });
} catch (err) {
// 使用 toString() 将打印异常的所有属性
print(err.toString());
}
try {
// 离开当前大厅
await httpClient.delete('/lol-lobby/v2/lobby');
} catch (err) {
// 使用 toString() 将打印异常的所有属性
print(err.toString());
}
LCULiveGameWatcher
LCULiveGameWatcher
与游戏客户端 API 通信。游戏客户端是在选将阶段后启动的实际应用程序,在其中你积极玩游戏。LCULiveGameWatcher
独立于其他服务工作,可以不依赖于 League 客户端运行,而 LCUSocket
和 LCUHttpClient
则依赖于 League 客户端运行。此服务将公开以下 API。
订阅流以接收
- 当发现或终止正在进行的游戏时
- 当正在进行的游戏完全加载并开始时(玩家可以与游戏互动)
- 当接收到游戏客户端暴露的数据更新时(游戏汇总更新)
- 当游戏计时器发生变化时
在游戏汇总更新时请求以下数据:
- 游戏统计数据(端点:gamestats)
- 事件数据(端点:eventdata)
- 整个玩家列表(端点:playerlist) – 可以禁用
final gp = Gangplank();
final liveGameWatcher = gp.createLCULiveGameWatcher();
liveGameWatcher.onGameFound.listen((_) {
// 发射当找到正在进行的游戏时
});
liveGameWatcher.onGameEnded.listen((_) {
// 发射当正在进行的游戏结束或终止时
});
liveGameWatcher.onGameStarted.listen((gameTime) {
// 发射当正在进行的游戏实际开始时,当前游戏时间为 gameTime
});
liveGameWatcher.onGameSummaryUpdate.listen((summary) {
/* 发射游戏客户端暴露的数据摘要
发射在你自己可以配置的间隔内 */
});
liveGameWatcher.onGameTimerUpdate.listen((time) {
// 发射当游戏计时器更新时 - 每秒一次
/* 此函数将秒数转换为 MM:SS 格式
可用于显示当前游戏计时器 */
print(liveGameWatcher.formatSecondsToMMSS(time));
});
// 开始监视游戏客户端
liveGameWatcher.watch();
你还可以向 createLCULiveGameWatcher
函数传递一个配置对象。
final liveGameWatcher = gp.createLCULiveGameWatcher(
config: LCULiveGameWatcherConfig(
disableLogging: true,
fetchPlayerList: false,
gamePresenceCheckerInterval: const Duration(seconds: 5),
gameSummaryInterval: const Duration(seconds: 10),
emitNullForGameSummaryUpdateOnGameEnded: false,
emitResettedGameTimerOnGameEnded: false,
),
);
如果你愿意,你也可以手动停止监视 LCULiveGameWatcher
。
liveGameWatcher.stopWatching();
资源释放
大多数情况下,Gangplank
实例将在应用程序的整个生命周期中使用,但如果你决定仅将其作为应用程序的一部分使用,请确保在 Gangplank
实例上调用 dispose
函数。
gp.dispose();
示例代码
import 'dart:io';
import 'package:gangplank/gangplank.dart';
import 'package:flutter/material.dart';
void main() {
HttpOverrides.global = GangplankHttpOverrides();
runApp(const GangplankExampleApp());
}
class GangplankExampleApp extends StatelessWidget {
const GangplankExampleApp({Key? key}) : super(key: key);
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
title: 'Gangplank',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
),
home: const GangplankExamplePage(),
);
}
}
class GangplankExamplePage extends StatefulWidget {
const GangplankExamplePage({Key? key}) : super(key: key);
[@override](/user/override)
State<GangplankExamplePage> createState() => _GangplankExamplePageState();
}
class _GangplankExamplePageState extends State<GangplankExamplePage> {
late Gangplank gp;
late LCUWatcher watcher;
late LCUSocket socket;
late LCUHttpClient httpClient;
late LCULiveGameWatcher liveGameWatcher;
LCUCredentials? _credentials;
List<EventResponse> events = [];
String? currentGameflowPhase;
int currentLiveGameTime = 0;
[@override](/user/override)
void initState() {
super.initState();
gp = Gangplank();
watcher = gp.createLCUWatcher(
config: LCUWatcherConfig(
disableLogging: true,
));
socket = gp.createLCUSocket(
config: LCUSocketConfig(
debounceDuration: const Duration(milliseconds: 500),
),
);
httpClient = gp.createLCUHttpClient(
config: LCUHttpClientConfig(
getRoutesToCache: [
LCUGetRouteToCache(
route: '/lol-summoner/v1/*',
cacheExpiration: const Duration(minutes: 120),
),
],
cacheExpiration: const Duration(minutes: 20),
),
);
watcher.onClientStarted.listen((credentials) async {
// 客户端已启动
// 现在我们可以连接到 WebSocket
// 如果你在触发此事件之前尝试连接到 WebSocket,将引发异常(缺少凭证)
_credentials = credentials;
socket.connect();
setState(() {});
});
watcher.onClientClosed.listen((_) {
// 客户端已关闭
/* 如果 LCU-Socket 已连接,它将自动断开
因为 League 客户端已关闭 */
setState(() {});
});
socket.onConnect.listen((_) async {
// WebSocket 已连接
currentGameflowPhase = await httpClient.get('/lol-gameflow/v1/gameflow-phase');
await httpClient.get('/lol-summoner/v1/current-summoner');
setState(() {});
});
socket.onDisconnect.listen((_) {
// WebSocket 已断开连接
events.clear();
setState(() {});
});
// 开始监视 LCU
watcher.watch();
/* 订阅事件 -> 记住!即使 Socket 关闭/重新连接,也只订阅一次
订阅始终保持!*/
// socket.subscribe('*', (event) {
// events.add(event);
// setState(() {});
// });
socket.subscribe('/lol-champ-select/v1/session', (event) {
events.add(event);
setState(() {});
});
socket.subscribe('/lol-lobby/v2/lobby', (event) {
events.add(event);
setState(() {});
});
socket.subscribe('/lol-gameflow/v1/gameflow-phase', (event) {
currentGameflowPhase = event.data;
events.add(event);
setState(() {});
});
socket.subscribe('/lol-game-client-chat/v1/buddies/*', (event) {
events.add(event);
setState(() {});
});
socket.subscribe('/lol-chat/v1/conversations/*/messages', (event) {
events.add(event);
setState(() {});
});
// 通过函数订阅和取消订阅特定事件(无匿名函数)
socket.subscribe('/lol-lobby/v2/lobby', onLobbyEvent);
// 手动触发事件以测试和模拟事件监听器
socket.fireEvent(
'/lol-lobby/v2/lobby',
ManualEventResponse(uri: '/lol-lobby/v2/lobby', data: {'mockedData': true}),
);
// 上述操作将导致此事件监听器触发并发出上述数据
socket.subscribe('/lol-lobby/v2/lobby', (event) {
// EVENT.uri = '/lol-lobby/v2/lobby';
// EVENT.data = {'mockedData': true };
});
// 通过函数取消订阅特定事件(无匿名函数)
socket.unsubscribeSpecific(onLobbyEvent);
liveGameWatcher = gp.createLCULiveGameWatcher(
config: LCULiveGameWatcherConfig(
disableLogging: false,
fetchPlayerList: false,
gamePresenceCheckStrategy: GamePresenceCheckStrategy.process,
gamePresenceCheckerInterval: const Duration(seconds: 5),
//gameSummaryInterval: const Duration(seconds: 2),
// emitNullForGameSummaryUpdateOnGameEnded: false,
// emitResettedGameTimerOnGameEnded: false,
),
);
liveGameWatcher.onGameFound.listen((_) {
// 发射当找到正在进行的游戏时
setState(() {});
});
liveGameWatcher.onGameEnded.listen((_) {
// 发射当正在进行的游戏结束或终止时
setState(() {});
});
liveGameWatcher.onGameStarted.listen((gameTime) {
// 发射当正在进行的游戏实际开始时,当前游戏时间为 gameTime
setState(() {});
});
liveGameWatcher.onGameSummaryUpdate.listen((summary) {
/* 发射游戏客户端暴露的数据摘要
发射在你自己可以配置的间隔内 */
setState(() {});
});
liveGameWatcher.onGameTimerUpdate.listen((time) {
// 发射当游戏计时器更新时 - 每秒一次
/* 此函数将秒数转换为 MM:SS 格式
可用于显示当前游戏计时器 */
currentLiveGameTime = time;
print(liveGameWatcher.formatSecondsToMMSS(time));
setState(() {});
});
// 开始监视游戏客户端
liveGameWatcher.watch();
}
void onLobbyEvent(EventResponse data) => print(data);
[@override](/user/override)
void dispose() {
gp.dispose();
super.dispose();
}
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Gangplank'),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('LCU-CREDENTIALS'),
const SizedBox(
height: 10,
),
Text(
_credentials == null ? 'Not found yet.' : _credentials.toString(),
),
],
))),
const SizedBox(
height: 10,
),
Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('LCU-STATUSES'),
const SizedBox(
height: 10,
),
ListTile(
leading: _buildStatusDot(true),
title: Text(currentGameflowPhase != null ? 'Gameflowphase: $currentGameflowPhase' : 'Gameflowphase: No gameflow found yet.'),
dense: true,
),
ListTile(
leading: _buildStatusDot(watcher.clientIsRunning),
title: Text(watcher.clientIsRunning ? 'LCU is running' : 'LCU is not running'),
dense: true,
),
ListTile(
leading: _buildStatusDot(socket.isConnected),
title: Text(socket.isConnected ? 'LCU-Socket is connected' : 'LCU-Socket is not connected'),
dense: true,
),
ListTile(
leading: _buildStatusDot(liveGameWatcher.gameInProgress),
title: Text(liveGameWatcher.gameInProgress ? 'Player is currently ingame' : 'Player is currently not ingame'),
dense: true,
),
ListTile(
leading: _buildStatusDot(liveGameWatcher.gameHasStarted),
title: Text(liveGameWatcher.gameHasStarted ? 'The active game has started' : 'If active, has not yet started'),
dense: true,
),
ListTile(
leading: _buildStatusDot(liveGameWatcher.gameHasStarted),
title: Text(liveGameWatcher.formatSecondsToMMSS(currentLiveGameTime)),
dense: true,
),
],
)),
),
const SizedBox(
height: 10,
),
Wrap(
spacing: 10,
runSpacing: 10,
alignment: WrapAlignment.center,
children: [
ElevatedButton(
onPressed: watcher.clientIsRunning && socket.isConnected
? () async {
try {
await httpClient.post('/lol-lobby/v2/lobby', body: {'queueId': 440});
} catch (err) {
print(err.toString());
}
}
: null,
child: const Text('CREATE FLEX LOBBY')),
ElevatedButton(
onPressed: watcher.clientIsRunning && socket.isConnected
? () async {
try {
await httpClient.post('/lol-lobby/v2/lobby', body: {'queueId': 420});
} catch (err) {
print(err.toString());
}
}
: null,
child: const Text('CREATE SOLO/DUO LOBBY')),
ElevatedButton(
onPressed: watcher.clientIsRunning && socket.isConnected && currentGameflowPhase == 'Lobby'
? () async {
try {
await httpClient.delete('/lol-lobby/v2/lobby');
} catch (err) {
print(err.toString());
}
}
: null,
child: const Text('LEAVE LOBBY')),
],
),
const SizedBox(
height: 10,
),
Flexible(
child: ListView.separated(
shrinkWrap: true,
itemBuilder: (_, index) {
return Text(
events[index].toString(),
style: TextStyle(fontSize: 12),
);
},
separatorBuilder: (_, __) {
return const SizedBox(height: 10);
},
itemCount: events.length,
),
)
],
),
),
);
}
Widget _buildStatusDot(bool success) {
return Container(
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: success ? Colors.green : Colors.red,
),
);
}
}
更多关于Flutter未知功能插件gangplank的潜在用途的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter未知功能插件gangplank的潜在用途的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
关于Flutter中未知功能插件gangplank
的潜在用途,由于gangplank
并非一个广为人知的官方或广泛使用的Flutter插件,我无法提供确切的文档或广泛认可的用例。不过,我可以根据插件名称和一般Flutter插件开发的原理,给出一个假设性的代码案例,以展示如何探索和使用一个假想的gangplank
插件。
请注意,以下代码是基于假设的,实际使用时需要根据gangplank
插件的真实API进行调整。
假设性的gangplank
插件使用案例
1. 假设gangplank
插件提供了跨平台文件访问功能
import 'package:flutter/material.dart';
import 'package:gangplank/gangplank.dart'; // 假设这是gangplank插件的导入路径
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Gangplank Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Gangplank Demo'),
),
body: Center(
child: GangplankDemo(),
),
),
);
}
}
class GangplankDemo extends StatefulWidget {
@override
_GangplankDemoState createState() => _GangplankDemoState();
}
class _GangplankDemoState extends State<GangplankDemo> {
String _fileContent = '';
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('File Content:'),
Text(_fileContent, style: TextStyle(fontSize: 20)),
SizedBox(height: 20),
ElevatedButton(
onPressed: _readFile,
child: Text('Read File'),
),
],
);
}
Future<void> _readFile() async {
try {
// 假设gangplank插件有一个readFile方法,用于读取文件内容
String filePath = '/path/to/your/file.txt'; // 替换为实际文件路径
String content = await Gangplank.readFile(filePath);
setState(() {
_fileContent = content;
});
} catch (e) {
print('Error reading file: $e');
}
}
}
2. 假设gangplank
插件提供了与原生设备功能交互的接口
import 'package:flutter/material.dart';
import 'package:gangplank/gangplank.dart'; // 假设这是gangplank插件的导入路径
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Gangplank Native Feature Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Gangplank Native Feature Demo'),
),
body: Center(
child: NativeFeatureDemo(),
),
),
);
}
}
class NativeFeatureDemo extends StatefulWidget {
@override
_NativeFeatureDemoState createState() => _NativeFeatureDemoState();
}
class _NativeFeatureDemoState extends State<NativeFeatureDemo> {
String _nativeFeatureResult = '';
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Native Feature Result:'),
Text(_nativeFeatureResult, style: TextStyle(fontSize: 20)),
SizedBox(height: 20),
ElevatedButton(
onPressed: _invokeNativeFeature,
child: Text('Invoke Native Feature'),
),
],
);
}
Future<void> _invokeNativeFeature() async {
try {
// 假设gangplank插件有一个invokeNativeMethod方法,用于调用原生设备功能
dynamic result = await Gangplank.invokeNativeMethod('someNativeMethodName');
setState(() {
_nativeFeatureResult = result?.toString() ?? 'No result';
});
} catch (e) {
print('Error invoking native feature: $e');
}
}
}
结论
上述代码案例是基于对gangplank
插件功能的假设而编写的,实际使用时需要根据插件的真实API文档进行调整。如果gangplank
插件提供了不同的功能,如网络通信、硬件访问等,代码结构和API调用方式也会有所不同。因此,在使用任何第三方插件之前,建议查阅该插件的官方文档或源代码,以了解其提供的具体功能和API。