Flutter未知功能插件gangplank的潜在用途

发布于 1周前 作者 vueper 来自 Flutter

Flutter未知功能插件gangplank的潜在用途

Gangplank 是一个用于简化使用 LCU(League Client Update)API 的插件。它提供了多种功能,如监听 League 客户端的启动和关闭、WebSocket 连接、HTTP 请求等。本插件目前仅支持 Windows 和 macOS。

示例应用

示例截图1 示例截图2

功能特性

  1. LCUWatcher:监听你的 League 客户端并通知你客户端的启动和关闭。
  2. LCUSocket:负责 WebSocket 连接。它连接到 League 客户端,你可以订阅你想监听的事件。
  3. LCUHttpClient:提供最常用的 HTTP 方法,用于向 League 客户端发送 HTTP 请求(例如创建大厅、开始匹配等)。
  4. LCULiveGameWatcher:监听你的游戏客户端并以多种方式通知你(见下文)。

使用方法

预备工作

LCUWatcher, LCUSocket, LCUHttpClientLCULiveGameWatcher 应该通过 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 事件之前,你不能使用 LCUSocketLCUHttpClient。当 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 客户端运行,而 LCUSocketLCUHttpClient 则依赖于 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

1 回复

更多关于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。

回到顶部