Giter VIP home page Giter VIP logo

racedirector's Introduction

RaceDirector

Build Deploy to Pages

RaceDirector is the sim racer's Swiss Army knife.

The main software implements a pluggable architecture and a default set of components for sim racing. Plugins can be written to extend it with new functionality. In this early development phase, some plugins come bundled with it:

Install

At the moment there is no installer.

The package can be found in the "Artifacts" section of the latest build.

Run

Execute RaceDirector.exe from the installation directory.

The software can be configured editing the application.json file in the same location.

Build

.NET 6 is required to build, run tests and publish the artefacts:

dotnet test
dotnet publish

RaceDirector.exe will then be found in src\RaceDirector\bin\Debug\net6.0\publish.

racedirector's People

Contributors

paoloambrosio avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

racedirector's Issues

ACC memory-mapped file Race telemetry

Implement telemetry for ACC with what provided by memory-mapped files. Relates to #49 and #50.

  • Read MM telemetry
    • Error if Major version doesn't match
    • Handle absence of MM files
    • Handle reading from 3 files independently
    • Cool off period before trying to open the file again (e.g. from 15ms to 1s poll time - simulated by skipping reads)
  • Use the current MM data to create telemetry (what fields?)
  • Gaming modes
    • Menu
    • Race (SP, MP driving, MP connected)
  • Update documentation

PitCrew server URL does not accept hostnames

It only accepts IP addresses. Stack trace:

Unhandled exception. System.FormatException: An invalid IP address was specified.
 ---> System.Net.Sockets.SocketException (10022): An invalid argument was supplied.
   --- End of inner exception stack trace ---
   at System.Net.IPAddressParser.Parse(ReadOnlySpan`1 ipSpan, Boolean tryParse)
   at System.Net.IPAddress.Parse(String ipString)
   at NetCoreServer.TcpClient..ctor(String address, Int32 port)
   at NetCoreServer.HttpClient..ctor(String address, Int32 port)
   at NetCoreServer.WsClient..ctor(String address, Int32 port)
   at RaceDirector.Remote.Networking.Client.WsClient`2..ctor(Uri uri, ICodec`2 codec) in D:\a\RaceDirector\RaceDirector\src\Remote.Networking\Client\WsClient.cs:line 21
   at RaceDirector.Remote.Networking.Client.WsClient`2..ctor(String uri, ICodec`2 codec) in D:\a\RaceDirector\RaceDirector\src\Remote.Networking\Client\WsClient.cs:line 14
   at RaceDirector.PitCrew.PitCrewClient..ctor(String serverUrl) in D:\a\RaceDirector\RaceDirector\src\PitCrew.Plugin\PitCrewClient.cs:line 16
   at RaceDirector.PitCrew.Plugin.<>c__DisplayClass1_0.<Init>b__0(IServiceProvider _) in D:\a\RaceDirector\RaceDirector\src\PitCrew.Plugin\Plugin.cs:line 16
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at RaceDirector.DependencyInjection.ServiceCollectionServiceExtensions.<>c__4`1.<AddInterfaces>b__4_0(IServiceProvider s) in D:\a\RaceDirector\RaceDirector\src\RaceDirector.Plugin\DependencyInjection.cs:line 37
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitIEnumerable(IEnumerableCallSite enumerableCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitNoCache(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at RaceDirector.DependencyInjection.ServiceCollectionServiceExtensions.<>c__4`1.<AddInterfaces>b__4_0(IServiceProvider s) in D:\a\RaceDirector\RaceDirector\src\RaceDirector.Plugin\DependencyInjection.cs:line 37
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitIEnumerable(IEnumerableCallSite enumerableCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitNoCache(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetServices[T](IServiceProvider provider)
   at RaceDirector.Program.Main(String[] args) in D:\a\RaceDirector\RaceDirector\src\RaceDirector\Program.cs:line 30

RaceRoom telemetry

Expose all fields required by Sector3 Studios WebHud in the same way as the official Dash.exe does.

  • Read telemetry and transform it to standardised format
    • WebHud fields (#6)
    • OtterHud fields (#47)
  • Gaming modes
    • Menu
    • Race (SP, MP)
    • Practice
    • Leaderboard
    • Replay (during race or standalone)
    • Spectator
    • Between sessions

Configuration

  • Load configuration at startup #52
  • Support per-user overrides (with config files in the user's directory)
  • Configuration reload when it changes (it requires reloading the pipeline!)
  • Implement config overrides per game
  • Write configuration from UI?

Installer

Tools:

Notes:

  • Most applications seem to use the MSI format. The MSIX format is required to publish to the Microsoft Store, but it comes with limitations.

See:

Setup CD

Setup GitHub Actions to build

Do:

  • Evolve interfaces separately from implementations (semantic versioning is very important here)
  • Consider StyleCop Analyzers

Links:

Separate node connect from start

Currently there is a chance that the server won't start if the observer is subscribed after the process is found. We should start producing events only when all subscribers are subscribed.

  • Support ConnectableObservable calling connect() after linking to all observers?

Expose one field from RaceRoom

  • Produce RaceRoom telemetry events
    • Poll MM file when the game is running (configurable, but start with 2Hz)
    • Publish only Simulation Time
  • Log simulation time

Notification icon

  • Display NotifyIcon
    • Transform the console application into a WinForms one.
    • Make sure that there is no console output (use ILogger everywhere!)
    • Windowless application
  • When a game is running
    • Change tooltip with game name
    • Change icon
  • Add "Exit" menu item

RaceRoom Race telemetry for WebHud

Expose all fields required by Sector3 Studios WebHud in the same way as the official Dash.exe does.

  • Read telemetry and transform it to standardise format
    • Error if Major version doesn't match
  • Expose it back on the WebSocket
    • It should run the WebHud (#34 split out of it)
  • Gaming modes
    • Menu
    • Race (SP, MP)
  • Update documentation

PitCrew fuel pit strategy

Relates to #50.

  • Read fuel to add from the game telemetry and send it to the server
    • Implement fuel left for ACC (already implemented in R3E)
  • Web clients to send pit strategy command back to RaceDirector (and all other connected clients)
    • Automatic, since server doesn't need to understand the protocol
  • Pit strategy converted into game commands
    • Define an interface that game support can implement with IGame (to be renamed to IBaseGame for the BasePlugin) or separately only implementing IPitCrewGame
    • ACC
    • R3E
  • Game commands sent as keyboard inputs
    • Create a RaceDirector.DeviceIO project (and RaceDirector.DeviceIO.Interface) to send key strokes
  • Expose pit strategy request as an Observer for other plugins (e.g. voice commands) to send pit strategy requests
    • Create PitCrew.Interface project
  • Docs

General-purpose HUD

Expose generic HUD to be used with all supported games.

  • Always-on-top full screen window displaying a transparent Web page
  • Configurable URL, initially supporting RaceRoom's telemetry format

Startup time configuration

  • Load configuration from file
  • Support command line argument overrides
  • Plugin-specific configuration sections

Notes:

  • Only global configuration file, no plugin defaults (for now)

PitCrew basic Web UI

Relates to #50.
Implement Web UI.

  • Change elements in the page when receiving telemetry
  • Send request from form submit

Notes:

  • Use GitHub pages and have the server URL in a text input
    • Optionally, populate from GET parameter
  • JavaScript client
    • Plain JS code, no libraries/frameworks

Events might be missed if nodes are linked after they are produced

There might be an edge case when source nodes start emitting events before they are linked together. Example: process monitor notifies that a game is running, but either the telemetry reader doesn't start polling telemetry or the telemetry server doesn't start the server.

ACC telemetry

Implement telemetry for ACC with what provided by memory-mapped files and UDP socket.

  • Read MM telemetry
  • Receive UDP telemetry (requires #28 or it would be a lot of work for nothing)
    • Ignore UDP telemetry data if older than the current session
    • Handle reconnections if socket can't be opened or if it closes
  • Use the current MM data and the latest received UDP data
  • Support all gaming modes
    • Menu
    • Race (SP, MP driving, MP connected)
    • Practice
    • Replay (during race or standalone)
    • Spectator
    • Between sessions

Notes:

Automatically build pipeline

Wire pipeline graph based on properties of pipeline nodes.

  • Allow linking a source to targets of its sub-types (for versioning)
  • Use broadcast only when multiple links
  • Link all sources and target even when node is incomplete (i.e. at least one source or target is unlinked) 1
  • Nodes that are both source and target to count as both source and target 1

Do not

  • Introduce dependency injection (pass the constructed nodes) #15
  • Introduce the Plugin interface just yet #7

Footnotes

  1. Can fix this later if/when needed 2

JoyToKey timed press

  • Support short or long key presses (example from ACC)

Example configuration and expected behaviour:

usb_input:
- device: Fanatec CSL Elite
  rules:
  - controls:
    - Button 4
    events:
    - name: flasher
    - min_press_ms: 500
      name: toggle_lights
  • If TimeReleased-TimePressed < 500ms (the button is released within 500ms), send KeyDown and KeyUp at TimeReleased.
  • Otherwise, send KeyDown at TimePressed+500ms, send KeyUp at TimeReleased.
  • Note: as timestamp for the event KeyDown always uses TimePressed and KeyUp uses TimeReleased, no matter when they are generated.

We should be able to support delays for multiple button combinations (to avoid generating events for the single buttons being pressed):

usb_input:
- device: Fanatec CSL Elite
  rules:
  - controls:
    - Button 1
    - Button 2
    events:
    - name: together
  - controls:
    - Button 1
    events:
    - min_press_ms: 50
      name: one
  - controls:
    - Button 2
    events:
    - min_press_ms: 50
      name: two

Expose telemetry as Web Socket

  • Separate project so that it can later be extracted as a plugin
  • Start Web Socket server when game starts
  • Push telemetry as JSON
    • RaceRoom format for now
    • Consider versioning; will be classic API versioning, not same as for the pipeline
    • Consider endpoint for our own format, using native JSON formats (e.g. no TimeSpan)

PitCrew Walking Skeleton

Relates to #50.

  • Server
    • One WebSocket endpoint to receive and send telemetry
    • When telemetry is received, it is sent to all other connected clients
  • RaceDirector Client
    • Connect to server when games start, disconnect when they stop
      • Only if the server address is configured plugin is enabled
    • Export Player.Fuel.Left from telemetry to server

Out of scope:

  • Authentication
  • Configurable rate different from telemetry (default 1Hz)
  • Web client: we should connect using a WebSocket client at this point
  • ACC telemetry: part of #35

Game Listener

Write to console when a game is launched or closed.

  • Create a console application for now.
  • Structure projects according to vision (main, standard plugins implementation and interfaces).
  • Write to console if a game is running or not
    • If more than one game, keep the previous one detected
    • If no previous game detected, choose one
  • Make sure that dotnet commands work

Do not

  • Implement plugin interface
  • Implement UI
  • Setup CI build (extracted as #3)

ACC pit menu navigator improvements

Relates to #50.

  • Avoid doing "Left Left Right" to detect where we are.
    • We must pass down telemetry to know how much fuel we have.
  • Use tyre set from strategy if not requested.
  • Use global tyre pressure adjustment when all tyres need adjusting.
    • Can be used only if all deltas are positive or all are negative.

Switch to a modern reactive framework

The Dataflow API has some major drawbacks:

  • It does not provide building blocks to create sources from scratch (e.g. UDP client) without writing a lot of code
  • Despite being in the .NET framework, it is a niche framework

Replace Dataflow with Rx.NET (see analysis below for why that was chosen).

PitCrew tyre pit strategy

Relates to #50, depends on #68.

  • Send tyre telemetry to clients
  • Send configured tyre pit strategy to clients (if available)
  • Receive and apply tyre pit strategy change
  • Docs

Focus game window

Relates to #50.

Focus game window before applying pit strategy.

  • It should be done at DeviceIO level.
  • Do it if "enough time" has passed since last time and a key press needs to be sent.

Make WebSocket minor version configurable

To support Sector 3's WebHub in #6, we need

  • Minor version exposed by the WebSocket telemetry to be overridable to 8.
  • Make it configurable via config (#15)
  • Update docs to show how to use a command line argument to override it

RaceRoom 0.9.2.38 telemetry update

Client 0.9.2.38 updates the MM version from 2.9 to 2.10 with:

  • added engineState for all drivers (-1 unavailable, 0 = ign off, 1 = ign on but not running, 2 = ign on and running)
  • added lap_valid_state for player (-1 = N/A, 0 = this and next lap valid, 1 = this lap invalid, 2 = this and next lap invalid)
  • added traction_control_percent for player (-1.0 unavailable, 0.0 -> 100.0 percent)

JoyToKey

Replicate JoyToKey:

  • Generate events on USB button inputs
  • Generate key presses on events
    • It should stay pressed until the button is released
  • Node to transform one into the other
  • Support button combinations (multiple buttons simultaneously) to produce a single key press
    • Each device is independent, no cross-device combinations

Example configuration:

usb_input:
- device: Fanatec CSL Elite
  rules:
  - controls:
    - Button 1
    - Button 7
    events:
    - name: look_back
  - controls:
    - Button 1
    events:
    - name: look_left
  - controls:
    - Button 7
    events:
    - name: look_right
keyboard_output:
  look_back:
  - scan_code: LookBack_ScanCode 
  look_left:
  - scan_code: LookLeft_ScanCode
  look_right:
  - scan_code: LookRight_ScanCode

Depends on #15.

Do not:

  • Support short or long key presses (separate issue #29)

I/O libraries and methods:

Variable-speed telemetry

Split telemetry into three categories:

  • semi-static telemetry (track, car and driver information, etc.)
  • low-speed telemetry (e.g. lap times, deltas, etc.) to be used visually
  • high-speed telemetry (i.e. physics and inputs) to be used for FFB and recording

Plugin interface

Extract the WebSocket server as a plugin that is discovered at runtime.

  • Split plugin interface into separate project from the "core plugin" interface (that provides built in nodes and services). DI extension is part of it.
  • Plugin's Init method to register classes for DI
  • Discover plugins in the currently loaded assemblies Return a hardcoded list of plugins because assembly loading is lazy and, even trying to force loading referenced assemblies doesn't seem to work for local project dependencies.

Do not:

  • Dynamically load assemblies (see #21).

See :

RaceRoom Race telemetry for OtterHud

Depends on #6.

  • Include all fields that are used by OtterHUD
  • Update documentation

Notes:

  • OtterHUD is closed source, so we can't really be sure we expose everything that's needed unless we expose everything. We could look at what fields of data (e.g. data.VehicleInfo.UserId) are used, but reading obfuscated code just to find nested references would drive anyone insane.

Increase telemetry polling time

  • Set polling time to 15ms/~66Hz (configured system-wide, not per game)

Notes:

Dependency Injection

  • Introduce DI

Do not:

  • Use ILogger (moved to #18)

Create interface for configurations and register a fixed one before #15 is implemented.

PitCrew (PitManager clone)

Implement what RST PitManager (video) or ACC Drive (video) provide.

  • Server

    • Serve Web client page
    • Receive telemetry, MFD state and MFD changes and broadcast to all connected clients
    • Support one race/team at a time
    • Single-instance in-memory storage No storage
    • Basic Authentication
      • Bearer would work better for client connections, but not for browser
      • mTLS could work for both but it might be a faff to set up (e.g. for browser)
  • RaceDirector Client

    • Export telemetry when the Player field is present (rename it to LocalPlayer?)
      • Telemetry
        • Driver stint time
        • Fuel left
        • Fuel per lap
        • Tyre pressures
        • Tyre set
      • MFD state
        • Fuel to add
        • Tyres
          • Set
          • Pressures
    • Receive MFD changes from server and configure the in-game MFD (video)
      • Fuel to add
      • Tyres
        • Set
        • Compound
        • Pressures
      • Only if the game window is focused
  • Web Client

    • Receive telemetry and MFD status from server and display it
    • Send MFD changes to server
    • Display requested MFD changes (useful to display when others push changes)

image

Out of scope:

  • Client to receive remote telemetry and expose it to SecondMonitor
  • Support multiple races at a time (can replace static "username" with race hash)
  • ...

Links:

  • How CrewChief does it for R3E and ACC.

Split out modules

  • Move out the general "node" logic and utilities to a "DirectorFramework` module
  • Move the Web server logic to a Remote module. Keep the RaceRoom implementation in HUD (it will be further split up when we start implementing overlay support for other games).

Serve PitCrew UI

Relates to #50.

  • Serve PitCrew UI from server
    • Root for WebSocket endpoint, /ui for the Web interface
    • Configuration option to disable Web interface
  • Publish page from main instead of gh-pages branch
    • Publish to PitCrew
    • Add root index page
  • Update docs with both GitHub page and standalone serving

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.